Compare commits

..

17 Commits

Author SHA1 Message Date
Shadowfacts 66fe861442
Merge branch 'master' into multiple-accounts 2020-01-18 19:33:01 -05:00
Shadowfacts 11f9642cba
Actually fix link interaction 2020-01-18 19:32:39 -05:00
Shadowfacts 6421d4dc12
Merge branch 'master' into multiple-accounts 2020-01-18 18:56:36 -05:00
Shadowfacts 38085eee37
Fix not being able to collapse/expand statuses
Instead of simply returning the content text view from hitTest(_:with:),
we need to call the super method so that the system still performs its
own checks.
2020-01-18 18:38:00 -05:00
Shadowfacts e19364abdf
Fix content text view text color in dark mode 2020-01-18 18:21:01 -05:00
Shadowfacts fa358a3e97
Remove xtra padding from content text view 2020-01-18 16:27:18 -05:00
Shadowfacts 5d86b35672
Enable text selection in conversation main status 2020-01-18 16:18:32 -05:00
Shadowfacts 784c71342d
Fix preformatted text not being displayed correctly 2020-01-18 16:05:44 -05:00
Shadowfacts b5a41badcc
Replace content labels with text views
UITextView uses TextKit internally, unlike UILabel, so no additional
code is needed to keep the TextKit and view representations of the text
in sync since they are one and the same. This means that detecting which
character was tapped in a content text view is much more accurate, which
means link handling is substantially imrpoved.

Fixes #20
2020-01-18 16:05:00 -05:00
Shadowfacts 23de131290
Add preference to require attachment descriptions before posting
Closes #76
2020-01-17 21:55:21 -05:00
Shadowfacts 8178a1f339
Fix crash when tapping more actions buttons on iPad
Fixes #78
2020-01-17 21:29:53 -05:00
Shadowfacts 53702a8324
Add pinned status refreshing
Closes #82
2020-01-17 21:13:17 -05:00
Shadowfacts bb86e1aafd
Allow rotaion in the attachment gallery
Closes #77
2020-01-07 22:19:38 -05:00
Shadowfacts db4312ee34
Fix refreshing multiple times with no new data not working
When the requested range has no results, no pagination data is returned,
so the existing `newer` request range is replaced with nil. As there
was no new data, the existing request range is still correct and should
not be replaced.

Fixes #75
2020-01-07 21:54:19 -05:00
Shadowfacts ec2062ad42
Fix not being able to sign into Mastodon instances not in the
recommended list
2020-01-06 22:14:17 -05:00
Shadowfacts 1e066ac28e
Add installation instructions to readme 2020-01-06 20:35:11 -05:00
Shadowfacts 29e0128a55
Fix broken file paths 2020-01-06 20:27:58 -05:00
34 changed files with 650 additions and 598 deletions

View File

@ -1,3 +1,15 @@
# Tusker
Tusker is a WIP iOS app for Mastodon and Pleroma.
## Installing for Development
Xcode 11 is required, macOS Mojave or later should work (only macOS Catalina is regularly tested).
1. Clone the project: `git clone https://git.shadowfacts.net/shadowfacts/Tusker.git`
2. Change directory into the project: `cd Tusker`
3. Clone the submodules: `git submodule init && git submodule update`
4. Open `Tusker.xcworkspace` in Xcode.
5. Change the code signing identity to your own.
6. Change the bundle identifier to something unique.
7. Select a target in the Tusker scheme and build & run.

View File

@ -12,7 +12,6 @@
0427033822B30F5F000D31B6 /* BehaviorPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0427033722B30F5F000D31B6 /* BehaviorPrefsView.swift */; };
0427033A22B31269000D31B6 /* AdvancedPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0427033922B31269000D31B6 /* AdvancedPrefsView.swift */; };
0427037C22B316B9000D31B6 /* SilentActionPrefs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0427037B22B316B9000D31B6 /* SilentActionPrefs.swift */; };
04496BD721625361001F1B23 /* ContentLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04496BD621625361001F1B23 /* ContentLabel.swift */; };
0450531F22B0097E00100BA2 /* Timline+UI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0450531E22B0097E00100BA2 /* Timline+UI.swift */; };
0454DDAF22B462EF00B8BB8E /* GalleryExpandAnimationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0454DDAE22B462EF00B8BB8E /* GalleryExpandAnimationController.swift */; };
0454DDB122B467AA00B8BB8E /* GalleryShrinkAnimationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0454DDB022B467AA00B8BB8E /* GalleryShrinkAnimationController.swift */; };
@ -73,6 +72,10 @@
D61AC1D5232E9FA600C54D2D /* InstanceSelectorTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61AC1D4232E9FA600C54D2D /* InstanceSelectorTableViewController.swift */; };
D61AC1D8232EA42D00C54D2D /* InstanceTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61AC1D6232EA42D00C54D2D /* InstanceTableViewCell.swift */; };
D61AC1D9232EA42D00C54D2D /* InstanceTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D61AC1D7232EA42D00C54D2D /* InstanceTableViewCell.xib */; };
D620483223D2A6A3008A63EF /* CompositionState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483123D2A6A3008A63EF /* CompositionState.swift */; };
D620483423D3801D008A63EF /* LinkTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483323D3801D008A63EF /* LinkTextView.swift */; };
D620483623D38075008A63EF /* ContentTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483523D38075008A63EF /* ContentTextView.swift */; };
D620483823D38190008A63EF /* StatusContentTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483723D38190008A63EF /* StatusContentTextView.swift */; };
D626493323BD751600612E6E /* ShowCameraCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D626493123BD751600612E6E /* ShowCameraCollectionViewCell.xib */; };
D626493523BD94CE00612E6E /* CompositionAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D626493423BD94CE00612E6E /* CompositionAttachment.swift */; };
D626493823C0FD0000612E6E /* AllPhotosTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D626493623C0FD0000612E6E /* AllPhotosTableViewCell.swift */; };
@ -200,7 +203,6 @@
D6BED170212663DA00F02DA0 /* SwiftSoup.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D6BED16E212663DA00F02DA0 /* SwiftSoup.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
D6BED174212667E900F02DA0 /* TimelineStatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BED173212667E900F02DA0 /* TimelineStatusTableViewCell.swift */; };
D6C693EF216192C2007D6A6D /* TuskerNavigationDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */; };
D6C693F92162E4DB007D6A6D /* StatusContentLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693F82162E4DB007D6A6D /* StatusContentLabel.swift */; };
D6C693FC2162FE6F007D6A6D /* LoadingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693FB2162FE6F007D6A6D /* LoadingViewController.swift */; };
D6C693FE2162FEEA007D6A6D /* UIViewController+Children.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693FD2162FEEA007D6A6D /* UIViewController+Children.swift */; };
D6C7D27D22B6EBF800071952 /* AttachmentsContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C7D27C22B6EBF800071952 /* AttachmentsContainerView.swift */; };
@ -211,7 +213,6 @@
D6D4DDDA212518A200E1C4BB /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D6D4DDD8212518A200E1C4BB /* LaunchScreen.storyboard */; };
D6D4DDE5212518A200E1C4BB /* TuskerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D4DDE4212518A200E1C4BB /* TuskerTests.swift */; };
D6D4DDF0212518A200E1C4BB /* TuskerUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D4DDEF212518A200E1C4BB /* TuskerUITests.swift */; };
D6D58DF922074B74009C8DD9 /* LinkLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D58DF822074B74009C8DD9 /* LinkLabel.swift */; };
D6DD353D22F28CD000A9563A /* ContentWarningCopyMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD353C22F28CD000A9563A /* ContentWarningCopyMode.swift */; };
D6DD353F22F502EC00A9563A /* Preferences+Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD353E22F502EC00A9563A /* Preferences+Notification.swift */; };
D6E0DC8E216EDF1E00369478 /* Previewing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E0DC8D216EDF1E00369478 /* Previewing.swift */; };
@ -285,7 +286,6 @@
0427033722B30F5F000D31B6 /* BehaviorPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BehaviorPrefsView.swift; sourceTree = "<group>"; };
0427033922B31269000D31B6 /* AdvancedPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedPrefsView.swift; sourceTree = "<group>"; };
0427037B22B316B9000D31B6 /* SilentActionPrefs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SilentActionPrefs.swift; sourceTree = "<group>"; };
04496BD621625361001F1B23 /* ContentLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentLabel.swift; sourceTree = "<group>"; };
0450531E22B0097E00100BA2 /* Timline+UI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Timline+UI.swift"; sourceTree = "<group>"; };
0454DDAE22B462EF00B8BB8E /* GalleryExpandAnimationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryExpandAnimationController.swift; sourceTree = "<group>"; };
0454DDB022B467AA00B8BB8E /* GalleryShrinkAnimationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryShrinkAnimationController.swift; sourceTree = "<group>"; };
@ -348,6 +348,10 @@
D61AC1D4232E9FA600C54D2D /* InstanceSelectorTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceSelectorTableViewController.swift; sourceTree = "<group>"; };
D61AC1D6232EA42D00C54D2D /* InstanceTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceTableViewCell.swift; sourceTree = "<group>"; };
D61AC1D7232EA42D00C54D2D /* InstanceTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = InstanceTableViewCell.xib; sourceTree = "<group>"; };
D620483123D2A6A3008A63EF /* CompositionState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositionState.swift; sourceTree = "<group>"; };
D620483323D3801D008A63EF /* LinkTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkTextView.swift; sourceTree = "<group>"; };
D620483523D38075008A63EF /* ContentTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentTextView.swift; sourceTree = "<group>"; };
D620483723D38190008A63EF /* StatusContentTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusContentTextView.swift; sourceTree = "<group>"; };
D626493123BD751600612E6E /* ShowCameraCollectionViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ShowCameraCollectionViewCell.xib; sourceTree = "<group>"; };
D626493423BD94CE00612E6E /* CompositionAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositionAttachment.swift; sourceTree = "<group>"; };
D626493623C0FD0000612E6E /* AllPhotosTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllPhotosTableViewCell.swift; sourceTree = "<group>"; };
@ -473,7 +477,6 @@
D6BED16E212663DA00F02DA0 /* SwiftSoup.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SwiftSoup.framework; sourceTree = BUILT_PRODUCTS_DIR; };
D6BED173212667E900F02DA0 /* TimelineStatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStatusTableViewCell.swift; sourceTree = "<group>"; };
D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TuskerNavigationDelegate.swift; sourceTree = "<group>"; };
D6C693F82162E4DB007D6A6D /* StatusContentLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusContentLabel.swift; sourceTree = "<group>"; };
D6C693FB2162FE6F007D6A6D /* LoadingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingViewController.swift; sourceTree = "<group>"; };
D6C693FD2162FEEA007D6A6D /* UIViewController+Children.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Children.swift"; sourceTree = "<group>"; };
D6C7D27C22B6EBF800071952 /* AttachmentsContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentsContainerView.swift; sourceTree = "<group>"; };
@ -490,9 +493,8 @@
D6D4DDEB212518A200E1C4BB /* TuskerUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TuskerUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
D6D4DDEF212518A200E1C4BB /* TuskerUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TuskerUITests.swift; sourceTree = "<group>"; };
D6D4DDF1212518A200E1C4BB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
D6D58DF822074B74009C8DD9 /* LinkLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkLabel.swift; sourceTree = "<group>"; };
D6DD353C22F28CD000A9563A /* ContentWarningCopyMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ContentWarningCopyMode.swift; path = ../../../../../../../System/Volumes/Data/Users/shadowfacts/Dev/iOS/Tusker/Tusker/Preferences/ContentWarningCopyMode.swift; sourceTree = "<group>"; };
D6DD353E22F502EC00A9563A /* Preferences+Notification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "Preferences+Notification.swift"; path = "../../../../../../../System/Volumes/Data/Users/shadowfacts/Dev/iOS/Tusker/Tusker/Preferences/Preferences+Notification.swift"; sourceTree = "<group>"; };
D6DD353C22F28CD000A9563A /* ContentWarningCopyMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentWarningCopyMode.swift; sourceTree = "<group>"; };
D6DD353E22F502EC00A9563A /* Preferences+Notification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Preferences+Notification.swift"; sourceTree = "<group>"; };
D6E0DC8D216EDF1E00369478 /* Previewing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Previewing.swift; sourceTree = "<group>"; };
D6E6F26221603F8B006A8599 /* CharacterCounter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterCounter.swift; sourceTree = "<group>"; };
D6E6F26421604242006A8599 /* CharacterCounterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterCounterTests.swift; sourceTree = "<group>"; };
@ -862,6 +864,7 @@
D66362702136338600C9CBA2 /* ComposeViewController.swift */,
D626493423BD94CE00612E6E /* CompositionAttachment.swift */,
D6285B5221EA708700FE4B39 /* StatusFormat.swift */,
D620483123D2A6A3008A63EF /* CompositionState.swift */,
);
path = Compose;
sourceTree = "<group>";
@ -1108,9 +1111,9 @@
D6BED1722126661300F02DA0 /* Views */ = {
isa = PBXGroup;
children = (
D6D58DF822074B74009C8DD9 /* LinkLabel.swift */,
04496BD621625361001F1B23 /* ContentLabel.swift */,
D6C693F82162E4DB007D6A6D /* StatusContentLabel.swift */,
D620483323D3801D008A63EF /* LinkTextView.swift */,
D620483523D38075008A63EF /* ContentTextView.swift */,
D620483723D38190008A63EF /* StatusContentTextView.swift */,
D641C77E213DC78A004B4513 /* InlineTextAttachment.swift */,
04ED00B021481ED800567C53 /* SteppedProgressView.swift */,
D627943A23A55BA600D38C68 /* NavigableTableViewCell.swift */,
@ -1630,9 +1633,7 @@
04DACE8C212CB14B009840C4 /* MainTabBarViewController.swift in Sources */,
D6AEBB412321642700E5038B /* SendMesasgeActivity.swift in Sources */,
D6BC9DB1232C61BC002CA326 /* NotificationsPageViewController.swift in Sources */,
D6C693F92162E4DB007D6A6D /* StatusContentLabel.swift in Sources */,
D6BC9DB3232D4C07002CA326 /* WellnessPrefsView.swift in Sources */,
D6D58DF922074B74009C8DD9 /* LinkLabel.swift in Sources */,
0454DDAF22B462EF00B8BB8E /* GalleryExpandAnimationController.swift in Sources */,
D64BC18F23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.swift in Sources */,
D6A3BC8A2321F79B00FD64D5 /* AccountTableViewCell.swift in Sources */,
@ -1642,6 +1643,7 @@
D667E5F52135BCD50057A976 /* ConversationTableViewController.swift in Sources */,
D6945C3623AC6C09005C403C /* SavedInstancesManager.swift in Sources */,
D6C7D27D22B6EBF800071952 /* AttachmentsContainerView.swift in Sources */,
D620483823D38190008A63EF /* StatusContentTextView.swift in Sources */,
D6F953F021251A2900CF0F2B /* MastodonController.swift in Sources */,
0411610022B442870030A9B7 /* AttachmentViewController.swift in Sources */,
D611C2CF232DC61100C86A49 /* HashtagTableViewCell.swift in Sources */,
@ -1652,6 +1654,7 @@
D66362712136338600C9CBA2 /* ComposeViewController.swift in Sources */,
D6AC956723C4347E008C9946 /* SceneDelegate.swift in Sources */,
D6BC9DD7232D7811002CA326 /* TimelinesPageViewController.swift in Sources */,
D620483623D38075008A63EF /* ContentTextView.swift in Sources */,
D6028B9B2150811100F223B9 /* MastodonCache.swift in Sources */,
D6A3BC802321B7E600FD64D5 /* FollowNotificationGroupTableViewCell.swift in Sources */,
D627944D23A9A03D00D38C68 /* ListTimelineViewController.swift in Sources */,
@ -1707,9 +1710,9 @@
D626493823C0FD0000612E6E /* AllPhotosTableViewCell.swift in Sources */,
D627943B23A55BA600D38C68 /* NavigableTableViewCell.swift in Sources */,
D6333B792139AEFD00CE884A /* Date+TimeAgo.swift in Sources */,
D620483423D3801D008A63EF /* LinkTextView.swift in Sources */,
D641C77F213DC78A004B4513 /* InlineTextAttachment.swift in Sources */,
D627943523A5525100D38C68 /* StatusActivity.swift in Sources */,
04496BD721625361001F1B23 /* ContentLabel.swift in Sources */,
D663626C21361C6700C9CBA2 /* Account+Preferences.swift in Sources */,
D6333B372137838300CE884A /* AttributedString+Helpers.swift in Sources */,
D61AC1D8232EA42D00C54D2D /* InstanceTableViewCell.swift in Sources */,
@ -1729,6 +1732,7 @@
D6A3BC7C232195C600FD64D5 /* ActionNotificationGroupTableViewCell.swift in Sources */,
D6F953EC212519E700CF0F2B /* TimelineTableViewController.swift in Sources */,
04586B4122B2FFB10021BD04 /* PreferencesView.swift in Sources */,
D620483223D2A6A3008A63EF /* CompositionState.swift in Sources */,
D6BC9DB5232D4CE3002CA326 /* NotificationsMode.swift in Sources */,
D667E5EB21349EF80057A976 /* ProfileHeaderTableViewCell.swift in Sources */,
04D14BB022B34A2800642648 /* GalleryViewController.swift in Sources */,

View File

@ -88,6 +88,8 @@
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeRight</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>

View File

@ -45,6 +45,7 @@ class Preferences: Codable, ObservableObject {
self.defaultPostVisibility = try container.decode(Status.Visibility.self, forKey: .defaultPostVisibility)
self.automaticallySaveDrafts = try container.decode(Bool.self, forKey: .automaticallySaveDrafts)
self.contentWarningCopyMode = try container.decode(ContentWarningCopyMode.self, forKey: .contentWarningCopyMode)
self.requireAttachmentDescriptions = try container.decode(Bool.self, forKey: .requireAttachmentDescriptions)
self.blurAllMedia = try container.decode(Bool.self, forKey: .blurAllMedia)
self.openLinksInApps = try container.decode(Bool.self, forKey: .openLinksInApps)
self.useInAppSafari = try container.decode(Bool.self, forKey: .useInAppSafari)
@ -68,6 +69,7 @@ class Preferences: Codable, ObservableObject {
try container.encode(defaultPostVisibility, forKey: .defaultPostVisibility)
try container.encode(automaticallySaveDrafts, forKey: .automaticallySaveDrafts)
try container.encode(contentWarningCopyMode, forKey: .contentWarningCopyMode)
try container.encode(requireAttachmentDescriptions, forKey: .requireAttachmentDescriptions)
try container.encode(blurAllMedia, forKey: .blurAllMedia)
try container.encode(openLinksInApps, forKey: .openLinksInApps)
try container.encode(useInAppSafari, forKey: .useInAppSafari)
@ -90,6 +92,7 @@ class Preferences: Codable, ObservableObject {
@Published var defaultPostVisibility = Status.Visibility.public
@Published var automaticallySaveDrafts = true
@Published var contentWarningCopyMode = ContentWarningCopyMode.asIs
@Published var requireAttachmentDescriptions = false
@Published var blurAllMedia = false
@Published var openLinksInApps = true
@Published var useInAppSafari = true
@ -112,6 +115,7 @@ class Preferences: Codable, ObservableObject {
case defaultPostVisibility
case automaticallySaveDrafts
case contentWarningCopyMode
case requireAttachmentDescriptions
case blurAllMedia
case openLinksInApps
case useInAppSafari

View File

@ -39,6 +39,12 @@ class ComposeViewController: UIViewController {
weak var xcbSession: XCBSession?
var postedStatus: Status?
var compositionState: CompositionState = .valid {
didSet {
postBarButtonItem.isEnabled = compositionState.isValid
}
}
weak var postBarButtonItem: UIBarButtonItem!
var visibilityBarButtonItem: UIBarButtonItem!
var contentWarningBarButtonItem: UIBarButtonItem!
@ -135,6 +141,7 @@ class ComposeViewController: UIViewController {
// we have to set the font here, because the monospaced digit font is not available in IB
charactersRemainingLabel.font = .monospacedDigitSystemFont(ofSize: 17, weight: .regular)
updateCharactersRemaining()
updateAttachmentDescriptionsRequired()
updatePlaceholder()
NotificationCenter.default.addObserver(self, selector: #selector(contentWarningTextFieldDidChange), name: UITextField.textDidChangeNotification, object: contentWarningTextField)
@ -270,17 +277,29 @@ class ComposeViewController: UIViewController {
scrollView.scrollIndicatorInsets = scrollView.contentInset
}
func updateAttachmentDescriptionsRequired() {
if Preferences.shared.requireAttachmentDescriptions {
for case let mediaView as ComposeMediaView in attachmentsStackView.arrangedSubviews {
if mediaView.descriptionTextView.text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
compositionState.formUnion(.requiresAttachmentDescriptions)
return
}
}
}
compositionState.subtract(.requiresAttachmentDescriptions)
}
func updateCharactersRemaining() {
// TODO: include CW char count
let count = CharacterCounter.count(text: statusTextView.text)
let cwCount = contentWarningEnabled ? (contentWarningTextField.text?.count ?? 0) : 0
let remaining = (mastodonController.instance.maxStatusCharacters ?? 500) - count - cwCount
if remaining < 0 {
charactersRemainingLabel.textColor = .red
postBarButtonItem.isEnabled = false
compositionState.formUnion(.tooManyCharacters)
} else {
charactersRemainingLabel.textColor = .darkGray
postBarButtonItem.isEnabled = true
compositionState.subtract(.tooManyCharacters)
}
charactersRemainingLabel.text = String(remaining)
charactersRemainingLabel.accessibilityLabel = String(format: NSLocalizedString("%d characters remaining", comment: "compose characters remaining accessibility label"), remaining)
@ -458,7 +477,7 @@ class ComposeViewController: UIViewController {
saveDraft()
// disable post button while sending post request
postBarButtonItem.isEnabled = false
compositionState.formUnion(.currentlyPosting)
let contentWarning: String?
if contentWarningEnabled, let cwText = contentWarningTextField.text, !cwText.isEmpty {
@ -579,6 +598,7 @@ extension ComposeViewController: AssetPickerViewControllerDelegate {
}
func assetPicker(_ assetPicker: AssetPickerViewController, didSelectAttachments attachments: [CompositionAttachment]) {
selectedAttachments.append(contentsOf: attachments)
updateAttachmentDescriptionsRequired()
}
}
@ -587,6 +607,11 @@ extension ComposeViewController: ComposeMediaViewDelegate {
let index = attachmentsStackView.arrangedSubviews.firstIndex(of: mediaView)!
selectedAttachments.remove(at: index)
updateAddAttachmentButton()
updateAttachmentDescriptionsRequired()
}
func descriptionTextViewDidChange(_ mediaView: ComposeMediaView) {
updateAttachmentDescriptionsRequired()
}
}
@ -635,6 +660,8 @@ extension ComposeViewController: DraftsTableViewControllerDelegate {
// call the delegate method manually, since setting the text property doesn't call it
mediaView.textViewDidChange(mediaView.descriptionTextView)
}
updateAttachmentDescriptionsRequired()
}
func draftSelectionCompleted() {

View File

@ -0,0 +1,23 @@
//
// CompositionState.swift
// Tusker
//
// Created by Shadowfacts on 1/17/20.
// Copyright © 2020 Shadowfacts. All rights reserved.
//
import Foundation
struct CompositionState: OptionSet {
let rawValue: Int
static let currentlyPosting = CompositionState(rawValue: 1 << 0)
static let tooManyCharacters = CompositionState(rawValue: 1 << 1)
static let requiresAttachmentDescriptions = CompositionState(rawValue: 1 << 2)
static let valid: CompositionState = []
var isValid: Bool {
isEmpty
}
}

View File

@ -35,6 +35,14 @@ class GalleryViewController: UIPageViewController, UIPageViewControllerDataSourc
return viewControllers?.first
}
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
if UIDevice.current.userInterfaceIdiom == .phone {
return .allButUpsideDown
} else {
return .all
}
}
init(attachments: [Attachment], sourcesInfo: [LargeImageViewController.SourceInfo?], startIndex: Int) {
self.attachments = attachments
self.sourcesInfo = sourcesInfo

View File

@ -12,6 +12,14 @@ class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate {
let mastodonController: MastodonController
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
if UIDevice.current.userInterfaceIdiom == .phone {
return .portrait
} else {
return .all
}
}
init(mastodonController: MastodonController) {
self.mastodonController = mastodonController

View File

@ -211,7 +211,9 @@ class NotificationsTableViewController: EnhancedTableViewController {
self.mastodonController.cache.addAll(statuses: newNotifications.compactMap { $0.status })
self.mastodonController.cache.addAll(accounts: newNotifications.map { $0.account })
self.newer = pagination?.newer
if let newer = pagination?.newer {
self.newer = newer
}
DispatchQueue.main.async {
self.refreshControl?.endRefreshing()

View File

@ -29,6 +29,14 @@ class InstanceSelectorTableViewController: UITableViewController {
var urlHandler: AnyCancellable?
var currentQuery: String?
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
if UIDevice.current.userInterfaceIdiom == .phone {
return .portrait
} else {
return .all
}
}
init() {
super.init(style: .grouped)
@ -159,7 +167,9 @@ class InstanceSelectorTableViewController: UITableViewController {
}
switch item {
case let .selected(instance):
delegate.didSelectInstance(url: URL(string: instance.uri)!)
// we can't just turn the URI string from the API into a URL instance, because Mastodon only includes the domain in the "URI"
let components = parseURLComponents(input: instance.uri)
delegate.didSelectInstance(url: components.url!)
case let .recommended(instance):
var components = URLComponents()
components.scheme = "https"

View File

@ -40,6 +40,9 @@ struct BehaviorPrefsView: View {
Text("Prepend 're: '").tag(ContentWarningCopyMode.prependRe)
Text("Don't copy").tag(ContentWarningCopyMode.doNotCopy)
}
Toggle(isOn: $preferences.requireAttachmentDescriptions) {
Text("Require Attachment Descriptions")
}
}
}

View File

@ -222,12 +222,32 @@ class ProfileTableViewController: EnhancedTableViewController {
self.mastodonController.cache.addAll(statuses: newStatuses)
self.timelineSegments[0].insert(contentsOf: newStatuses.map { ($0.id, .unknown) }, at: 0)
self.newer = pagination?.newer
if let newer = pagination?.newer {
self.newer = newer
}
DispatchQueue.main.async {
self.refreshControl?.endRefreshing()
}
}
getStatuses(onlyPinned: true) { (response) in
guard case let .success(newPinnedStatuses, _) = response else { fatalError() }
self.mastodonController.cache.addAll(statuses: newPinnedStatuses)
let oldPinnedStatuses = self.pinnedStatuses
var pinnedStatuses = [(id: String, state: StatusState)]()
for status in newPinnedStatuses {
let state: StatusState
if let (_, oldState) = oldPinnedStatuses.first(where: { $0.id == status.id }) {
state = oldState
} else {
state = .unknown
}
pinnedStatuses.append((status.id, state))
}
self.pinnedStatuses = pinnedStatuses
}
}
@objc func composePressed(_ sender: Any) {
@ -247,7 +267,7 @@ extension ProfileTableViewController: StatusTableViewCellDelegate {
}
extension ProfileTableViewController: ProfileHeaderTableViewCellDelegate {
func showMoreOptions() {
func showMoreOptions(cell: ProfileHeaderTableViewCell) {
let account = mastodonController.cache.account(for: accountID)!
mastodonController.cache.relationship(for: account.id) { [weak self] (relationship) in
@ -262,6 +282,7 @@ extension ProfileTableViewController: ProfileHeaderTableViewCellDelegate {
DispatchQueue.main.async {
let activityController = UIActivityViewController(activityItems: [account.url, account], applicationActivities: customActivities)
activityController.completionWithItemsHandler = OpenInSafariActivity.completionHandler(viewController: self, url: account.url)
activityController.popoverPresentationController?.sourceView = cell.moreButtonVisualEffectView
self.present(activityController, animated: true)
}
}

View File

@ -131,6 +131,11 @@ class TimelineTableViewController: EnhancedTableViewController {
self.newer = pagination?.newer
self.mastodonController.cache.addAll(statuses: newStatuses)
self.timelineSegments[0].insert(contentsOf: newStatuses.map { ($0.id, .unknown) }, at: 0)
if let newer = pagination?.newer {
self.newer = newer
}
DispatchQueue.main.async {
self.refreshControl?.endRefreshing()

View File

@ -24,7 +24,7 @@ extension MenuPreviewProvider {
private var mastodonController: MastodonController? { navigationDelegate?.apiController }
func actionsForProfile(accountID: String) -> [UIAction] {
func actionsForProfile(accountID: String, sourceView: UIView?) -> [UIAction] {
guard let mastodonController = mastodonController,
let account = mastodonController.cache.account(for: accountID) else { return [] }
return [
@ -35,27 +35,27 @@ extension MenuPreviewProvider {
self.navigationDelegate?.compose(mentioning: account.acct)
}),
createAction(identifier: "share", title: "Share...", systemImageName: "square.and.arrow.up", handler: { (_) in
self.navigationDelegate?.showMoreOptions(forAccount: accountID)
self.navigationDelegate?.showMoreOptions(forAccount: accountID, sourceView: sourceView)
})
]
}
func actionsForURL(_ url: URL) -> [UIAction] {
func actionsForURL(_ url: URL, sourceView: UIView?) -> [UIAction] {
return [
createAction(identifier: "openinsafari", title: "Open in Safari", systemImageName: "safari", handler: { (_) in
self.navigationDelegate?.selected(url: url)
}),
createAction(identifier: "share", title: "Share...", systemImageName: "square.and.arrow.up", handler: { (_) in
self.navigationDelegate?.showMoreOptions(forURL: url)
self.navigationDelegate?.showMoreOptions(forURL: url, sourceView: sourceView)
})
]
}
func actionsForHashtag(_ hashtag: Hashtag) -> [UIAction] {
return actionsForURL(hashtag.url)
func actionsForHashtag(_ hashtag: Hashtag, sourceView: UIView?) -> [UIAction] {
return actionsForURL(hashtag.url, sourceView: sourceView)
}
func actionsForStatus(statusID: String) -> [UIAction] {
func actionsForStatus(statusID: String, sourceView: UIView?) -> [UIAction] {
guard let mastodonController = mastodonController,
let status = mastodonController.cache.status(for: statusID) else { return [] }
return [
@ -66,7 +66,7 @@ extension MenuPreviewProvider {
self.navigationDelegate?.selected(url: status.url!)
}),
createAction(identifier: "share", title: "Share...", systemImageName: "square.and.arrow.up", handler: { (_) in
self.navigationDelegate?.showMoreOptions(forStatus: statusID)
self.navigationDelegate?.showMoreOptions(forStatus: statusID, sourceView: sourceView)
})
]
}

View File

@ -46,11 +46,11 @@ protocol TuskerNavigationDelegate {
func showGallery(attachments: [Attachment], sourceViews: [UIImageView?], startIndex: Int)
func showMoreOptions(forStatus statusID: String)
func showMoreOptions(forStatus statusID: String, sourceView: UIView?)
func showMoreOptions(forAccount accountID: String)
func showMoreOptions(forAccount accountID: String, sourceView: UIView?)
func showMoreOptions(forURL url: URL)
func showMoreOptions(forURL url: URL, sourceView: UIView?)
func showFollowedByList(accountIDs: [String])
@ -60,8 +60,12 @@ protocol TuskerNavigationDelegate {
extension TuskerNavigationDelegate where Self: UIViewController {
func show(_ vc: UIViewController) {
if vc is LargeImageViewController || vc is GalleryViewController || vc is SFSafariViewController {
present(vc, animated: true)
} else {
show(vc, sender: self)
}
}
func selected(account accountID: String) {
// don't open if the account is the same as the current one
@ -184,7 +188,7 @@ extension TuskerNavigationDelegate where Self: UIViewController {
present(gallery(attachments: attachments, sourceViews: sourceViews, startIndex: startIndex), animated: true)
}
private func moreOptions(forURL url: URL) -> UIViewController {
private func moreOptions(forURL url: URL) -> UIActivityViewController {
let customActivites: [UIActivity] = [
OpenInSafariActivity()
]
@ -193,7 +197,7 @@ extension TuskerNavigationDelegate where Self: UIViewController {
return activityController
}
private func moreOptions(forStatus statusID: String) -> UIViewController {
private func moreOptions(forStatus statusID: String) -> UIActivityViewController {
guard let status = apiController.cache.status(for: statusID) else { fatalError("Missing cached status \(statusID)") }
guard let url = status.url else { fatalError("Missing url for status \(statusID)") }
var customActivites: [UIActivity] = [OpenInSafariActivity()]
@ -212,21 +216,27 @@ extension TuskerNavigationDelegate where Self: UIViewController {
return activityController
}
private func moreOptions(forAccount accountID: String) -> UIViewController {
private func moreOptions(forAccount accountID: String) -> UIActivityViewController {
guard let account = apiController.cache.account(for: accountID) else { fatalError("Missing cached account \(accountID)") }
return moreOptions(forURL: account.url)
}
func showMoreOptions(forStatus statusID: String) {
present(moreOptions(forStatus: statusID), animated: true)
func showMoreOptions(forStatus statusID: String, sourceView: UIView?) {
let vc = moreOptions(forStatus: statusID)
vc.popoverPresentationController?.sourceView = sourceView
present(vc, animated: true)
}
func showMoreOptions(forURL url: URL) {
present(moreOptions(forURL: url), animated: true)
func showMoreOptions(forURL url: URL, sourceView: UIView?) {
let vc = moreOptions(forURL: url)
vc.popoverPresentationController?.sourceView = sourceView
present(vc, animated: true)
}
func showMoreOptions(forAccount accountID: String) {
present(moreOptions(forAccount: accountID), animated: true)
func showMoreOptions(forAccount accountID: String, sourceView: UIView?) {
let vc = moreOptions(forAccount: accountID)
vc.popoverPresentationController?.sourceView = sourceView
present(vc, animated: true)
}
func showFollowedByList(accountIDs: [String]) {

View File

@ -70,10 +70,9 @@ extension AccountTableViewCell: MenuPreviewProvider {
func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? {
guard let mastodonController = mastodonController else { return nil }
return (content: {
ProfileTableViewController(accountID: self.accountID, mastodonController: mastodonController)
}, actions: {
self.actionsForProfile(accountID: self.accountID)
})
return (
content: { ProfileTableViewController(accountID: self.accountID, mastodonController: mastodonController) },
actions: { self.actionsForProfile(accountID: self.accountID, sourceView: self.avatarImageView) }
)
}
}

View File

@ -12,6 +12,7 @@ import AVFoundation
protocol ComposeMediaViewDelegate {
func didRemoveMedia(_ mediaView: ComposeMediaView)
func descriptionTextViewDidChange(_ mediaView: ComposeMediaView)
}
class ComposeMediaView: UIView {
@ -69,5 +70,6 @@ class ComposeMediaView: UIView {
extension ComposeMediaView: UITextViewDelegate {
func textViewDidChange(_ textView: UITextView) {
placeholderLabel.isHidden = !descriptionTextView.text.isEmpty
delegate?.descriptionTextViewDidChange(self)
}
}

View File

@ -14,7 +14,7 @@ class ComposeStatusReplyView: UIView {
@IBOutlet weak var avatarImageView: UIImageView!
@IBOutlet weak var displayNameLabel: UILabel!
@IBOutlet weak var usernameLabel: UILabel!
@IBOutlet weak var contentLabel: StatusContentLabel!
@IBOutlet weak var statusContentTextView: StatusContentTextView!
static func create() -> ComposeStatusReplyView {
return UINib(nibName: "ComposeStatusReplyView", bundle: nil).instantiate(withOwner: nil, options: nil).first as! ComposeStatusReplyView
@ -34,7 +34,7 @@ class ComposeStatusReplyView: UIView {
func updateUI(for status: Status) {
displayNameLabel.text = status.account.realDisplayName
usernameLabel.text = "@\(status.account.acct)"
contentLabel.statusID = status.id
statusContentTextView.statusID = status.id
ImageCache.avatars.get(status.account.avatar) { (data) in
guard let data = data else { return }

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="15400" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="15702" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15404"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15704"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
@ -39,23 +39,24 @@
<color key="textColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" text="Content" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="OEF-Hj-v3f" customClass="StatusContentLabel" customModule="Tusker" customModuleProvider="target">
<rect key="frame" x="0.0" y="24.5" width="301" height="626.5"/>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" userInteractionEnabled="NO" contentMode="scaleToFill" scrollEnabled="NO" delaysContentTouches="NO" editable="NO" textAlignment="natural" selectable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="atN-ay-ceL" customClass="StatusContentTextView" customModule="Tusker" customModuleProvider="target">
<rect key="frame" x="0.0" y="25" width="301" height="626"/>
<string key="text">Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda.</string>
<color key="textColor" systemColor="labelColor" cocoaTouchSystemColor="darkTextColor"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
</textView>
</subviews>
<constraints>
<constraint firstAttribute="bottom" secondItem="atN-ay-ceL" secondAttribute="bottom" id="3ub-qq-laF"/>
<constraint firstItem="Sdv-dB-Plm" firstAttribute="leading" secondItem="2cE-sS-Uut" secondAttribute="leading" id="6v5-7p-9gm"/>
<constraint firstAttribute="bottom" secondItem="OEF-Hj-v3f" secondAttribute="bottom" id="IEQ-Ab-tsP"/>
<constraint firstItem="OEF-Hj-v3f" firstAttribute="top" secondItem="Sdv-dB-Plm" secondAttribute="bottom" constant="4" id="J5s-TU-odB"/>
<constraint firstItem="Sdv-dB-Plm" firstAttribute="top" secondItem="2cE-sS-Uut" secondAttribute="top" id="YmP-yU-sfe"/>
<constraint firstItem="OEF-Hj-v3f" firstAttribute="leading" secondItem="2cE-sS-Uut" secondAttribute="leading" id="bbW-07-e2x"/>
<constraint firstItem="0yZ-71-eTj" firstAttribute="top" secondItem="2cE-sS-Uut" secondAttribute="top" id="bdX-ge-bMT"/>
<constraint firstAttribute="trailing" secondItem="0yZ-71-eTj" secondAttribute="trailing" constant="8" id="hU7-aZ-ibI"/>
<constraint firstItem="atN-ay-ceL" firstAttribute="leading" secondItem="2cE-sS-Uut" secondAttribute="leading" id="k5c-jg-Dy8"/>
<constraint firstItem="0yZ-71-eTj" firstAttribute="leading" secondItem="Sdv-dB-Plm" secondAttribute="trailing" constant="8" id="m0X-YU-m3V"/>
<constraint firstAttribute="trailing" secondItem="OEF-Hj-v3f" secondAttribute="trailing" id="xqX-4X-lJl"/>
<constraint firstItem="atN-ay-ceL" firstAttribute="top" secondItem="0yZ-71-eTj" secondAttribute="bottom" constant="4" id="pXc-4g-PAe"/>
<constraint firstAttribute="trailing" secondItem="atN-ay-ceL" secondAttribute="trailing" id="qcg-bA-8ba"/>
</constraints>
</view>
</subviews>
@ -72,8 +73,8 @@
<viewLayoutGuide key="safeArea" id="vUN-kp-3ea"/>
<connections>
<outlet property="avatarImageView" destination="Ypn-Ed-MTq" id="eea-bc-klc"/>
<outlet property="contentLabel" destination="OEF-Hj-v3f" id="GBI-ib-5T0"/>
<outlet property="displayNameLabel" destination="Sdv-dB-Plm" id="RxW-Ra-Ups"/>
<outlet property="statusContentTextView" destination="atN-ay-ceL" id="i6A-Rd-rJp"/>
<outlet property="usernameLabel" destination="0yZ-71-eTj" id="VQm-Dq-3zP"/>
</connections>
<point key="canvasLocation" x="138.40000000000001" y="-72.863568215892059"/>

View File

@ -1,231 +0,0 @@
//
// ContentLabel.swift
// Tusker
//
// Created by Shadowfacts on 10/1/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import UIKit
import SafariServices
import Pachyderm
import SwiftSoup
class ContentLabel: LinkLabel {
private static let emojiRegex = try! NSRegularExpression(pattern: ":(\\w+):", options: [])
var navigationDelegate: TuskerNavigationDelegate?
// MARK: - Emojis
func setEmojis(_ emojis: [Emoji]) {
guard !emojis.isEmpty else { return }
let group = DispatchGroup()
let mutAttrString = NSMutableAttributedString(attributedString: self.attributedText!)
let string = mutAttrString.string
let matches = ContentLabel.emojiRegex.matches(in: string, options: [], range: NSRange(location: 0, length: mutAttrString.length))
for match in matches.reversed() {
let shortcode = (string as NSString).substring(with: match.range(at: 1))
guard let emoji = emojis.first(where: { $0.shortcode == shortcode }) else {
continue
}
group.enter()
ImageCache.emojis.get(emoji.url) { (data) in
guard let data = data, let image = UIImage(data: data) else {
group.leave()
return
}
DispatchQueue.main.async {
let attachment = self.createEmojiTextAttachment(image: image, index: match.range.location)
mutAttrString.replaceCharacters(in: match.range, with: NSAttributedString(attachment: attachment))
group.leave()
}
}
}
group.notify(queue: .main) {
self.attributedText = mutAttrString
self.setNeedsLayout()
self.setNeedsDisplay()
}
}
// Based on https://github.com/ReticentJohn/Amaroq/blob/7c5b7088eb9fd1611dcb0f47d43bf8df093e142c/DireFloof/InlineImageHelpers.m
func createEmojiTextAttachment(image: UIImage, index: Int) -> NSTextAttachment {
let font = self.font!
let adjustedCapHeight = font.capHeight - 1
var imageSizeMatchingFontSize = CGSize(width: image.size.width * (adjustedCapHeight / image.size.height), height: adjustedCapHeight)
let defaultScale: CGFloat = 1.4
imageSizeMatchingFontSize = CGSize(width: imageSizeMatchingFontSize.width * defaultScale, height: imageSizeMatchingFontSize.height * defaultScale)
let textColor = self.textColor!
UIGraphicsBeginImageContextWithOptions(imageSizeMatchingFontSize, false, 0.0)
textColor.set()
image.draw(in: CGRect(origin: .zero, size: imageSizeMatchingFontSize))
let attachmentImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
let attachment = NSTextAttachment()
attachment.image = attachmentImage
return attachment
}
// MARK: - HTML Parsing
func setTextFromHtml(_ html: String) {
let doc = try! SwiftSoup.parse(html)
let body = doc.body()!
let (attributedText, links) = attributedTextForHTMLNode(body)
let mutAttrString = NSMutableAttributedString(attributedString: attributedText)
// only trailing whitespace can be trimmed here
// when posting an attachment without any text, pleromafe includes U+200B ZERO WIDTH SPACE at the beginning
// this would get trimmed and cause range out of bounds crashes
mutAttrString.trimTrailingCharactersInSet(.whitespacesAndNewlines)
self.links = []
let linkAttributes: [NSAttributedString.Key: Any] = [
.foregroundColor: UIColor.systemBlue,
]
for (range, url) in links {
mutAttrString.addAttributes(linkAttributes, range: range)
self.links.append(Link(range: range, url: url))
}
self.attributedText = mutAttrString
}
private func attributedTextForHTMLNode(_ node: Node) -> (NSAttributedString, [NSRange: URL]) {
switch node {
case let node as TextNode:
return (NSAttributedString(string: node.text()), [:])
case let node as Element:
var links = [NSRange: URL]()
let attributed = NSMutableAttributedString()
for child in node.getChildNodes() {
let (text, childLinks) = attributedTextForHTMLNode(child)
for (range, url) in childLinks {
let newRange = NSRange(location: range.location + attributed.length, length: range.length)
links[newRange] = url
}
attributed.append(text)
}
switch node.tagName() {
case "br":
attributed.append(NSAttributedString(string: "\n"))
case "a":
if let link = try? node.attr("href"),
let url = URL(string: link) {
links[attributed.fullRange] = url
}
case "p":
attributed.append(NSAttributedString(string: "\n\n"))
case "em", "i":
let currentFont: UIFont = attributed.attribute(.font, at: 0, effectiveRange: nil) as? UIFont ?? self.font
attributed.addAttribute(.font, value: currentFont.addingTraits(.traitItalic)!, range: attributed.fullRange)
case "strong", "b":
let currentFont: UIFont = attributed.attribute(.font, at: 0, effectiveRange: nil) as? UIFont ?? self.font
attributed.addAttribute(.font, value: currentFont.addingTraits(.traitBold)!, range: attributed.fullRange)
case "del":
attributed.addAttribute(.strikethroughStyle, value: NSUnderlineStyle.single.rawValue, range: attributed.fullRange)
case "code":
attributed.addAttribute(.font, value: UIFont(name: "Menlo", size: font!.pointSize)!, range: attributed.fullRange)
case "pre":
attributed.addAttribute(.font, value: UIFont(name: "Menlo", size: font!.pointSize)!, range: attributed.fullRange)
attributed.append(NSAttributedString(string: "\n\n"))
case "ol", "ul":
attributed.trimLeadingCharactersInSet(.whitespacesAndNewlines)
attributed.append(NSAttributedString(string: "\n"))
break
case "li":
let parentEl = node.parent()!
let parentTag = parentEl.tagName()
let bullet: NSAttributedString
if parentTag == "ol" {
let index = (try? node.elementSiblingIndex()) ?? 0
// we use the monospace digit font so that the periods of all the list items line up
bullet = NSAttributedString(string: "\(index + 1).\t", attributes: [.font: UIFont.monospacedDigitSystemFont(ofSize: font!.pointSize, weight: .regular)])
} else if parentTag == "ul" {
bullet = NSAttributedString(string: "\u{2022}\t")
} else {
bullet = NSAttributedString(string: "")
}
// inserting bullets at the beginning of the string shifts all the links down, so we adjust the link ranges
for (range, url) in links {
let newRange = NSRange(location: range.location + bullet.length - 1, length: range.length)
links[newRange] = url
links.removeValue(forKey: range)
}
attributed.insert(bullet, at: 0)
attributed.append(NSAttributedString(string: "\n"))
default:
break
}
return (attributed, links)
default:
fatalError("Unexpected node type: \(type(of: node))")
}
}
func getViewController(forLink url: URL, inRange range: NSRange) -> UIViewController {
let text = (self.text! as NSString).substring(with: range)
if let navigationDelegate = navigationDelegate {
if let mention = getMention(for: url, text: text) {
return ProfileTableViewController(accountID: mention.id, mastodonController: navigationDelegate.apiController)
} else if let tag = getHashtag(for: url, text: text) {
return HashtagTimelineViewController(for: tag, mastodonController: navigationDelegate.apiController)
}
}
return SFSafariViewController(url: url)
}
func getViewController(forLinkAt point: CGPoint) -> UIViewController? {
guard let link = getLink(atPoint: point) else {
return nil
}
return getViewController(forLink: link.url, inRange: link.range)
}
// MARK: - Interaction
override func linkTapped(_ link: LinkLabel.Link) {
let text = (self.text! as NSString).substring(with: link.range)
if let mention = getMention(for: link.url, text: text) {
navigationDelegate?.selected(mention: mention)
} else if let tag = getHashtag(for: link.url, text: text) {
navigationDelegate?.selected(tag: tag)
} else {
navigationDelegate?.selected(url: link.url)
}
}
override func linkLongPressed(_ link: LinkLabel.Link) {
navigationDelegate?.showMoreOptions(forURL: link.url)
}
// MARK: - Navigation
func getMention(for url: URL, text: String) -> Mention? {
return nil
}
func getHashtag(for url: URL, text: String) -> Hashtag? {
if text.starts(with: "#") {
let tag = String(text.dropFirst())
return Hashtag(name: tag, url: url)
} else {
return nil
}
}
}

View File

@ -0,0 +1,300 @@
//
// ContentTextView.swift
// Tusker
//
// Created by Shadowfacts on 1/18/20.
// Copyright © 2020 Shadowfacts. All rights reserved.
//
import UIKit
import SwiftSoup
import Pachyderm
import SafariServices
private let emojiRegex = try! NSRegularExpression(pattern: ":(\\w+):", options: [])
class ContentTextView: LinkTextView {
// todo: should be weak
var navigationDelegate: TuskerNavigationDelegate?
var mastodonController: MastodonController? { navigationDelegate?.apiController }
var defaultFont: UIFont = .systemFont(ofSize: 17)
var defaultColor: UIColor = .label
override func awakeFromNib() {
super.awakeFromNib()
delegate = self
addInteraction(UIContextMenuInteraction(delegate: self))
textDragInteraction?.isEnabled = false
textContainerInset = .zero
textContainer.lineFragmentPadding = 0
// the text view's builtin link interaction code is tied to isSelectable, so we need to use our own tap recognizer
let recognizer = UITapGestureRecognizer(target: self, action: #selector(textTapped(_:)))
addGestureRecognizer(recognizer)
}
// MARK: - Emojis
func setEmojis(_ emojis: [Emoji]) {
guard !emojis.isEmpty else { return }
let emojiImages = CachedDictionary<UIImage>(name: "ContentTextView Emoji Images")
let group = DispatchGroup()
for emoji in emojis {
group.enter()
ImageCache.emojis.get(emoji.url) { (data) in
defer { group.leave() }
guard let data = data, let image = UIImage(data: data) else {
return
}
emojiImages[emoji.shortcode] = image
}
}
group.notify(queue: .main) {
let mutAttrString = NSMutableAttributedString(attributedString: self.attributedText!)
let string = mutAttrString.string
let matches = emojiRegex.matches(in: string, options: [], range: mutAttrString.fullRange)
// replaces the emojis started from the end of the string as to not alter the indexes of the other emojis
for match in matches.reversed() {
let shortcode = (string as NSString).substring(with: match.range(at: 1))
guard let emojiImage = emojiImages[shortcode] else {
continue
}
let attachment = self.createEmojiTextAttachment(image: emojiImage, index: match.range.location)
let attachmentStr = NSAttributedString(attachment: attachment)
mutAttrString.replaceCharacters(in: match.range, with: attachmentStr)
}
self.attributedText = mutAttrString
self.setNeedsLayout()
self.setNeedsDisplay()
}
}
// Based on https://github.com/ReticentJohn/Amaroq/blob/7c5b7088eb9fd1611dcb0f47d43bf8df093e142c/DireFloof/InlineImageHelpers.m
private func createEmojiTextAttachment(image: UIImage, index: Int) -> NSTextAttachment {
let font = self.font!
let adjustedCapHeight = font.capHeight - 1
var imageSizeMatchingFontSize = CGSize(width: image.size.width * (adjustedCapHeight / image.size.height), height: adjustedCapHeight)
let defaultScale: CGFloat = 1.4
imageSizeMatchingFontSize = CGSize(width: imageSizeMatchingFontSize.width * defaultScale, height: imageSizeMatchingFontSize.height * defaultScale)
let textColor = self.textColor ?? UIColor.label
UIGraphicsBeginImageContextWithOptions(imageSizeMatchingFontSize, false, 0.0)
textColor.set()
image.draw(in: CGRect(origin: .zero, size: imageSizeMatchingFontSize))
let attachmentImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
let attachment = NSTextAttachment()
attachment.image = attachmentImage
return attachment
}
// MARK: - HTML Parsing
func setTextFromHtml(_ html: String) {
let doc = try! SwiftSoup.parse(html)
let body = doc.body()!
let attributedText = attributedTextForHTMLNode(body)
let mutAttrString = NSMutableAttributedString(attributedString: attributedText)
mutAttrString.trimTrailingCharactersInSet(.whitespacesAndNewlines)
self.attributedText = mutAttrString
}
func attributedTextForHTMLNode(_ node: Node, usePreformattedText: Bool = false) -> NSAttributedString {
switch node {
case let node as TextNode:
let text: String
if usePreformattedText {
text = node.getWholeText()
} else {
text = node.text()
}
return NSAttributedString(string: text, attributes: [.font: defaultFont, .foregroundColor: defaultColor])
case let node as Element:
let attributed = NSMutableAttributedString(string: "", attributes: [.font: defaultFont, .foregroundColor: defaultColor])
for child in node.getChildNodes() {
attributed.append(attributedTextForHTMLNode(child, usePreformattedText: usePreformattedText || node.tagName() == "pre"))
}
switch node.tagName() {
case "br":
attributed.append(NSAttributedString(string: "\n"))
case "a":
if let link = try? node.attr("href"),
let url = URL(string: link) {
attributed.addAttribute(.link, value: url, range: attributed.fullRange)
}
case "p":
attributed.append(NSAttributedString(string: "\n\n"))
case "em", "i":
let currentFont: UIFont = attributed.attribute(.font, at: 0, effectiveRange: nil) as? UIFont ?? self.font!
attributed.addAttribute(.font, value: currentFont.addingTraits(.traitItalic)!, range: attributed.fullRange)
case "strong", "b":
let currentFont: UIFont = attributed.attribute(.font, at: 0, effectiveRange: nil) as? UIFont ?? self.font!
attributed.addAttribute(.font, value: currentFont.addingTraits(.traitBold)!, range: attributed.fullRange)
case "del":
attributed.addAttribute(.strikethroughStyle, value: NSUnderlineStyle.single.rawValue, range: attributed.fullRange)
case "code":
attributed.addAttribute(.font, value: UIFont.monospacedSystemFont(ofSize: self.font!.pointSize, weight: .regular), range: attributed.fullRange)
case "pre":
attributed.addAttribute(.font, value: UIFont.monospacedSystemFont(ofSize: self.font!.pointSize, weight: .regular), range: attributed.fullRange)
attributed.append(NSAttributedString(string: "\n\n"))
case "ol", "ul":
attributed.trimLeadingCharactersInSet(.whitespacesAndNewlines)
attributed.append(NSAttributedString(string: "\n\n"))
case "li":
let parentEl = node.parent()!
let parentTag = parentEl.tagName()
let bullet: NSAttributedString
if parentTag == "ol" {
let index = (try? node.elementSiblingIndex()) ?? 0
// we use the monospace digit font so that the periods of all the list items line up
bullet = NSAttributedString(string: "\(index + 1).\t", attributes: [.font: UIFont.monospacedDigitSystemFont(ofSize: self.font!.pointSize, weight: .regular)])
} else if parentTag == "ul" {
bullet = NSAttributedString(string: "\u{2022}\t")
} else {
bullet = NSAttributedString()
}
attributed.insert(bullet, at: 0)
attributed.append(NSAttributedString(string: "\n"))
default:
break
}
return attributed
default:
fatalError("Unexpected node type \(type(of: node))")
}
}
// MARK: - Interaction
// only accept touches that are over a link
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if getLinkAtPoint(point) != nil || isSelectable {
return super.hitTest(point, with: event)
} else {
return nil
}
}
// only handles link taps via the gesture recognizer which is used when selection is disabled
@objc func textTapped(_ recognizer: UITapGestureRecognizer) {
let location = recognizer.location(in: self)
if let (link, range) = getLinkAtPoint(location) {
let text = (self.text as NSString).substring(with: range)
handleLinkTapped(url: link, text: text)
}
}
func getLinkAtPoint(_ point: CGPoint) -> (URL, NSRange)? {
let locationInTextContainer = CGPoint(x: point.x - textContainerInset.left, y: point.y - textContainerInset.top)
var partialFraction: CGFloat = 0
let characterIndex = layoutManager.characterIndex(for: locationInTextContainer, in: textContainer, fractionOfDistanceBetweenInsertionPoints: &partialFraction)
if characterIndex < textStorage.length {
var range = NSRange()
if let link = textStorage.attribute(.link, at: characterIndex, longestEffectiveRange: &range, in: textStorage.fullRange) as? URL {
return (link, range)
}
}
return nil
}
func handleLinkTapped(url: URL, text: String) {
if let mention = getMention(for: url, text: text) {
navigationDelegate?.selected(mention: mention)
} else if let tag = getHashtag(for: url, text: text) {
navigationDelegate?.selected(tag: tag)
} else {
navigationDelegate?.selected(url: url)
}
}
// MARK: - Navigation
func getViewController(forLink url: URL, inRange range: NSRange) -> UIViewController {
let text = (self.text as NSString).substring(with: range)
if let mention = getMention(for: url, text: text) {
return ProfileTableViewController(accountID: mention.id, mastodonController: mastodonController!)
} else if let tag = getHashtag(for: url, text: text) {
return HashtagTimelineViewController(for: tag, mastodonController: mastodonController!)
} else {
return SFSafariViewController(url: url)
}
}
open func getMention(for url: URL, text: String) -> Mention? {
return nil
}
open func getHashtag(for url: URL, text: String) -> Hashtag? {
if text.starts(with: "#") {
let tag = String(text.dropFirst())
return Hashtag(name: tag, url: url)
} else {
return nil
}
}
}
extension ContentTextView: UITextViewDelegate {
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
// disable the text view's link interactions, we handle tapping links ourself with a gesture recognizer
return false
}
}
extension ContentTextView: MenuPreviewProvider {
func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? {
fatalError("unimplemented")
}
}
extension ContentTextView: UIContextMenuInteractionDelegate {
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
if let (link, range) = getLinkAtPoint(location) {
let preview: UIContextMenuContentPreviewProvider = {
self.getViewController(forLink: link, inRange: range)
}
let actions: UIContextMenuActionProvider = { (_) in
let text = (self.text as NSString).substring(with: range)
let actions: [UIAction]
if let mention = self.getMention(for: link, text: text) {
actions = self.actionsForProfile(accountID: mention.id, sourceView: self)
} else if let tag = self.getHashtag(for: link, text: text) {
actions = self.actionsForHashtag(tag, sourceView: self)
} else {
actions = self.actionsForURL(link, sourceView: self)
}
return UIMenu(title: "", image: nil, identifier: nil, options: [], children: actions)
}
return UIContextMenuConfiguration(identifier: nil, previewProvider: preview, actionProvider: actions)
} else {
return nil
}
}
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) {
if let viewController = animator.previewViewController {
animator.preferredCommitStyle = .pop
animator.addCompletion {
self.navigationDelegate?.show(viewController)
}
}
}
}

View File

@ -14,7 +14,7 @@ class InstanceTableViewCell: UITableViewCell {
@IBOutlet weak var thumbnailImageView: UIImageView!
@IBOutlet weak var domainLabel: UILabel!
@IBOutlet weak var adultLabel: UILabel!
@IBOutlet weak var descriptionLabel: ContentLabel!
@IBOutlet weak var descriptionTextView: ContentTextView!
var instance: Instance?
var selectorInstance: InstanceSelector.Instance?
@ -37,7 +37,7 @@ class InstanceTableViewCell: UITableViewCell {
domainLabel.text = instance.domain
adultLabel.isHidden = instance.category != .adult
descriptionLabel.setTextFromHtml(instance.description)
descriptionTextView.setTextFromHtml(instance.description)
updateThumbnail(url: instance.proxiedThumbnailURL)
}
@ -47,7 +47,7 @@ class InstanceTableViewCell: UITableViewCell {
domainLabel.text = URLComponents(string: instance.uri)?.host ?? instance.uri
adultLabel.isHidden = true
descriptionLabel.setTextFromHtml(instance.description)
descriptionTextView.setTextFromHtml(instance.description)
if let thumbnail = instance.thumbnail {
updateThumbnail(url: thumbnail)

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="15701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="15702" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15703"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15704"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
@ -27,7 +27,7 @@
</constraints>
</imageView>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="QG1-xB-nmt">
<rect key="frame" x="88" y="0.0" width="200" height="47"/>
<rect key="frame" x="88" y="0.0" width="200" height="63"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" translatesAutoresizingMaskIntoConstraints="NO" id="XtJ-BL-iHb">
<rect key="frame" x="0.0" y="0.0" width="200" height="26.5"/>
@ -51,15 +51,12 @@
</label>
</subviews>
</stackView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" text="Instance Description" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="aD6-LG-BWG" customClass="ContentLabel" customModule="Tusker" customModuleProvider="target">
<rect key="frame" x="0.0" y="26.5" width="200" height="20.5"/>
<accessibility key="accessibilityConfiguration">
<accessibilityTraits key="traits" staticText="YES" notEnabled="YES"/>
</accessibility>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" userInteractionEnabled="NO" contentMode="scaleToFill" scrollEnabled="NO" editable="NO" text="Instance Description" textAlignment="natural" selectable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Z5t-Zl-040" customClass="ContentTextView" customModule="Tusker" customModuleProvider="target">
<rect key="frame" x="0.0" y="26.5" width="200" height="36.5"/>
<color key="textColor" systemColor="labelColor" cocoaTouchSystemColor="darkTextColor"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
</textView>
</subviews>
</stackView>
</subviews>
@ -75,7 +72,7 @@
<viewLayoutGuide key="safeArea" id="njF-e1-oar"/>
<connections>
<outlet property="adultLabel" destination="ekk-aL-7Pq" id="vzP-Gm-QF7"/>
<outlet property="descriptionLabel" destination="aD6-LG-BWG" id="KNk-Gq-cDU"/>
<outlet property="descriptionTextView" destination="Z5t-Zl-040" id="Iz3-bX-Zh2"/>
<outlet property="domainLabel" destination="SjP-Nk-sSH" id="QPQ-n6-yoK"/>
<outlet property="thumbnailImageView" destination="e2C-wt-pkK" id="KeP-Xf-0Tn"/>
</connections>

View File

@ -1,185 +0,0 @@
//
// LinkLabel.swift
// Tusker
//
// Created by Shadowfacts on 2/3/19.
// Copyright © 2019 Shadowfacts. All rights reserved.
//
import UIKit
class LinkLabel: UILabel {
typealias Link = (range: NSRange, url: URL)
let layoutManager = NSLayoutManager()
let textContainer = NSTextContainer(size: .zero)
var textStorage: NSTextStorage!
var links = [Link]()
var selectedLinkAttributes: [NSAttributedString.Key: Any] = [
// .backgroundColor: UIColor(hue: 0, saturation: 0, brightness: 0.9, alpha: 1)
.backgroundColor: UIColor.secondarySystemBackground
]
var selectedLinkRange: NSRange? {
didSet {
if let oldValue = oldValue {
removeSelectedLinkAttributes(oldValue)
}
if let newValue = selectedLinkRange {
addSelectedLinkAttributes(newValue)
}
}
}
override var attributedText: NSAttributedString? {
didSet {
guard let attributedText = attributedText else { return }
textStorage?.removeLayoutManager(layoutManager)
textStorage = NSTextStorage(attributedString: attributedText)
textStorage.addLayoutManager(layoutManager)
}
}
override var text: String? {
willSet {
fatalError("LinkLabel does not support non-attributed text")
}
}
override func awakeFromNib() {
super.awakeFromNib()
isUserInteractionEnabled = true
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(labelTapped(_:)))
tapRecognizer.delegate = self
addGestureRecognizer(tapRecognizer)
let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(labelLongPressed(_:)))
longPressRecognizer.delegate = self
addGestureRecognizer(longPressRecognizer)
layoutManager.addTextContainer(textContainer)
textContainer.lineFragmentPadding = 0.0
textContainer.lineBreakMode = lineBreakMode
textContainer.maximumNumberOfLines = numberOfLines
}
override func layoutSubviews() {
super.layoutSubviews()
textContainer.size = bounds.size
}
func getLink(atPoint point: CGPoint) -> Link? {
let labelSize = bounds.size
let textBoundingBox = layoutManager.usedRect(for: textContainer)
let textContainerOffset = CGPoint(x: (labelSize.width - textBoundingBox.size.width) * 0.5 - textBoundingBox.origin.x,
y: (labelSize.height - textBoundingBox.size.height) * 0.5 - textBoundingBox.origin.y)
let locationOfTouchInTextContainer = CGPoint(x: point.x - textContainerOffset.x,
y: point.y - textContainerOffset.y)
// let indexOfCharacter = layoutManager.characterIndex(for: locationOfTouchInTextContainer, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
let indexOfCharacter = layoutManager.glyphIndex(for: locationOfTouchInTextContainer, in: textContainer)
if let link = links.first(where: { $0.range.contains(indexOfCharacter) }) {
return link
} else {
return nil
}
}
func addSelectedLinkAttributes(_ range: NSRange) {
let mutAttrString = NSMutableAttributedString(attributedString: attributedText!)
mutAttrString.addAttributes(selectedLinkAttributes, range: range)
self.attributedText = mutAttrString
setNeedsDisplay()
}
func removeSelectedLinkAttributes(_ range: NSRange) {
let mutAttrString = NSMutableAttributedString(attributedString: attributedText!)
selectedLinkAttributes.keys.forEach { mutAttrString.removeAttribute($0, range: range) }
self.attributedText = mutAttrString
setNeedsDisplay()
}
// MARK: - Interaction
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first, onTouch(touch) {
return
}
super.touchesBegan(touches, with: event)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first, onTouch(touch) {
return
}
super.touchesMoved(touches, with: event)
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first, onTouch(touch) {
return
}
super.touchesEnded(touches, with: event)
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first, onTouch(touch) {
return
}
super.touchesCancelled(touches, with: event)
}
func onTouch(_ touch: UITouch) -> Bool {
let location = touch.location(in: self)
let link = getLink(atPoint: location)
switch touch.phase {
case .began, .moved:
selectedLinkRange = link?.range
case .cancelled, .ended:
selectedLinkRange = nil
default:
break
}
return link != nil
}
@objc func labelTapped(_ recognizer: UITapGestureRecognizer) {
let location = recognizer.location(in: self)
guard let link = getLink(atPoint: location) else {
return
}
linkTapped(link)
}
@objc func labelLongPressed(_ recognizer: UILongPressGestureRecognizer) {
let location = recognizer.location(in: self)
guard let link = getLink(atPoint: location) else {
return
}
linkLongPressed(link)
}
func linkTapped(_ link: Link) {
}
func linkLongPressed(_ link: Link) {
}
}
extension LinkLabel: UIGestureRecognizerDelegate {
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
let location = touch.location(in: self)
let link = getLink(atPoint: location)
return link != nil
}
}

View File

@ -0,0 +1,22 @@
//
// LinkTextView.swift
// Tusker
//
// Created by Shadowfacts on 1/18/20.
// Copyright © 2020 Shadowfacts. All rights reserved.
//
import UIKit
class LinkTextView: UITextView {
override func awakeFromNib() {
super.awakeFromNib()
delaysContentTouches = false
isScrollEnabled = false
isEditable = false
isUserInteractionEnabled = true
}
}

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="15505" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="15702" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15509"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15704"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>

View File

@ -10,7 +10,7 @@ import UIKit
import Pachyderm
protocol ProfileHeaderTableViewCellDelegate: TuskerNavigationDelegate {
func showMoreOptions()
func showMoreOptions(cell: ProfileHeaderTableViewCell)
}
class ProfileHeaderTableViewCell: UITableViewCell {
@ -24,7 +24,7 @@ class ProfileHeaderTableViewCell: UITableViewCell {
@IBOutlet weak var displayNameLabel: UILabel!
@IBOutlet weak var usernameLabel: UILabel!
@IBOutlet weak var followsYouLabel: UILabel!
@IBOutlet weak var noteLabel: StatusContentLabel!
@IBOutlet weak var noteTextView: StatusContentTextView!
@IBOutlet weak var fieldsStackView: UIStackView!
@IBOutlet weak var fieldNamesStackView: UIStackView!
@IBOutlet weak var fieldValuesStack: UIStackView!
@ -79,9 +79,9 @@ class ProfileHeaderTableViewCell: UITableViewCell {
}
}
noteLabel.navigationDelegate = delegate
noteLabel.setTextFromHtml(account.note)
noteLabel.setEmojis(account.emojis)
noteTextView.navigationDelegate = delegate
noteTextView.setTextFromHtml(account.note)
noteTextView.setEmojis(account.emojis)
if accountID != mastodonController.account.id {
// don't show relationship label for the user's own account
@ -104,16 +104,18 @@ class ProfileHeaderTableViewCell: UITableViewCell {
nameLabel.text = field.name
nameLabel.font = .boldSystemFont(ofSize: 17)
nameLabel.textAlignment = .right
nameLabel.numberOfLines = 0
fieldNamesStackView.addArrangedSubview(nameLabel)
let valueLabel = ContentLabel()
valueLabel.setTextFromHtml(field.value)
valueLabel.setEmojis(account.emojis)
valueLabel.font = .systemFont(ofSize: 17)
valueLabel.textAlignment = .left
valueLabel.awakeFromNib() // TODO: this shouldn't be necessary
valueLabel.navigationDelegate = delegate
fieldValuesStack.addArrangedSubview(valueLabel)
let valueTextView = ContentTextView()
valueTextView.isSelectable = false
valueTextView.font = .systemFont(ofSize: 17)
valueTextView.setTextFromHtml(field.value)
valueTextView.setEmojis(account.emojis)
valueTextView.textAlignment = .left
valueTextView.awakeFromNib()
valueTextView.navigationDelegate = delegate
fieldValuesStack.addArrangedSubview(valueTextView)
}
} else {
fieldsStackView.isHidden = true
@ -138,7 +140,7 @@ class ProfileHeaderTableViewCell: UITableViewCell {
}
@objc func morePressed() {
delegate?.showMoreOptions()
delegate?.showMoreOptions(cell: self)
}
@objc func avatarPressed() {
@ -151,27 +153,27 @@ class ProfileHeaderTableViewCell: UITableViewCell {
}
extension ProfileHeaderTableViewCell: MenuPreviewProvider {
var navigationDelegate: TuskerNavigationDelegate? { return delegate }
func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? {
let noteLabelPoint = noteLabel.convert(location, from: self)
if noteLabel.bounds.contains(noteLabelPoint),
let link = noteLabel.getLink(atPoint: noteLabelPoint) {
return (
content: { self.noteLabel.getViewController(forLink: link.url, inRange: link.range) },
actions: {
let text = (self.noteLabel.text! as NSString).substring(with: link.range)
if let mention = self.noteLabel.getMention(for: link.url, text: text) {
return self.actionsForProfile(accountID: mention.id)
} else if let hashtag = self.noteLabel.getHashtag(for: link.url, text: text) {
return self.actionsForHashtag(hashtag)
} else {
return self.actionsForURL(link.url)
}
}
)
} else {
return nil
}
}
}
//extension ProfileHeaderTableViewCell: MenuPreviewProvider {
// var navigationDelegate: TuskerNavigationDelegate? { return delegate }
// func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? {
// let noteLabelPoint = noteLabel.convert(location, from: self)
// if noteLabel.bounds.contains(noteLabelPoint),
// let link = noteLabel.getLink(atPoint: noteLabelPoint) {
// return (
// content: { self.noteLabel.getViewController(forLink: link.url, inRange: link.range) },
// actions: {
// let text = (self.noteLabel.text! as NSString).substring(with: link.range)
// if let mention = self.noteLabel.getMention(for: link.url, text: text) {
// return self.actionsForProfile(accountID: mention.id, sourceView: self)
// } else if let hashtag = self.noteLabel.getHashtag(for: link.url, text: text) {
// return self.actionsForHashtag(hashtag, sourceView: self)
// } else {
// return self.actionsForURL(link.url, sourceView: self)
// }
// }
// )
// } else {
// return nil
// }
// }
//}

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="15505" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="15702" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15509"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15704"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
@ -66,12 +66,13 @@
<color key="textColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" text="Note" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="I0n-aP-dJP" customClass="StatusContentLabel" customModule="Tusker" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="37" height="12"/>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" delaysContentTouches="NO" editable="NO" textAlignment="natural" selectable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="bnc-3t-t7t" customClass="StatusContentTextView" customModule="Tusker" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="337" height="12"/>
<string key="text">Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda.</string>
<color key="textColor" systemColor="labelColor" cocoaTouchSystemColor="darkTextColor"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
</textView>
</subviews>
</stackView>
<stackView opaque="NO" contentMode="scaleToFill" distribution="fillProportionally" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="sHU-GU-klv">
@ -165,7 +166,7 @@
<outlet property="followsYouLabel" destination="a32-1a-xXZ" id="phY-0L-NnN"/>
<outlet property="headerImageView" destination="Fw7-OL-iy5" id="6sv-E5-D73"/>
<outlet property="moreButtonVisualEffectView" destination="mQY-XN-PfZ" id="t7l-wg-nj0"/>
<outlet property="noteLabel" destination="I0n-aP-dJP" id="7yW-mE-jxY"/>
<outlet property="noteTextView" destination="bnc-3t-t7t" id="dV2-7U-gSd"/>
<outlet property="usernameLabel" destination="MIj-OR-NOR" id="e1I-N7-rKx"/>
</connections>
<point key="canvasLocation" x="40.799999999999997" y="110.64467766116942"/>

View File

@ -18,7 +18,7 @@ class BaseStatusTableViewCell: UITableViewCell {
var delegate: StatusTableViewCellDelegate? {
didSet {
contentLabel.navigationDelegate = delegate
contentTextView.navigationDelegate = delegate
}
}
var overrideMastodonController: MastodonController?
@ -29,7 +29,7 @@ class BaseStatusTableViewCell: UITableViewCell {
@IBOutlet weak var usernameLabel: UILabel!
@IBOutlet weak var contentWarningLabel: UILabel!
@IBOutlet weak var collapseButton: UIButton!
@IBOutlet weak var contentLabel: StatusContentLabel!
@IBOutlet weak var contentTextView: StatusContentTextView!
@IBOutlet weak var attachmentsView: AttachmentsContainerView!
@IBOutlet weak var replyButton: UIButton!
@IBOutlet weak var favoriteButton: UIButton!
@ -91,7 +91,7 @@ class BaseStatusTableViewCell: UITableViewCell {
collapseButton.layer.masksToBounds = true
collapseButton.layer.cornerRadius = 5
accessibilityElements = [displayNameLabel!, contentWarningLabel!, collapseButton!, contentLabel!, attachmentsView!]
accessibilityElements = [displayNameLabel!, contentWarningLabel!, collapseButton!, contentTextView!, attachmentsView!]
attachmentsView.isAccessibilityElement = true
NotificationCenter.default.addObserver(self, selector: #selector(updateUIForPreferences), name: .preferencesChanged, object: nil)
@ -134,7 +134,7 @@ class BaseStatusTableViewCell: UITableViewCell {
updateStatusState(status: status)
contentLabel.statusID = statusID
contentTextView.statusID = statusID
contentWarningLabel.text = status.spoilerText
contentWarningLabel.isHidden = status.spoilerText.isEmpty
@ -143,7 +143,7 @@ class BaseStatusTableViewCell: UITableViewCell {
collapsible = !status.spoilerText.isEmpty
var shouldCollapse = collapsible
if !shouldCollapse,
let text = contentLabel.text,
let text = contentTextView.text,
text.count > 500 {
collapsible = true
shouldCollapse = true
@ -215,7 +215,7 @@ class BaseStatusTableViewCell: UITableViewCell {
func setCollapsed(_ collapsed: Bool, animated: Bool) {
self.collapsed = collapsed
contentLabel.isHidden = collapsed
contentTextView.isHidden = collapsed
attachmentsView.isHidden = attachmentsView.attachments.count == 0 || collapsed
let buttonImage = UIImage(systemName: collapsed ? "chevron.down" : "chevron.up")!
@ -301,7 +301,7 @@ class BaseStatusTableViewCell: UITableViewCell {
}
@IBAction func morePressed() {
delegate?.showMoreOptions(forStatus: statusID)
delegate?.showMoreOptions(forStatus: statusID, sourceView: moreButton)
}
@objc func accountPressed() {
@ -327,11 +327,10 @@ extension BaseStatusTableViewCell: MenuPreviewProvider {
func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? {
guard let mastodonController = mastodonController else { return nil }
if avatarImageView.frame.contains(location) {
return (content: {
ProfileTableViewController(accountID: self.accountID, mastodonController: mastodonController)
}, actions: {
self.actionsForProfile(accountID: self.accountID)
})
return (
content: { ProfileTableViewController(accountID: self.accountID, mastodonController: mastodonController) },
actions: { self.actionsForProfile(accountID: self.accountID, sourceView: self.avatarImageView) }
)
} else if attachmentsView.frame.contains(location) {
let attachmentsViewLocation = attachmentsView.convert(location, from: self)
if let attachmentView = attachmentsView.attachmentViews.allObjects.first(where: { $0.frame.contains(attachmentsViewLocation) }),
@ -339,22 +338,22 @@ extension BaseStatusTableViewCell: MenuPreviewProvider {
let description = attachmentView.attachment.description
return (content: { self.delegate?.largeImage(image, description: description, sourceView: attachmentView) }, actions: { [] })
}
} else if contentLabel.frame.contains(location),
}/* else if contentLabel.frame.contains(location),
let link = contentLabel.getLink(atPoint: contentLabel.convert(location, from: self)) {
return (
content: { self.contentLabel.getViewController(forLink: link.url, inRange: link.range) },
actions: {
let text = (self.contentLabel.text! as NSString).substring(with: link.range)
if let mention = self.contentLabel.getMention(for: link.url, text: text) {
return self.actionsForProfile(accountID: mention.id)
return self.actionsForProfile(accountID: mention.id, sourceView: self)
} else if let hashtag = self.contentLabel.getHashtag(for: link.url, text: text) {
return self.actionsForHashtag(hashtag)
return self.actionsForHashtag(hashtag, sourceView: self)
} else {
return self.actionsForURL(link.url)
return self.actionsForURL(link.url, sourceView: self)
}
}
)
}
}*/
return self.getStatusCellPreviewProviders(for: location, sourceViewController: sourceViewController)
}
}

View File

@ -33,7 +33,9 @@ class ConversationMainStatusTableViewCell: BaseStatusTableViewCell {
profileAccessibilityElement = UIAccessibilityElement(accessibilityContainer: self)
profileAccessibilityElement.accessibilityFrameInContainerSpace = profileDetailContainerView.convert(profileDetailContainerView.frame, to: self)
accessibilityElements = [profileAccessibilityElement!, contentWarningLabel!, collapseButton!, contentLabel!, totalFavoritesButton!, totalReblogsButton!, timestampAndClientLabel!, replyButton!, favoriteButton!, reblogButton!, moreButton!]
accessibilityElements = [profileAccessibilityElement!, contentWarningLabel!, collapseButton!, contentTextView!, totalFavoritesButton!, totalReblogsButton!, timestampAndClientLabel!, replyButton!, favoriteButton!, reblogButton!, moreButton!]
contentTextView.defaultFont = .systemFont(ofSize: 20)
}
override func updateUI(statusID: String, state: StatusState) {

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="15505" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="15702" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15509"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15704"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
@ -75,12 +75,13 @@
<action selector="collapseButtonPressed" destination="iN0-l3-epB" eventType="touchUpInside" id="2Jy-L1-lN6"/>
</connections>
</button>
<label opaque="NO" userInteractionEnabled="NO" contentMode="TopLeft" horizontalHuggingPriority="251" verticalHuggingPriority="249" text="Content" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="TgY-hs-Klo" customClass="StatusContentLabel" customModule="Tusker" customModuleProvider="target">
<rect key="frame" x="0.0" y="124.5" width="70" height="55.5"/>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" verticalCompressionResistancePriority="749" scrollEnabled="NO" delaysContentTouches="NO" editable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="z0g-HN-gS0" customClass="StatusContentTextView" customModule="Tusker" customModuleProvider="target">
<rect key="frame" x="0.0" y="124.5" width="343" height="55.5"/>
<string key="text">Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda.</string>
<color key="textColor" systemColor="labelColor" cocoaTouchSystemColor="darkTextColor"/>
<fontDescription key="fontDescription" type="system" pointSize="20"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
</textView>
<view hidden="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="IF9-9U-Gk0" customClass="AttachmentsContainerView" customModule="Tusker" customModuleProvider="target">
<rect key="frame" x="0.0" y="184" width="343" height="0.0"/>
<color key="backgroundColor" systemColor="secondarySystemBackgroundColor" red="0.94901960780000005" green="0.94901960780000005" blue="0.96862745100000003" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
@ -183,6 +184,7 @@
</subviews>
<constraints>
<constraint firstItem="Cnd-Fj-B7l" firstAttribute="width" secondItem="GuG-Qd-B8I" secondAttribute="width" id="2hS-RG-81T"/>
<constraint firstItem="z0g-HN-gS0" firstAttribute="width" secondItem="GuG-Qd-B8I" secondAttribute="width" id="4TF-2Z-mdf"/>
<constraint firstItem="IF9-9U-Gk0" firstAttribute="width" secondItem="GuG-Qd-B8I" secondAttribute="width" id="8A8-wi-7sg"/>
<constraint firstItem="8r8-O8-Agh" firstAttribute="width" secondItem="GuG-Qd-B8I" secondAttribute="width" id="bZv-bR-jJ3"/>
<constraint firstItem="ejU-sO-Og5" firstAttribute="width" secondItem="GuG-Qd-B8I" secondAttribute="width" id="biK-oQ-SLy"/>
@ -204,7 +206,7 @@
<outlet property="attachmentsView" destination="IF9-9U-Gk0" id="Oxw-sJ-MJE"/>
<outlet property="avatarImageView" destination="mB9-HO-1vf" id="0R0-rt-Osh"/>
<outlet property="collapseButton" destination="8r8-O8-Agh" id="0es-Hi-bpt"/>
<outlet property="contentLabel" destination="TgY-hs-Klo" id="SEi-B2-VQf"/>
<outlet property="contentTextView" destination="z0g-HN-gS0" id="atk-1f-83e"/>
<outlet property="contentWarningLabel" destination="cwQ-mR-L1b" id="5sm-PC-FIN"/>
<outlet property="displayNameLabel" destination="lZY-2e-17d" id="7og-23-eHy"/>
<outlet property="favoriteAndReblogCountStackView" destination="HZv-qj-gi6" id="jC9-cA-dXg"/>

View File

@ -134,7 +134,7 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
guard let mastodonController = mastodonController else { return nil }
return (
content: { ConversationTableViewController(for: self.statusID, state: self.statusState.copy(), mastodonController: mastodonController) },
actions: { self.actionsForStatus(statusID: self.statusID) }
actions: { self.actionsForStatus(statusID: self.statusID, sourceView: self) }
)
}
@ -219,7 +219,7 @@ extension TimelineStatusTableViewCell: TableViewSwipeActionProvider {
reply.backgroundColor = tintColor
let more = UIContextualAction(style: .normal, title: "More") { (action, view, completion) in
completion(true)
self.delegate?.showMoreOptions(forStatus: self.statusID)
self.delegate?.showMoreOptions(forStatus: self.statusID, sourceView: self)
}
more.image = UIImage(systemName: "ellipsis")
more.backgroundColor = .gray

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="15505" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="15702" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15509"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15704"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
@ -23,7 +23,7 @@
<nil key="highlightedColor"/>
</label>
<view contentMode="scaleToFill" verticalHuggingPriority="249" translatesAutoresizingMaskIntoConstraints="NO" id="ve3-Y1-NQH">
<rect key="frame" x="0.0" y="28.5" width="343" height="103.5"/>
<rect key="frame" x="0.0" y="28.5" width="343" height="165.5"/>
<subviews>
<imageView contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="QMP-j2-HLn">
<rect key="frame" x="0.0" y="0.0" width="50" height="50"/>
@ -37,7 +37,7 @@
</constraints>
</imageView>
<stackView opaque="NO" contentMode="scaleToFill" verticalCompressionResistancePriority="751" axis="vertical" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="gIY-Wp-RSk">
<rect key="frame" x="58" y="0.0" width="277" height="103.5"/>
<rect key="frame" x="58" y="0.0" width="277" height="165.5"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="3Sm-P0-ySf">
<rect key="frame" x="0.0" y="0.0" width="277" height="20.5"/>
@ -105,12 +105,13 @@
<action selector="collapseButtonPressed" destination="iN0-l3-epB" eventType="touchUpInside" id="JaH-xX-UOD"/>
</connections>
</button>
<label opaque="NO" userInteractionEnabled="NO" contentMode="TopLeft" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Content" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="HrJ-t9-KcD" customClass="StatusContentLabel" customModule="Tusker" customModuleProvider="target">
<rect key="frame" x="0.0" y="83" width="277" height="20.5"/>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" delaysContentTouches="NO" editable="NO" selectable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="waJ-f5-LKv" customClass="StatusContentTextView" customModule="Tusker" customModuleProvider="target">
<rect key="frame" x="0.0" y="83" width="277" height="82.5"/>
<string key="text">Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda.</string>
<color key="textColor" systemColor="labelColor" cocoaTouchSystemColor="darkTextColor"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
</textView>
</subviews>
</stackView>
</subviews>
@ -125,17 +126,17 @@
</constraints>
</view>
<view hidden="YES" contentMode="scaleToFill" verticalCompressionResistancePriority="1" translatesAutoresizingMaskIntoConstraints="NO" id="nbq-yr-2mA" customClass="AttachmentsContainerView" customModule="Tusker" customModuleProvider="target">
<rect key="frame" x="0.0" y="136" width="343" height="0.0"/>
<rect key="frame" x="0.0" y="198" width="343" height="0.0"/>
<color key="backgroundColor" systemColor="secondarySystemBackgroundColor" red="0.94901960780000005" green="0.94901960780000005" blue="0.96862745100000003" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="height" priority="999" constant="200" id="J42-49-2MU"/>
</constraints>
</view>
<stackView opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" distribution="equalSpacing" alignment="bottom" translatesAutoresizingMaskIntoConstraints="NO" id="Zlb-yt-NTw">
<rect key="frame" x="0.0" y="140" width="343" height="84"/>
<rect key="frame" x="0.0" y="202" width="343" height="22"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" contentHorizontalAlignment="leading" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="rKF-yF-KIa">
<rect key="frame" x="0.0" y="62" width="21" height="22"/>
<rect key="frame" x="0.0" y="0.0" width="21" height="22"/>
<accessibility key="accessibilityConfiguration" label="Reply"/>
<state key="normal" image="arrowshape.turn.up.left.fill" catalog="system"/>
<connections>
@ -143,7 +144,7 @@
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="x0t-TR-jJ4">
<rect key="frame" x="107" y="62" width="22" height="22"/>
<rect key="frame" x="107" y="0.0" width="22" height="22"/>
<accessibility key="accessibilityConfiguration" label="Favorite"/>
<state key="normal" image="star.fill" catalog="system"/>
<connections>
@ -151,7 +152,7 @@
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="6tW-z8-Qh9">
<rect key="frame" x="215.5" y="62" width="22.5" height="22"/>
<rect key="frame" x="215.5" y="0.0" width="22.5" height="22"/>
<accessibility key="accessibilityConfiguration" label="Reblog"/>
<state key="normal" image="repeat" catalog="system"/>
<connections>
@ -159,7 +160,7 @@
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="982-J4-NGl">
<rect key="frame" x="324" y="62" width="19" height="22"/>
<rect key="frame" x="324" y="0.0" width="19" height="22"/>
<accessibility key="accessibilityConfiguration" label="More Actions"/>
<state key="normal" image="ellipsis" catalog="system"/>
<connections>
@ -189,7 +190,7 @@
<outlet property="attachmentsView" destination="nbq-yr-2mA" id="SVm-zl-mPb"/>
<outlet property="avatarImageView" destination="QMP-j2-HLn" id="xfS-v8-Gzu"/>
<outlet property="collapseButton" destination="O0E-Vf-XYR" id="nWd-gg-st8"/>
<outlet property="contentLabel" destination="HrJ-t9-KcD" id="s6V-cx-bBt"/>
<outlet property="contentTextView" destination="waJ-f5-LKv" id="hrR-Zg-gLY"/>
<outlet property="contentWarningLabel" destination="inI-Og-YiU" id="C7a-eK-qcx"/>
<outlet property="displayNameLabel" destination="gll-xe-FSr" id="vVS-WM-Wqx"/>
<outlet property="favoriteButton" destination="x0t-TR-jJ4" id="guV-yz-Lm6"/>

View File

@ -1,22 +1,23 @@
//
// StatusContentLabel.swift
// StatusContentTextView.swift
// Tusker
//
// Created by Shadowfacts on 10/1/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
// Created by Shadowfacts on 1/18/20.
// Copyright © 2020 Shadowfacts. All rights reserved.
//
import UIKit
import Pachyderm
class StatusContentLabel: ContentLabel {
var mastodonController: MastodonController? { navigationDelegate?.apiController }
class StatusContentTextView: ContentTextView {
var statusID: String? {
didSet {
guard let statusID = statusID, let mastodonController = mastodonController else { return }
guard let status = mastodonController.cache.status(for: statusID) else { fatalError("Can't set StatusContentLabel text without cached status \(statusID)") }
guard let statusID = statusID else { return }
guard let mastodonController = mastodonController,
let status = mastodonController.cache.status(for: statusID) else {
fatalError("Can't set StatusContentTextView text without cached status for \(statusID)")
}
setTextFromHtml(status.content)
setEmojis(status.emojis)
}