Compare commits
17 Commits
3928b2e88a
...
66fe861442
Author | SHA1 | Date |
---|---|---|
Shadowfacts | 66fe861442 | |
Shadowfacts | 11f9642cba | |
Shadowfacts | 6421d4dc12 | |
Shadowfacts | 38085eee37 | |
Shadowfacts | e19364abdf | |
Shadowfacts | fa358a3e97 | |
Shadowfacts | 5d86b35672 | |
Shadowfacts | 784c71342d | |
Shadowfacts | b5a41badcc | |
Shadowfacts | 23de131290 | |
Shadowfacts | 8178a1f339 | |
Shadowfacts | 53702a8324 | |
Shadowfacts | bb86e1aafd | |
Shadowfacts | db4312ee34 | |
Shadowfacts | ec2062ad42 | |
Shadowfacts | 1e066ac28e | |
Shadowfacts | 29e0128a55 |
12
README.md
12
README.md
|
@ -1,3 +1,15 @@
|
||||||
# Tusker
|
# Tusker
|
||||||
|
|
||||||
Tusker is a WIP iOS app for Mastodon and Pleroma.
|
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.
|
||||||
|
|
|
@ -12,7 +12,6 @@
|
||||||
0427033822B30F5F000D31B6 /* BehaviorPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0427033722B30F5F000D31B6 /* BehaviorPrefsView.swift */; };
|
0427033822B30F5F000D31B6 /* BehaviorPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0427033722B30F5F000D31B6 /* BehaviorPrefsView.swift */; };
|
||||||
0427033A22B31269000D31B6 /* AdvancedPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0427033922B31269000D31B6 /* AdvancedPrefsView.swift */; };
|
0427033A22B31269000D31B6 /* AdvancedPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0427033922B31269000D31B6 /* AdvancedPrefsView.swift */; };
|
||||||
0427037C22B316B9000D31B6 /* SilentActionPrefs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0427037B22B316B9000D31B6 /* SilentActionPrefs.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 */; };
|
0450531F22B0097E00100BA2 /* Timline+UI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0450531E22B0097E00100BA2 /* Timline+UI.swift */; };
|
||||||
0454DDAF22B462EF00B8BB8E /* GalleryExpandAnimationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0454DDAE22B462EF00B8BB8E /* GalleryExpandAnimationController.swift */; };
|
0454DDAF22B462EF00B8BB8E /* GalleryExpandAnimationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0454DDAE22B462EF00B8BB8E /* GalleryExpandAnimationController.swift */; };
|
||||||
0454DDB122B467AA00B8BB8E /* GalleryShrinkAnimationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0454DDB022B467AA00B8BB8E /* GalleryShrinkAnimationController.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 */; };
|
D61AC1D5232E9FA600C54D2D /* InstanceSelectorTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61AC1D4232E9FA600C54D2D /* InstanceSelectorTableViewController.swift */; };
|
||||||
D61AC1D8232EA42D00C54D2D /* InstanceTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61AC1D6232EA42D00C54D2D /* InstanceTableViewCell.swift */; };
|
D61AC1D8232EA42D00C54D2D /* InstanceTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61AC1D6232EA42D00C54D2D /* InstanceTableViewCell.swift */; };
|
||||||
D61AC1D9232EA42D00C54D2D /* InstanceTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D61AC1D7232EA42D00C54D2D /* InstanceTableViewCell.xib */; };
|
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 */; };
|
D626493323BD751600612E6E /* ShowCameraCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D626493123BD751600612E6E /* ShowCameraCollectionViewCell.xib */; };
|
||||||
D626493523BD94CE00612E6E /* CompositionAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D626493423BD94CE00612E6E /* CompositionAttachment.swift */; };
|
D626493523BD94CE00612E6E /* CompositionAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D626493423BD94CE00612E6E /* CompositionAttachment.swift */; };
|
||||||
D626493823C0FD0000612E6E /* AllPhotosTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D626493623C0FD0000612E6E /* AllPhotosTableViewCell.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, ); }; };
|
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 */; };
|
D6BED174212667E900F02DA0 /* TimelineStatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BED173212667E900F02DA0 /* TimelineStatusTableViewCell.swift */; };
|
||||||
D6C693EF216192C2007D6A6D /* TuskerNavigationDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.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 */; };
|
D6C693FC2162FE6F007D6A6D /* LoadingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693FB2162FE6F007D6A6D /* LoadingViewController.swift */; };
|
||||||
D6C693FE2162FEEA007D6A6D /* UIViewController+Children.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693FD2162FEEA007D6A6D /* UIViewController+Children.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 */; };
|
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 */; };
|
D6D4DDDA212518A200E1C4BB /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D6D4DDD8212518A200E1C4BB /* LaunchScreen.storyboard */; };
|
||||||
D6D4DDE5212518A200E1C4BB /* TuskerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D4DDE4212518A200E1C4BB /* TuskerTests.swift */; };
|
D6D4DDE5212518A200E1C4BB /* TuskerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D4DDE4212518A200E1C4BB /* TuskerTests.swift */; };
|
||||||
D6D4DDF0212518A200E1C4BB /* TuskerUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D4DDEF212518A200E1C4BB /* TuskerUITests.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 */; };
|
D6DD353D22F28CD000A9563A /* ContentWarningCopyMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD353C22F28CD000A9563A /* ContentWarningCopyMode.swift */; };
|
||||||
D6DD353F22F502EC00A9563A /* Preferences+Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD353E22F502EC00A9563A /* Preferences+Notification.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 */; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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; };
|
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>"; };
|
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>"; };
|
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; path = ContentWarningCopyMode.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; path = "Preferences+Notification.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>"; };
|
|
||||||
D6E0DC8D216EDF1E00369478 /* Previewing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Previewing.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>"; };
|
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>"; };
|
D6E6F26421604242006A8599 /* CharacterCounterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterCounterTests.swift; sourceTree = "<group>"; };
|
||||||
|
@ -862,6 +864,7 @@
|
||||||
D66362702136338600C9CBA2 /* ComposeViewController.swift */,
|
D66362702136338600C9CBA2 /* ComposeViewController.swift */,
|
||||||
D626493423BD94CE00612E6E /* CompositionAttachment.swift */,
|
D626493423BD94CE00612E6E /* CompositionAttachment.swift */,
|
||||||
D6285B5221EA708700FE4B39 /* StatusFormat.swift */,
|
D6285B5221EA708700FE4B39 /* StatusFormat.swift */,
|
||||||
|
D620483123D2A6A3008A63EF /* CompositionState.swift */,
|
||||||
);
|
);
|
||||||
path = Compose;
|
path = Compose;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1108,9 +1111,9 @@
|
||||||
D6BED1722126661300F02DA0 /* Views */ = {
|
D6BED1722126661300F02DA0 /* Views */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
D6D58DF822074B74009C8DD9 /* LinkLabel.swift */,
|
D620483323D3801D008A63EF /* LinkTextView.swift */,
|
||||||
04496BD621625361001F1B23 /* ContentLabel.swift */,
|
D620483523D38075008A63EF /* ContentTextView.swift */,
|
||||||
D6C693F82162E4DB007D6A6D /* StatusContentLabel.swift */,
|
D620483723D38190008A63EF /* StatusContentTextView.swift */,
|
||||||
D641C77E213DC78A004B4513 /* InlineTextAttachment.swift */,
|
D641C77E213DC78A004B4513 /* InlineTextAttachment.swift */,
|
||||||
04ED00B021481ED800567C53 /* SteppedProgressView.swift */,
|
04ED00B021481ED800567C53 /* SteppedProgressView.swift */,
|
||||||
D627943A23A55BA600D38C68 /* NavigableTableViewCell.swift */,
|
D627943A23A55BA600D38C68 /* NavigableTableViewCell.swift */,
|
||||||
|
@ -1630,9 +1633,7 @@
|
||||||
04DACE8C212CB14B009840C4 /* MainTabBarViewController.swift in Sources */,
|
04DACE8C212CB14B009840C4 /* MainTabBarViewController.swift in Sources */,
|
||||||
D6AEBB412321642700E5038B /* SendMesasgeActivity.swift in Sources */,
|
D6AEBB412321642700E5038B /* SendMesasgeActivity.swift in Sources */,
|
||||||
D6BC9DB1232C61BC002CA326 /* NotificationsPageViewController.swift in Sources */,
|
D6BC9DB1232C61BC002CA326 /* NotificationsPageViewController.swift in Sources */,
|
||||||
D6C693F92162E4DB007D6A6D /* StatusContentLabel.swift in Sources */,
|
|
||||||
D6BC9DB3232D4C07002CA326 /* WellnessPrefsView.swift in Sources */,
|
D6BC9DB3232D4C07002CA326 /* WellnessPrefsView.swift in Sources */,
|
||||||
D6D58DF922074B74009C8DD9 /* LinkLabel.swift in Sources */,
|
|
||||||
0454DDAF22B462EF00B8BB8E /* GalleryExpandAnimationController.swift in Sources */,
|
0454DDAF22B462EF00B8BB8E /* GalleryExpandAnimationController.swift in Sources */,
|
||||||
D64BC18F23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.swift in Sources */,
|
D64BC18F23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.swift in Sources */,
|
||||||
D6A3BC8A2321F79B00FD64D5 /* AccountTableViewCell.swift in Sources */,
|
D6A3BC8A2321F79B00FD64D5 /* AccountTableViewCell.swift in Sources */,
|
||||||
|
@ -1642,6 +1643,7 @@
|
||||||
D667E5F52135BCD50057A976 /* ConversationTableViewController.swift in Sources */,
|
D667E5F52135BCD50057A976 /* ConversationTableViewController.swift in Sources */,
|
||||||
D6945C3623AC6C09005C403C /* SavedInstancesManager.swift in Sources */,
|
D6945C3623AC6C09005C403C /* SavedInstancesManager.swift in Sources */,
|
||||||
D6C7D27D22B6EBF800071952 /* AttachmentsContainerView.swift in Sources */,
|
D6C7D27D22B6EBF800071952 /* AttachmentsContainerView.swift in Sources */,
|
||||||
|
D620483823D38190008A63EF /* StatusContentTextView.swift in Sources */,
|
||||||
D6F953F021251A2900CF0F2B /* MastodonController.swift in Sources */,
|
D6F953F021251A2900CF0F2B /* MastodonController.swift in Sources */,
|
||||||
0411610022B442870030A9B7 /* AttachmentViewController.swift in Sources */,
|
0411610022B442870030A9B7 /* AttachmentViewController.swift in Sources */,
|
||||||
D611C2CF232DC61100C86A49 /* HashtagTableViewCell.swift in Sources */,
|
D611C2CF232DC61100C86A49 /* HashtagTableViewCell.swift in Sources */,
|
||||||
|
@ -1652,6 +1654,7 @@
|
||||||
D66362712136338600C9CBA2 /* ComposeViewController.swift in Sources */,
|
D66362712136338600C9CBA2 /* ComposeViewController.swift in Sources */,
|
||||||
D6AC956723C4347E008C9946 /* SceneDelegate.swift in Sources */,
|
D6AC956723C4347E008C9946 /* SceneDelegate.swift in Sources */,
|
||||||
D6BC9DD7232D7811002CA326 /* TimelinesPageViewController.swift in Sources */,
|
D6BC9DD7232D7811002CA326 /* TimelinesPageViewController.swift in Sources */,
|
||||||
|
D620483623D38075008A63EF /* ContentTextView.swift in Sources */,
|
||||||
D6028B9B2150811100F223B9 /* MastodonCache.swift in Sources */,
|
D6028B9B2150811100F223B9 /* MastodonCache.swift in Sources */,
|
||||||
D6A3BC802321B7E600FD64D5 /* FollowNotificationGroupTableViewCell.swift in Sources */,
|
D6A3BC802321B7E600FD64D5 /* FollowNotificationGroupTableViewCell.swift in Sources */,
|
||||||
D627944D23A9A03D00D38C68 /* ListTimelineViewController.swift in Sources */,
|
D627944D23A9A03D00D38C68 /* ListTimelineViewController.swift in Sources */,
|
||||||
|
@ -1707,9 +1710,9 @@
|
||||||
D626493823C0FD0000612E6E /* AllPhotosTableViewCell.swift in Sources */,
|
D626493823C0FD0000612E6E /* AllPhotosTableViewCell.swift in Sources */,
|
||||||
D627943B23A55BA600D38C68 /* NavigableTableViewCell.swift in Sources */,
|
D627943B23A55BA600D38C68 /* NavigableTableViewCell.swift in Sources */,
|
||||||
D6333B792139AEFD00CE884A /* Date+TimeAgo.swift in Sources */,
|
D6333B792139AEFD00CE884A /* Date+TimeAgo.swift in Sources */,
|
||||||
|
D620483423D3801D008A63EF /* LinkTextView.swift in Sources */,
|
||||||
D641C77F213DC78A004B4513 /* InlineTextAttachment.swift in Sources */,
|
D641C77F213DC78A004B4513 /* InlineTextAttachment.swift in Sources */,
|
||||||
D627943523A5525100D38C68 /* StatusActivity.swift in Sources */,
|
D627943523A5525100D38C68 /* StatusActivity.swift in Sources */,
|
||||||
04496BD721625361001F1B23 /* ContentLabel.swift in Sources */,
|
|
||||||
D663626C21361C6700C9CBA2 /* Account+Preferences.swift in Sources */,
|
D663626C21361C6700C9CBA2 /* Account+Preferences.swift in Sources */,
|
||||||
D6333B372137838300CE884A /* AttributedString+Helpers.swift in Sources */,
|
D6333B372137838300CE884A /* AttributedString+Helpers.swift in Sources */,
|
||||||
D61AC1D8232EA42D00C54D2D /* InstanceTableViewCell.swift in Sources */,
|
D61AC1D8232EA42D00C54D2D /* InstanceTableViewCell.swift in Sources */,
|
||||||
|
@ -1729,6 +1732,7 @@
|
||||||
D6A3BC7C232195C600FD64D5 /* ActionNotificationGroupTableViewCell.swift in Sources */,
|
D6A3BC7C232195C600FD64D5 /* ActionNotificationGroupTableViewCell.swift in Sources */,
|
||||||
D6F953EC212519E700CF0F2B /* TimelineTableViewController.swift in Sources */,
|
D6F953EC212519E700CF0F2B /* TimelineTableViewController.swift in Sources */,
|
||||||
04586B4122B2FFB10021BD04 /* PreferencesView.swift in Sources */,
|
04586B4122B2FFB10021BD04 /* PreferencesView.swift in Sources */,
|
||||||
|
D620483223D2A6A3008A63EF /* CompositionState.swift in Sources */,
|
||||||
D6BC9DB5232D4CE3002CA326 /* NotificationsMode.swift in Sources */,
|
D6BC9DB5232D4CE3002CA326 /* NotificationsMode.swift in Sources */,
|
||||||
D667E5EB21349EF80057A976 /* ProfileHeaderTableViewCell.swift in Sources */,
|
D667E5EB21349EF80057A976 /* ProfileHeaderTableViewCell.swift in Sources */,
|
||||||
04D14BB022B34A2800642648 /* GalleryViewController.swift in Sources */,
|
04D14BB022B34A2800642648 /* GalleryViewController.swift in Sources */,
|
||||||
|
|
|
@ -88,6 +88,8 @@
|
||||||
<key>UISupportedInterfaceOrientations</key>
|
<key>UISupportedInterfaceOrientations</key>
|
||||||
<array>
|
<array>
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
</array>
|
</array>
|
||||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||||
<array>
|
<array>
|
||||||
|
|
|
@ -45,6 +45,7 @@ class Preferences: Codable, ObservableObject {
|
||||||
self.defaultPostVisibility = try container.decode(Status.Visibility.self, forKey: .defaultPostVisibility)
|
self.defaultPostVisibility = try container.decode(Status.Visibility.self, forKey: .defaultPostVisibility)
|
||||||
self.automaticallySaveDrafts = try container.decode(Bool.self, forKey: .automaticallySaveDrafts)
|
self.automaticallySaveDrafts = try container.decode(Bool.self, forKey: .automaticallySaveDrafts)
|
||||||
self.contentWarningCopyMode = try container.decode(ContentWarningCopyMode.self, forKey: .contentWarningCopyMode)
|
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.blurAllMedia = try container.decode(Bool.self, forKey: .blurAllMedia)
|
||||||
self.openLinksInApps = try container.decode(Bool.self, forKey: .openLinksInApps)
|
self.openLinksInApps = try container.decode(Bool.self, forKey: .openLinksInApps)
|
||||||
self.useInAppSafari = try container.decode(Bool.self, forKey: .useInAppSafari)
|
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(defaultPostVisibility, forKey: .defaultPostVisibility)
|
||||||
try container.encode(automaticallySaveDrafts, forKey: .automaticallySaveDrafts)
|
try container.encode(automaticallySaveDrafts, forKey: .automaticallySaveDrafts)
|
||||||
try container.encode(contentWarningCopyMode, forKey: .contentWarningCopyMode)
|
try container.encode(contentWarningCopyMode, forKey: .contentWarningCopyMode)
|
||||||
|
try container.encode(requireAttachmentDescriptions, forKey: .requireAttachmentDescriptions)
|
||||||
try container.encode(blurAllMedia, forKey: .blurAllMedia)
|
try container.encode(blurAllMedia, forKey: .blurAllMedia)
|
||||||
try container.encode(openLinksInApps, forKey: .openLinksInApps)
|
try container.encode(openLinksInApps, forKey: .openLinksInApps)
|
||||||
try container.encode(useInAppSafari, forKey: .useInAppSafari)
|
try container.encode(useInAppSafari, forKey: .useInAppSafari)
|
||||||
|
@ -90,6 +92,7 @@ class Preferences: Codable, ObservableObject {
|
||||||
@Published var defaultPostVisibility = Status.Visibility.public
|
@Published var defaultPostVisibility = Status.Visibility.public
|
||||||
@Published var automaticallySaveDrafts = true
|
@Published var automaticallySaveDrafts = true
|
||||||
@Published var contentWarningCopyMode = ContentWarningCopyMode.asIs
|
@Published var contentWarningCopyMode = ContentWarningCopyMode.asIs
|
||||||
|
@Published var requireAttachmentDescriptions = false
|
||||||
@Published var blurAllMedia = false
|
@Published var blurAllMedia = false
|
||||||
@Published var openLinksInApps = true
|
@Published var openLinksInApps = true
|
||||||
@Published var useInAppSafari = true
|
@Published var useInAppSafari = true
|
||||||
|
@ -112,6 +115,7 @@ class Preferences: Codable, ObservableObject {
|
||||||
case defaultPostVisibility
|
case defaultPostVisibility
|
||||||
case automaticallySaveDrafts
|
case automaticallySaveDrafts
|
||||||
case contentWarningCopyMode
|
case contentWarningCopyMode
|
||||||
|
case requireAttachmentDescriptions
|
||||||
case blurAllMedia
|
case blurAllMedia
|
||||||
case openLinksInApps
|
case openLinksInApps
|
||||||
case useInAppSafari
|
case useInAppSafari
|
||||||
|
|
|
@ -39,6 +39,12 @@ class ComposeViewController: UIViewController {
|
||||||
weak var xcbSession: XCBSession?
|
weak var xcbSession: XCBSession?
|
||||||
var postedStatus: Status?
|
var postedStatus: Status?
|
||||||
|
|
||||||
|
var compositionState: CompositionState = .valid {
|
||||||
|
didSet {
|
||||||
|
postBarButtonItem.isEnabled = compositionState.isValid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
weak var postBarButtonItem: UIBarButtonItem!
|
weak var postBarButtonItem: UIBarButtonItem!
|
||||||
var visibilityBarButtonItem: UIBarButtonItem!
|
var visibilityBarButtonItem: UIBarButtonItem!
|
||||||
var contentWarningBarButtonItem: 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
|
// we have to set the font here, because the monospaced digit font is not available in IB
|
||||||
charactersRemainingLabel.font = .monospacedDigitSystemFont(ofSize: 17, weight: .regular)
|
charactersRemainingLabel.font = .monospacedDigitSystemFont(ofSize: 17, weight: .regular)
|
||||||
updateCharactersRemaining()
|
updateCharactersRemaining()
|
||||||
|
updateAttachmentDescriptionsRequired()
|
||||||
updatePlaceholder()
|
updatePlaceholder()
|
||||||
|
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(contentWarningTextFieldDidChange), name: UITextField.textDidChangeNotification, object: contentWarningTextField)
|
NotificationCenter.default.addObserver(self, selector: #selector(contentWarningTextFieldDidChange), name: UITextField.textDidChangeNotification, object: contentWarningTextField)
|
||||||
|
@ -270,17 +277,29 @@ class ComposeViewController: UIViewController {
|
||||||
scrollView.scrollIndicatorInsets = scrollView.contentInset
|
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() {
|
func updateCharactersRemaining() {
|
||||||
// TODO: include CW char count
|
|
||||||
let count = CharacterCounter.count(text: statusTextView.text)
|
let count = CharacterCounter.count(text: statusTextView.text)
|
||||||
let cwCount = contentWarningEnabled ? (contentWarningTextField.text?.count ?? 0) : 0
|
let cwCount = contentWarningEnabled ? (contentWarningTextField.text?.count ?? 0) : 0
|
||||||
let remaining = (mastodonController.instance.maxStatusCharacters ?? 500) - count - cwCount
|
let remaining = (mastodonController.instance.maxStatusCharacters ?? 500) - count - cwCount
|
||||||
if remaining < 0 {
|
if remaining < 0 {
|
||||||
charactersRemainingLabel.textColor = .red
|
charactersRemainingLabel.textColor = .red
|
||||||
postBarButtonItem.isEnabled = false
|
compositionState.formUnion(.tooManyCharacters)
|
||||||
} else {
|
} else {
|
||||||
charactersRemainingLabel.textColor = .darkGray
|
charactersRemainingLabel.textColor = .darkGray
|
||||||
postBarButtonItem.isEnabled = true
|
compositionState.subtract(.tooManyCharacters)
|
||||||
}
|
}
|
||||||
charactersRemainingLabel.text = String(remaining)
|
charactersRemainingLabel.text = String(remaining)
|
||||||
charactersRemainingLabel.accessibilityLabel = String(format: NSLocalizedString("%d characters remaining", comment: "compose characters remaining accessibility label"), remaining)
|
charactersRemainingLabel.accessibilityLabel = String(format: NSLocalizedString("%d characters remaining", comment: "compose characters remaining accessibility label"), remaining)
|
||||||
|
@ -458,7 +477,7 @@ class ComposeViewController: UIViewController {
|
||||||
saveDraft()
|
saveDraft()
|
||||||
|
|
||||||
// disable post button while sending post request
|
// disable post button while sending post request
|
||||||
postBarButtonItem.isEnabled = false
|
compositionState.formUnion(.currentlyPosting)
|
||||||
|
|
||||||
let contentWarning: String?
|
let contentWarning: String?
|
||||||
if contentWarningEnabled, let cwText = contentWarningTextField.text, !cwText.isEmpty {
|
if contentWarningEnabled, let cwText = contentWarningTextField.text, !cwText.isEmpty {
|
||||||
|
@ -579,6 +598,7 @@ extension ComposeViewController: AssetPickerViewControllerDelegate {
|
||||||
}
|
}
|
||||||
func assetPicker(_ assetPicker: AssetPickerViewController, didSelectAttachments attachments: [CompositionAttachment]) {
|
func assetPicker(_ assetPicker: AssetPickerViewController, didSelectAttachments attachments: [CompositionAttachment]) {
|
||||||
selectedAttachments.append(contentsOf: attachments)
|
selectedAttachments.append(contentsOf: attachments)
|
||||||
|
updateAttachmentDescriptionsRequired()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -587,6 +607,11 @@ extension ComposeViewController: ComposeMediaViewDelegate {
|
||||||
let index = attachmentsStackView.arrangedSubviews.firstIndex(of: mediaView)!
|
let index = attachmentsStackView.arrangedSubviews.firstIndex(of: mediaView)!
|
||||||
selectedAttachments.remove(at: index)
|
selectedAttachments.remove(at: index)
|
||||||
updateAddAttachmentButton()
|
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
|
// call the delegate method manually, since setting the text property doesn't call it
|
||||||
mediaView.textViewDidChange(mediaView.descriptionTextView)
|
mediaView.textViewDidChange(mediaView.descriptionTextView)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateAttachmentDescriptionsRequired()
|
||||||
}
|
}
|
||||||
|
|
||||||
func draftSelectionCompleted() {
|
func draftSelectionCompleted() {
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -35,6 +35,14 @@ class GalleryViewController: UIPageViewController, UIPageViewControllerDataSourc
|
||||||
return viewControllers?.first
|
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) {
|
init(attachments: [Attachment], sourcesInfo: [LargeImageViewController.SourceInfo?], startIndex: Int) {
|
||||||
self.attachments = attachments
|
self.attachments = attachments
|
||||||
self.sourcesInfo = sourcesInfo
|
self.sourcesInfo = sourcesInfo
|
||||||
|
|
|
@ -12,6 +12,14 @@ class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate {
|
||||||
|
|
||||||
let mastodonController: MastodonController
|
let mastodonController: MastodonController
|
||||||
|
|
||||||
|
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
|
||||||
|
if UIDevice.current.userInterfaceIdiom == .phone {
|
||||||
|
return .portrait
|
||||||
|
} else {
|
||||||
|
return .all
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
init(mastodonController: MastodonController) {
|
init(mastodonController: MastodonController) {
|
||||||
self.mastodonController = mastodonController
|
self.mastodonController = mastodonController
|
||||||
|
|
||||||
|
@ -20,7 +28,7 @@ class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate {
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
|
@ -211,7 +211,9 @@ class NotificationsTableViewController: EnhancedTableViewController {
|
||||||
self.mastodonController.cache.addAll(statuses: newNotifications.compactMap { $0.status })
|
self.mastodonController.cache.addAll(statuses: newNotifications.compactMap { $0.status })
|
||||||
self.mastodonController.cache.addAll(accounts: newNotifications.map { $0.account })
|
self.mastodonController.cache.addAll(accounts: newNotifications.map { $0.account })
|
||||||
|
|
||||||
self.newer = pagination?.newer
|
if let newer = pagination?.newer {
|
||||||
|
self.newer = newer
|
||||||
|
}
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.refreshControl?.endRefreshing()
|
self.refreshControl?.endRefreshing()
|
||||||
|
|
|
@ -29,6 +29,14 @@ class InstanceSelectorTableViewController: UITableViewController {
|
||||||
var urlHandler: AnyCancellable?
|
var urlHandler: AnyCancellable?
|
||||||
var currentQuery: String?
|
var currentQuery: String?
|
||||||
|
|
||||||
|
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
|
||||||
|
if UIDevice.current.userInterfaceIdiom == .phone {
|
||||||
|
return .portrait
|
||||||
|
} else {
|
||||||
|
return .all
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
super.init(style: .grouped)
|
super.init(style: .grouped)
|
||||||
|
|
||||||
|
@ -159,7 +167,9 @@ class InstanceSelectorTableViewController: UITableViewController {
|
||||||
}
|
}
|
||||||
switch item {
|
switch item {
|
||||||
case let .selected(instance):
|
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):
|
case let .recommended(instance):
|
||||||
var components = URLComponents()
|
var components = URLComponents()
|
||||||
components.scheme = "https"
|
components.scheme = "https"
|
||||||
|
|
|
@ -40,6 +40,9 @@ struct BehaviorPrefsView: View {
|
||||||
Text("Prepend 're: '").tag(ContentWarningCopyMode.prependRe)
|
Text("Prepend 're: '").tag(ContentWarningCopyMode.prependRe)
|
||||||
Text("Don't copy").tag(ContentWarningCopyMode.doNotCopy)
|
Text("Don't copy").tag(ContentWarningCopyMode.doNotCopy)
|
||||||
}
|
}
|
||||||
|
Toggle(isOn: $preferences.requireAttachmentDescriptions) {
|
||||||
|
Text("Require Attachment Descriptions")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -222,12 +222,32 @@ class ProfileTableViewController: EnhancedTableViewController {
|
||||||
self.mastodonController.cache.addAll(statuses: newStatuses)
|
self.mastodonController.cache.addAll(statuses: newStatuses)
|
||||||
self.timelineSegments[0].insert(contentsOf: newStatuses.map { ($0.id, .unknown) }, at: 0)
|
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 {
|
DispatchQueue.main.async {
|
||||||
self.refreshControl?.endRefreshing()
|
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) {
|
@objc func composePressed(_ sender: Any) {
|
||||||
|
@ -247,7 +267,7 @@ extension ProfileTableViewController: StatusTableViewCellDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ProfileTableViewController: ProfileHeaderTableViewCellDelegate {
|
extension ProfileTableViewController: ProfileHeaderTableViewCellDelegate {
|
||||||
func showMoreOptions() {
|
func showMoreOptions(cell: ProfileHeaderTableViewCell) {
|
||||||
let account = mastodonController.cache.account(for: accountID)!
|
let account = mastodonController.cache.account(for: accountID)!
|
||||||
|
|
||||||
mastodonController.cache.relationship(for: account.id) { [weak self] (relationship) in
|
mastodonController.cache.relationship(for: account.id) { [weak self] (relationship) in
|
||||||
|
@ -262,6 +282,7 @@ extension ProfileTableViewController: ProfileHeaderTableViewCellDelegate {
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
let activityController = UIActivityViewController(activityItems: [account.url, account], applicationActivities: customActivities)
|
let activityController = UIActivityViewController(activityItems: [account.url, account], applicationActivities: customActivities)
|
||||||
activityController.completionWithItemsHandler = OpenInSafariActivity.completionHandler(viewController: self, url: account.url)
|
activityController.completionWithItemsHandler = OpenInSafariActivity.completionHandler(viewController: self, url: account.url)
|
||||||
|
activityController.popoverPresentationController?.sourceView = cell.moreButtonVisualEffectView
|
||||||
self.present(activityController, animated: true)
|
self.present(activityController, animated: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -131,6 +131,11 @@ class TimelineTableViewController: EnhancedTableViewController {
|
||||||
self.newer = pagination?.newer
|
self.newer = pagination?.newer
|
||||||
self.mastodonController.cache.addAll(statuses: newStatuses)
|
self.mastodonController.cache.addAll(statuses: newStatuses)
|
||||||
self.timelineSegments[0].insert(contentsOf: newStatuses.map { ($0.id, .unknown) }, at: 0)
|
self.timelineSegments[0].insert(contentsOf: newStatuses.map { ($0.id, .unknown) }, at: 0)
|
||||||
|
|
||||||
|
if let newer = pagination?.newer {
|
||||||
|
self.newer = newer
|
||||||
|
}
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.refreshControl?.endRefreshing()
|
self.refreshControl?.endRefreshing()
|
||||||
|
|
||||||
|
|
|
@ -24,7 +24,7 @@ extension MenuPreviewProvider {
|
||||||
|
|
||||||
private var mastodonController: MastodonController? { navigationDelegate?.apiController }
|
private var mastodonController: MastodonController? { navigationDelegate?.apiController }
|
||||||
|
|
||||||
func actionsForProfile(accountID: String) -> [UIAction] {
|
func actionsForProfile(accountID: String, sourceView: UIView?) -> [UIAction] {
|
||||||
guard let mastodonController = mastodonController,
|
guard let mastodonController = mastodonController,
|
||||||
let account = mastodonController.cache.account(for: accountID) else { return [] }
|
let account = mastodonController.cache.account(for: accountID) else { return [] }
|
||||||
return [
|
return [
|
||||||
|
@ -35,27 +35,27 @@ extension MenuPreviewProvider {
|
||||||
self.navigationDelegate?.compose(mentioning: account.acct)
|
self.navigationDelegate?.compose(mentioning: account.acct)
|
||||||
}),
|
}),
|
||||||
createAction(identifier: "share", title: "Share...", systemImageName: "square.and.arrow.up", handler: { (_) in
|
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 [
|
return [
|
||||||
createAction(identifier: "openinsafari", title: "Open in Safari", systemImageName: "safari", handler: { (_) in
|
createAction(identifier: "openinsafari", title: "Open in Safari", systemImageName: "safari", handler: { (_) in
|
||||||
self.navigationDelegate?.selected(url: url)
|
self.navigationDelegate?.selected(url: url)
|
||||||
}),
|
}),
|
||||||
createAction(identifier: "share", title: "Share...", systemImageName: "square.and.arrow.up", handler: { (_) in
|
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] {
|
func actionsForHashtag(_ hashtag: Hashtag, sourceView: UIView?) -> [UIAction] {
|
||||||
return actionsForURL(hashtag.url)
|
return actionsForURL(hashtag.url, sourceView: sourceView)
|
||||||
}
|
}
|
||||||
|
|
||||||
func actionsForStatus(statusID: String) -> [UIAction] {
|
func actionsForStatus(statusID: String, sourceView: UIView?) -> [UIAction] {
|
||||||
guard let mastodonController = mastodonController,
|
guard let mastodonController = mastodonController,
|
||||||
let status = mastodonController.cache.status(for: statusID) else { return [] }
|
let status = mastodonController.cache.status(for: statusID) else { return [] }
|
||||||
return [
|
return [
|
||||||
|
@ -66,7 +66,7 @@ extension MenuPreviewProvider {
|
||||||
self.navigationDelegate?.selected(url: status.url!)
|
self.navigationDelegate?.selected(url: status.url!)
|
||||||
}),
|
}),
|
||||||
createAction(identifier: "share", title: "Share...", systemImageName: "square.and.arrow.up", handler: { (_) in
|
createAction(identifier: "share", title: "Share...", systemImageName: "square.and.arrow.up", handler: { (_) in
|
||||||
self.navigationDelegate?.showMoreOptions(forStatus: statusID)
|
self.navigationDelegate?.showMoreOptions(forStatus: statusID, sourceView: sourceView)
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,11 +46,11 @@ protocol TuskerNavigationDelegate {
|
||||||
|
|
||||||
func showGallery(attachments: [Attachment], sourceViews: [UIImageView?], startIndex: Int)
|
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])
|
func showFollowedByList(accountIDs: [String])
|
||||||
|
|
||||||
|
@ -60,7 +60,11 @@ protocol TuskerNavigationDelegate {
|
||||||
extension TuskerNavigationDelegate where Self: UIViewController {
|
extension TuskerNavigationDelegate where Self: UIViewController {
|
||||||
|
|
||||||
func show(_ vc: UIViewController) {
|
func show(_ vc: UIViewController) {
|
||||||
show(vc, sender: self)
|
if vc is LargeImageViewController || vc is GalleryViewController || vc is SFSafariViewController {
|
||||||
|
present(vc, animated: true)
|
||||||
|
} else {
|
||||||
|
show(vc, sender: self)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func selected(account accountID: String) {
|
func selected(account accountID: String) {
|
||||||
|
@ -184,7 +188,7 @@ extension TuskerNavigationDelegate where Self: UIViewController {
|
||||||
present(gallery(attachments: attachments, sourceViews: sourceViews, startIndex: startIndex), animated: true)
|
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] = [
|
let customActivites: [UIActivity] = [
|
||||||
OpenInSafariActivity()
|
OpenInSafariActivity()
|
||||||
]
|
]
|
||||||
|
@ -193,7 +197,7 @@ extension TuskerNavigationDelegate where Self: UIViewController {
|
||||||
return activityController
|
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 status = apiController.cache.status(for: statusID) else { fatalError("Missing cached status \(statusID)") }
|
||||||
guard let url = status.url else { fatalError("Missing url for status \(statusID)") }
|
guard let url = status.url else { fatalError("Missing url for status \(statusID)") }
|
||||||
var customActivites: [UIActivity] = [OpenInSafariActivity()]
|
var customActivites: [UIActivity] = [OpenInSafariActivity()]
|
||||||
|
@ -212,21 +216,27 @@ extension TuskerNavigationDelegate where Self: UIViewController {
|
||||||
return activityController
|
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)") }
|
guard let account = apiController.cache.account(for: accountID) else { fatalError("Missing cached account \(accountID)") }
|
||||||
return moreOptions(forURL: account.url)
|
return moreOptions(forURL: account.url)
|
||||||
}
|
}
|
||||||
|
|
||||||
func showMoreOptions(forStatus statusID: String) {
|
func showMoreOptions(forStatus statusID: String, sourceView: UIView?) {
|
||||||
present(moreOptions(forStatus: statusID), animated: true)
|
let vc = moreOptions(forStatus: statusID)
|
||||||
|
vc.popoverPresentationController?.sourceView = sourceView
|
||||||
|
present(vc, animated: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func showMoreOptions(forURL url: URL) {
|
func showMoreOptions(forURL url: URL, sourceView: UIView?) {
|
||||||
present(moreOptions(forURL: url), animated: true)
|
let vc = moreOptions(forURL: url)
|
||||||
|
vc.popoverPresentationController?.sourceView = sourceView
|
||||||
|
present(vc, animated: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func showMoreOptions(forAccount accountID: String) {
|
func showMoreOptions(forAccount accountID: String, sourceView: UIView?) {
|
||||||
present(moreOptions(forAccount: accountID), animated: true)
|
let vc = moreOptions(forAccount: accountID)
|
||||||
|
vc.popoverPresentationController?.sourceView = sourceView
|
||||||
|
present(vc, animated: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func showFollowedByList(accountIDs: [String]) {
|
func showFollowedByList(accountIDs: [String]) {
|
||||||
|
|
|
@ -70,10 +70,9 @@ extension AccountTableViewCell: MenuPreviewProvider {
|
||||||
|
|
||||||
func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? {
|
func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? {
|
||||||
guard let mastodonController = mastodonController else { return nil }
|
guard let mastodonController = mastodonController else { return nil }
|
||||||
return (content: {
|
return (
|
||||||
ProfileTableViewController(accountID: self.accountID, mastodonController: mastodonController)
|
content: { ProfileTableViewController(accountID: self.accountID, mastodonController: mastodonController) },
|
||||||
}, actions: {
|
actions: { self.actionsForProfile(accountID: self.accountID, sourceView: self.avatarImageView) }
|
||||||
self.actionsForProfile(accountID: self.accountID)
|
)
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ import AVFoundation
|
||||||
|
|
||||||
protocol ComposeMediaViewDelegate {
|
protocol ComposeMediaViewDelegate {
|
||||||
func didRemoveMedia(_ mediaView: ComposeMediaView)
|
func didRemoveMedia(_ mediaView: ComposeMediaView)
|
||||||
|
func descriptionTextViewDidChange(_ mediaView: ComposeMediaView)
|
||||||
}
|
}
|
||||||
|
|
||||||
class ComposeMediaView: UIView {
|
class ComposeMediaView: UIView {
|
||||||
|
@ -69,5 +70,6 @@ class ComposeMediaView: UIView {
|
||||||
extension ComposeMediaView: UITextViewDelegate {
|
extension ComposeMediaView: UITextViewDelegate {
|
||||||
func textViewDidChange(_ textView: UITextView) {
|
func textViewDidChange(_ textView: UITextView) {
|
||||||
placeholderLabel.isHidden = !descriptionTextView.text.isEmpty
|
placeholderLabel.isHidden = !descriptionTextView.text.isEmpty
|
||||||
|
delegate?.descriptionTextViewDidChange(self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ class ComposeStatusReplyView: UIView {
|
||||||
@IBOutlet weak var avatarImageView: UIImageView!
|
@IBOutlet weak var avatarImageView: UIImageView!
|
||||||
@IBOutlet weak var displayNameLabel: UILabel!
|
@IBOutlet weak var displayNameLabel: UILabel!
|
||||||
@IBOutlet weak var usernameLabel: UILabel!
|
@IBOutlet weak var usernameLabel: UILabel!
|
||||||
@IBOutlet weak var contentLabel: StatusContentLabel!
|
@IBOutlet weak var statusContentTextView: StatusContentTextView!
|
||||||
|
|
||||||
static func create() -> ComposeStatusReplyView {
|
static func create() -> ComposeStatusReplyView {
|
||||||
return UINib(nibName: "ComposeStatusReplyView", bundle: nil).instantiate(withOwner: nil, options: nil).first as! 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) {
|
func updateUI(for status: Status) {
|
||||||
displayNameLabel.text = status.account.realDisplayName
|
displayNameLabel.text = status.account.realDisplayName
|
||||||
usernameLabel.text = "@\(status.account.acct)"
|
usernameLabel.text = "@\(status.account.acct)"
|
||||||
contentLabel.statusID = status.id
|
statusContentTextView.statusID = status.id
|
||||||
|
|
||||||
ImageCache.avatars.get(status.account.avatar) { (data) in
|
ImageCache.avatars.get(status.account.avatar) { (data) in
|
||||||
guard let data = data else { return }
|
guard let data = data else { return }
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
<?xml version="1.0" encoding="UTF-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"/>
|
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||||
<dependencies>
|
<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="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
</dependencies>
|
</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"/>
|
<color key="textColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
<nil key="highlightedColor"/>
|
<nil key="highlightedColor"/>
|
||||||
</label>
|
</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">
|
<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="24.5" width="301" height="626.5"/>
|
<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"/>
|
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||||
<nil key="textColor"/>
|
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
|
||||||
<nil key="highlightedColor"/>
|
</textView>
|
||||||
</label>
|
|
||||||
</subviews>
|
</subviews>
|
||||||
<constraints>
|
<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 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="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 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 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 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>
|
</constraints>
|
||||||
</view>
|
</view>
|
||||||
</subviews>
|
</subviews>
|
||||||
|
@ -72,8 +73,8 @@
|
||||||
<viewLayoutGuide key="safeArea" id="vUN-kp-3ea"/>
|
<viewLayoutGuide key="safeArea" id="vUN-kp-3ea"/>
|
||||||
<connections>
|
<connections>
|
||||||
<outlet property="avatarImageView" destination="Ypn-Ed-MTq" id="eea-bc-klc"/>
|
<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="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"/>
|
<outlet property="usernameLabel" destination="0yZ-71-eTj" id="VQm-Dq-3zP"/>
|
||||||
</connections>
|
</connections>
|
||||||
<point key="canvasLocation" x="138.40000000000001" y="-72.863568215892059"/>
|
<point key="canvasLocation" x="138.40000000000001" y="-72.863568215892059"/>
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,7 +14,7 @@ class InstanceTableViewCell: UITableViewCell {
|
||||||
@IBOutlet weak var thumbnailImageView: UIImageView!
|
@IBOutlet weak var thumbnailImageView: UIImageView!
|
||||||
@IBOutlet weak var domainLabel: UILabel!
|
@IBOutlet weak var domainLabel: UILabel!
|
||||||
@IBOutlet weak var adultLabel: UILabel!
|
@IBOutlet weak var adultLabel: UILabel!
|
||||||
@IBOutlet weak var descriptionLabel: ContentLabel!
|
@IBOutlet weak var descriptionTextView: ContentTextView!
|
||||||
|
|
||||||
var instance: Instance?
|
var instance: Instance?
|
||||||
var selectorInstance: InstanceSelector.Instance?
|
var selectorInstance: InstanceSelector.Instance?
|
||||||
|
@ -37,7 +37,7 @@ class InstanceTableViewCell: UITableViewCell {
|
||||||
|
|
||||||
domainLabel.text = instance.domain
|
domainLabel.text = instance.domain
|
||||||
adultLabel.isHidden = instance.category != .adult
|
adultLabel.isHidden = instance.category != .adult
|
||||||
descriptionLabel.setTextFromHtml(instance.description)
|
descriptionTextView.setTextFromHtml(instance.description)
|
||||||
updateThumbnail(url: instance.proxiedThumbnailURL)
|
updateThumbnail(url: instance.proxiedThumbnailURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,7 +47,7 @@ class InstanceTableViewCell: UITableViewCell {
|
||||||
|
|
||||||
domainLabel.text = URLComponents(string: instance.uri)?.host ?? instance.uri
|
domainLabel.text = URLComponents(string: instance.uri)?.host ?? instance.uri
|
||||||
adultLabel.isHidden = true
|
adultLabel.isHidden = true
|
||||||
descriptionLabel.setTextFromHtml(instance.description)
|
descriptionTextView.setTextFromHtml(instance.description)
|
||||||
|
|
||||||
if let thumbnail = instance.thumbnail {
|
if let thumbnail = instance.thumbnail {
|
||||||
updateThumbnail(url: thumbnail)
|
updateThumbnail(url: thumbnail)
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
<?xml version="1.0" encoding="UTF-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"/>
|
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||||
<dependencies>
|
<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="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
@ -27,7 +27,7 @@
|
||||||
</constraints>
|
</constraints>
|
||||||
</imageView>
|
</imageView>
|
||||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="QG1-xB-nmt">
|
<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>
|
<subviews>
|
||||||
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" translatesAutoresizingMaskIntoConstraints="NO" id="XtJ-BL-iHb">
|
<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"/>
|
<rect key="frame" x="0.0" y="0.0" width="200" height="26.5"/>
|
||||||
|
@ -51,15 +51,12 @@
|
||||||
</label>
|
</label>
|
||||||
</subviews>
|
</subviews>
|
||||||
</stackView>
|
</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">
|
<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="20.5"/>
|
<rect key="frame" x="0.0" y="26.5" width="200" height="36.5"/>
|
||||||
<accessibility key="accessibilityConfiguration">
|
<color key="textColor" systemColor="labelColor" cocoaTouchSystemColor="darkTextColor"/>
|
||||||
<accessibilityTraits key="traits" staticText="YES" notEnabled="YES"/>
|
|
||||||
</accessibility>
|
|
||||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||||
<nil key="textColor"/>
|
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
|
||||||
<nil key="highlightedColor"/>
|
</textView>
|
||||||
</label>
|
|
||||||
</subviews>
|
</subviews>
|
||||||
</stackView>
|
</stackView>
|
||||||
</subviews>
|
</subviews>
|
||||||
|
@ -75,7 +72,7 @@
|
||||||
<viewLayoutGuide key="safeArea" id="njF-e1-oar"/>
|
<viewLayoutGuide key="safeArea" id="njF-e1-oar"/>
|
||||||
<connections>
|
<connections>
|
||||||
<outlet property="adultLabel" destination="ekk-aL-7Pq" id="vzP-Gm-QF7"/>
|
<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="domainLabel" destination="SjP-Nk-sSH" id="QPQ-n6-yoK"/>
|
||||||
<outlet property="thumbnailImageView" destination="e2C-wt-pkK" id="KeP-Xf-0Tn"/>
|
<outlet property="thumbnailImageView" destination="e2C-wt-pkK" id="KeP-Xf-0Tn"/>
|
||||||
</connections>
|
</connections>
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,8 +1,8 @@
|
||||||
<?xml version="1.0" encoding="UTF-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"/>
|
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||||
<dependencies>
|
<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="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
|
@ -10,7 +10,7 @@ import UIKit
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
|
|
||||||
protocol ProfileHeaderTableViewCellDelegate: TuskerNavigationDelegate {
|
protocol ProfileHeaderTableViewCellDelegate: TuskerNavigationDelegate {
|
||||||
func showMoreOptions()
|
func showMoreOptions(cell: ProfileHeaderTableViewCell)
|
||||||
}
|
}
|
||||||
|
|
||||||
class ProfileHeaderTableViewCell: UITableViewCell {
|
class ProfileHeaderTableViewCell: UITableViewCell {
|
||||||
|
@ -24,7 +24,7 @@ class ProfileHeaderTableViewCell: UITableViewCell {
|
||||||
@IBOutlet weak var displayNameLabel: UILabel!
|
@IBOutlet weak var displayNameLabel: UILabel!
|
||||||
@IBOutlet weak var usernameLabel: UILabel!
|
@IBOutlet weak var usernameLabel: UILabel!
|
||||||
@IBOutlet weak var followsYouLabel: UILabel!
|
@IBOutlet weak var followsYouLabel: UILabel!
|
||||||
@IBOutlet weak var noteLabel: StatusContentLabel!
|
@IBOutlet weak var noteTextView: StatusContentTextView!
|
||||||
@IBOutlet weak var fieldsStackView: UIStackView!
|
@IBOutlet weak var fieldsStackView: UIStackView!
|
||||||
@IBOutlet weak var fieldNamesStackView: UIStackView!
|
@IBOutlet weak var fieldNamesStackView: UIStackView!
|
||||||
@IBOutlet weak var fieldValuesStack: UIStackView!
|
@IBOutlet weak var fieldValuesStack: UIStackView!
|
||||||
|
@ -79,9 +79,9 @@ class ProfileHeaderTableViewCell: UITableViewCell {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
noteLabel.navigationDelegate = delegate
|
noteTextView.navigationDelegate = delegate
|
||||||
noteLabel.setTextFromHtml(account.note)
|
noteTextView.setTextFromHtml(account.note)
|
||||||
noteLabel.setEmojis(account.emojis)
|
noteTextView.setEmojis(account.emojis)
|
||||||
|
|
||||||
if accountID != mastodonController.account.id {
|
if accountID != mastodonController.account.id {
|
||||||
// don't show relationship label for the user's own account
|
// don't show relationship label for the user's own account
|
||||||
|
@ -104,16 +104,18 @@ class ProfileHeaderTableViewCell: UITableViewCell {
|
||||||
nameLabel.text = field.name
|
nameLabel.text = field.name
|
||||||
nameLabel.font = .boldSystemFont(ofSize: 17)
|
nameLabel.font = .boldSystemFont(ofSize: 17)
|
||||||
nameLabel.textAlignment = .right
|
nameLabel.textAlignment = .right
|
||||||
|
nameLabel.numberOfLines = 0
|
||||||
fieldNamesStackView.addArrangedSubview(nameLabel)
|
fieldNamesStackView.addArrangedSubview(nameLabel)
|
||||||
|
|
||||||
let valueLabel = ContentLabel()
|
let valueTextView = ContentTextView()
|
||||||
valueLabel.setTextFromHtml(field.value)
|
valueTextView.isSelectable = false
|
||||||
valueLabel.setEmojis(account.emojis)
|
valueTextView.font = .systemFont(ofSize: 17)
|
||||||
valueLabel.font = .systemFont(ofSize: 17)
|
valueTextView.setTextFromHtml(field.value)
|
||||||
valueLabel.textAlignment = .left
|
valueTextView.setEmojis(account.emojis)
|
||||||
valueLabel.awakeFromNib() // TODO: this shouldn't be necessary
|
valueTextView.textAlignment = .left
|
||||||
valueLabel.navigationDelegate = delegate
|
valueTextView.awakeFromNib()
|
||||||
fieldValuesStack.addArrangedSubview(valueLabel)
|
valueTextView.navigationDelegate = delegate
|
||||||
|
fieldValuesStack.addArrangedSubview(valueTextView)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
fieldsStackView.isHidden = true
|
fieldsStackView.isHidden = true
|
||||||
|
@ -138,7 +140,7 @@ class ProfileHeaderTableViewCell: UITableViewCell {
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func morePressed() {
|
@objc func morePressed() {
|
||||||
delegate?.showMoreOptions()
|
delegate?.showMoreOptions(cell: self)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func avatarPressed() {
|
@objc func avatarPressed() {
|
||||||
|
@ -151,27 +153,27 @@ class ProfileHeaderTableViewCell: UITableViewCell {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ProfileHeaderTableViewCell: MenuPreviewProvider {
|
//extension ProfileHeaderTableViewCell: MenuPreviewProvider {
|
||||||
var navigationDelegate: TuskerNavigationDelegate? { return delegate }
|
// var navigationDelegate: TuskerNavigationDelegate? { return delegate }
|
||||||
func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? {
|
// func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? {
|
||||||
let noteLabelPoint = noteLabel.convert(location, from: self)
|
// let noteLabelPoint = noteLabel.convert(location, from: self)
|
||||||
if noteLabel.bounds.contains(noteLabelPoint),
|
// if noteLabel.bounds.contains(noteLabelPoint),
|
||||||
let link = noteLabel.getLink(atPoint: noteLabelPoint) {
|
// let link = noteLabel.getLink(atPoint: noteLabelPoint) {
|
||||||
return (
|
// return (
|
||||||
content: { self.noteLabel.getViewController(forLink: link.url, inRange: link.range) },
|
// content: { self.noteLabel.getViewController(forLink: link.url, inRange: link.range) },
|
||||||
actions: {
|
// actions: {
|
||||||
let text = (self.noteLabel.text! as NSString).substring(with: link.range)
|
// let text = (self.noteLabel.text! as NSString).substring(with: link.range)
|
||||||
if let mention = self.noteLabel.getMention(for: link.url, text: text) {
|
// if let mention = self.noteLabel.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.noteLabel.getHashtag(for: link.url, text: text) {
|
// } else if let hashtag = self.noteLabel.getHashtag(for: link.url, text: text) {
|
||||||
return self.actionsForHashtag(hashtag)
|
// return self.actionsForHashtag(hashtag, sourceView: self)
|
||||||
} else {
|
// } else {
|
||||||
return self.actionsForURL(link.url)
|
// return self.actionsForURL(link.url, sourceView: self)
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
)
|
// )
|
||||||
} else {
|
// } else {
|
||||||
return nil
|
// return nil
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
//}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
<?xml version="1.0" encoding="UTF-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"/>
|
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||||
<dependencies>
|
<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="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
</dependencies>
|
</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"/>
|
<color key="textColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
<nil key="highlightedColor"/>
|
<nil key="highlightedColor"/>
|
||||||
</label>
|
</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">
|
<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="37" height="12"/>
|
<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"/>
|
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||||
<nil key="textColor"/>
|
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
|
||||||
<nil key="highlightedColor"/>
|
</textView>
|
||||||
</label>
|
|
||||||
</subviews>
|
</subviews>
|
||||||
</stackView>
|
</stackView>
|
||||||
<stackView opaque="NO" contentMode="scaleToFill" distribution="fillProportionally" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="sHU-GU-klv">
|
<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="followsYouLabel" destination="a32-1a-xXZ" id="phY-0L-NnN"/>
|
||||||
<outlet property="headerImageView" destination="Fw7-OL-iy5" id="6sv-E5-D73"/>
|
<outlet property="headerImageView" destination="Fw7-OL-iy5" id="6sv-E5-D73"/>
|
||||||
<outlet property="moreButtonVisualEffectView" destination="mQY-XN-PfZ" id="t7l-wg-nj0"/>
|
<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"/>
|
<outlet property="usernameLabel" destination="MIj-OR-NOR" id="e1I-N7-rKx"/>
|
||||||
</connections>
|
</connections>
|
||||||
<point key="canvasLocation" x="40.799999999999997" y="110.64467766116942"/>
|
<point key="canvasLocation" x="40.799999999999997" y="110.64467766116942"/>
|
||||||
|
|
|
@ -18,7 +18,7 @@ class BaseStatusTableViewCell: UITableViewCell {
|
||||||
|
|
||||||
var delegate: StatusTableViewCellDelegate? {
|
var delegate: StatusTableViewCellDelegate? {
|
||||||
didSet {
|
didSet {
|
||||||
contentLabel.navigationDelegate = delegate
|
contentTextView.navigationDelegate = delegate
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var overrideMastodonController: MastodonController?
|
var overrideMastodonController: MastodonController?
|
||||||
|
@ -29,7 +29,7 @@ class BaseStatusTableViewCell: UITableViewCell {
|
||||||
@IBOutlet weak var usernameLabel: UILabel!
|
@IBOutlet weak var usernameLabel: UILabel!
|
||||||
@IBOutlet weak var contentWarningLabel: UILabel!
|
@IBOutlet weak var contentWarningLabel: UILabel!
|
||||||
@IBOutlet weak var collapseButton: UIButton!
|
@IBOutlet weak var collapseButton: UIButton!
|
||||||
@IBOutlet weak var contentLabel: StatusContentLabel!
|
@IBOutlet weak var contentTextView: StatusContentTextView!
|
||||||
@IBOutlet weak var attachmentsView: AttachmentsContainerView!
|
@IBOutlet weak var attachmentsView: AttachmentsContainerView!
|
||||||
@IBOutlet weak var replyButton: UIButton!
|
@IBOutlet weak var replyButton: UIButton!
|
||||||
@IBOutlet weak var favoriteButton: UIButton!
|
@IBOutlet weak var favoriteButton: UIButton!
|
||||||
|
@ -91,7 +91,7 @@ class BaseStatusTableViewCell: UITableViewCell {
|
||||||
collapseButton.layer.masksToBounds = true
|
collapseButton.layer.masksToBounds = true
|
||||||
collapseButton.layer.cornerRadius = 5
|
collapseButton.layer.cornerRadius = 5
|
||||||
|
|
||||||
accessibilityElements = [displayNameLabel!, contentWarningLabel!, collapseButton!, contentLabel!, attachmentsView!]
|
accessibilityElements = [displayNameLabel!, contentWarningLabel!, collapseButton!, contentTextView!, attachmentsView!]
|
||||||
attachmentsView.isAccessibilityElement = true
|
attachmentsView.isAccessibilityElement = true
|
||||||
|
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(updateUIForPreferences), name: .preferencesChanged, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(updateUIForPreferences), name: .preferencesChanged, object: nil)
|
||||||
|
@ -134,7 +134,7 @@ class BaseStatusTableViewCell: UITableViewCell {
|
||||||
|
|
||||||
updateStatusState(status: status)
|
updateStatusState(status: status)
|
||||||
|
|
||||||
contentLabel.statusID = statusID
|
contentTextView.statusID = statusID
|
||||||
|
|
||||||
contentWarningLabel.text = status.spoilerText
|
contentWarningLabel.text = status.spoilerText
|
||||||
contentWarningLabel.isHidden = status.spoilerText.isEmpty
|
contentWarningLabel.isHidden = status.spoilerText.isEmpty
|
||||||
|
@ -143,7 +143,7 @@ class BaseStatusTableViewCell: UITableViewCell {
|
||||||
collapsible = !status.spoilerText.isEmpty
|
collapsible = !status.spoilerText.isEmpty
|
||||||
var shouldCollapse = collapsible
|
var shouldCollapse = collapsible
|
||||||
if !shouldCollapse,
|
if !shouldCollapse,
|
||||||
let text = contentLabel.text,
|
let text = contentTextView.text,
|
||||||
text.count > 500 {
|
text.count > 500 {
|
||||||
collapsible = true
|
collapsible = true
|
||||||
shouldCollapse = true
|
shouldCollapse = true
|
||||||
|
@ -215,7 +215,7 @@ class BaseStatusTableViewCell: UITableViewCell {
|
||||||
func setCollapsed(_ collapsed: Bool, animated: Bool) {
|
func setCollapsed(_ collapsed: Bool, animated: Bool) {
|
||||||
self.collapsed = collapsed
|
self.collapsed = collapsed
|
||||||
|
|
||||||
contentLabel.isHidden = collapsed
|
contentTextView.isHidden = collapsed
|
||||||
attachmentsView.isHidden = attachmentsView.attachments.count == 0 || collapsed
|
attachmentsView.isHidden = attachmentsView.attachments.count == 0 || collapsed
|
||||||
|
|
||||||
let buttonImage = UIImage(systemName: collapsed ? "chevron.down" : "chevron.up")!
|
let buttonImage = UIImage(systemName: collapsed ? "chevron.down" : "chevron.up")!
|
||||||
|
@ -301,7 +301,7 @@ class BaseStatusTableViewCell: UITableViewCell {
|
||||||
}
|
}
|
||||||
|
|
||||||
@IBAction func morePressed() {
|
@IBAction func morePressed() {
|
||||||
delegate?.showMoreOptions(forStatus: statusID)
|
delegate?.showMoreOptions(forStatus: statusID, sourceView: moreButton)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func accountPressed() {
|
@objc func accountPressed() {
|
||||||
|
@ -327,11 +327,10 @@ extension BaseStatusTableViewCell: MenuPreviewProvider {
|
||||||
func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? {
|
func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? {
|
||||||
guard let mastodonController = mastodonController else { return nil }
|
guard let mastodonController = mastodonController else { return nil }
|
||||||
if avatarImageView.frame.contains(location) {
|
if avatarImageView.frame.contains(location) {
|
||||||
return (content: {
|
return (
|
||||||
ProfileTableViewController(accountID: self.accountID, mastodonController: mastodonController)
|
content: { ProfileTableViewController(accountID: self.accountID, mastodonController: mastodonController) },
|
||||||
}, actions: {
|
actions: { self.actionsForProfile(accountID: self.accountID, sourceView: self.avatarImageView) }
|
||||||
self.actionsForProfile(accountID: self.accountID)
|
)
|
||||||
})
|
|
||||||
} else if attachmentsView.frame.contains(location) {
|
} else if attachmentsView.frame.contains(location) {
|
||||||
let attachmentsViewLocation = attachmentsView.convert(location, from: self)
|
let attachmentsViewLocation = attachmentsView.convert(location, from: self)
|
||||||
if let attachmentView = attachmentsView.attachmentViews.allObjects.first(where: { $0.frame.contains(attachmentsViewLocation) }),
|
if let attachmentView = attachmentsView.attachmentViews.allObjects.first(where: { $0.frame.contains(attachmentsViewLocation) }),
|
||||||
|
@ -339,22 +338,22 @@ extension BaseStatusTableViewCell: MenuPreviewProvider {
|
||||||
let description = attachmentView.attachment.description
|
let description = attachmentView.attachment.description
|
||||||
return (content: { self.delegate?.largeImage(image, description: description, sourceView: attachmentView) }, actions: { [] })
|
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)) {
|
let link = contentLabel.getLink(atPoint: contentLabel.convert(location, from: self)) {
|
||||||
return (
|
return (
|
||||||
content: { self.contentLabel.getViewController(forLink: link.url, inRange: link.range) },
|
content: { self.contentLabel.getViewController(forLink: link.url, inRange: link.range) },
|
||||||
actions: {
|
actions: {
|
||||||
let text = (self.contentLabel.text! as NSString).substring(with: link.range)
|
let text = (self.contentLabel.text! as NSString).substring(with: link.range)
|
||||||
if let mention = self.contentLabel.getMention(for: link.url, text: text) {
|
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) {
|
} else if let hashtag = self.contentLabel.getHashtag(for: link.url, text: text) {
|
||||||
return self.actionsForHashtag(hashtag)
|
return self.actionsForHashtag(hashtag, sourceView: self)
|
||||||
} else {
|
} else {
|
||||||
return self.actionsForURL(link.url)
|
return self.actionsForURL(link.url, sourceView: self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}*/
|
||||||
return self.getStatusCellPreviewProviders(for: location, sourceViewController: sourceViewController)
|
return self.getStatusCellPreviewProviders(for: location, sourceViewController: sourceViewController)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,7 +33,9 @@ class ConversationMainStatusTableViewCell: BaseStatusTableViewCell {
|
||||||
|
|
||||||
profileAccessibilityElement = UIAccessibilityElement(accessibilityContainer: self)
|
profileAccessibilityElement = UIAccessibilityElement(accessibilityContainer: self)
|
||||||
profileAccessibilityElement.accessibilityFrameInContainerSpace = profileDetailContainerView.convert(profileDetailContainerView.frame, to: 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) {
|
override func updateUI(statusID: String, state: StatusState) {
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
<?xml version="1.0" encoding="UTF-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"/>
|
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||||
<dependencies>
|
<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="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
@ -75,12 +75,13 @@
|
||||||
<action selector="collapseButtonPressed" destination="iN0-l3-epB" eventType="touchUpInside" id="2Jy-L1-lN6"/>
|
<action selector="collapseButtonPressed" destination="iN0-l3-epB" eventType="touchUpInside" id="2Jy-L1-lN6"/>
|
||||||
</connections>
|
</connections>
|
||||||
</button>
|
</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">
|
<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="70" height="55.5"/>
|
<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"/>
|
<fontDescription key="fontDescription" type="system" pointSize="20"/>
|
||||||
<nil key="textColor"/>
|
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
|
||||||
<nil key="highlightedColor"/>
|
</textView>
|
||||||
</label>
|
|
||||||
<view hidden="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="IF9-9U-Gk0" customClass="AttachmentsContainerView" customModule="Tusker" customModuleProvider="target">
|
<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"/>
|
<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"/>
|
<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>
|
</subviews>
|
||||||
<constraints>
|
<constraints>
|
||||||
<constraint firstItem="Cnd-Fj-B7l" firstAttribute="width" secondItem="GuG-Qd-B8I" secondAttribute="width" id="2hS-RG-81T"/>
|
<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="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="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"/>
|
<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="attachmentsView" destination="IF9-9U-Gk0" id="Oxw-sJ-MJE"/>
|
||||||
<outlet property="avatarImageView" destination="mB9-HO-1vf" id="0R0-rt-Osh"/>
|
<outlet property="avatarImageView" destination="mB9-HO-1vf" id="0R0-rt-Osh"/>
|
||||||
<outlet property="collapseButton" destination="8r8-O8-Agh" id="0es-Hi-bpt"/>
|
<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="contentWarningLabel" destination="cwQ-mR-L1b" id="5sm-PC-FIN"/>
|
||||||
<outlet property="displayNameLabel" destination="lZY-2e-17d" id="7og-23-eHy"/>
|
<outlet property="displayNameLabel" destination="lZY-2e-17d" id="7og-23-eHy"/>
|
||||||
<outlet property="favoriteAndReblogCountStackView" destination="HZv-qj-gi6" id="jC9-cA-dXg"/>
|
<outlet property="favoriteAndReblogCountStackView" destination="HZv-qj-gi6" id="jC9-cA-dXg"/>
|
||||||
|
|
|
@ -134,7 +134,7 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
|
||||||
guard let mastodonController = mastodonController else { return nil }
|
guard let mastodonController = mastodonController else { return nil }
|
||||||
return (
|
return (
|
||||||
content: { ConversationTableViewController(for: self.statusID, state: self.statusState.copy(), mastodonController: mastodonController) },
|
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
|
reply.backgroundColor = tintColor
|
||||||
let more = UIContextualAction(style: .normal, title: "More") { (action, view, completion) in
|
let more = UIContextualAction(style: .normal, title: "More") { (action, view, completion) in
|
||||||
completion(true)
|
completion(true)
|
||||||
self.delegate?.showMoreOptions(forStatus: self.statusID)
|
self.delegate?.showMoreOptions(forStatus: self.statusID, sourceView: self)
|
||||||
}
|
}
|
||||||
more.image = UIImage(systemName: "ellipsis")
|
more.image = UIImage(systemName: "ellipsis")
|
||||||
more.backgroundColor = .gray
|
more.backgroundColor = .gray
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
<?xml version="1.0" encoding="UTF-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"/>
|
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||||
<dependencies>
|
<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="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
@ -23,7 +23,7 @@
|
||||||
<nil key="highlightedColor"/>
|
<nil key="highlightedColor"/>
|
||||||
</label>
|
</label>
|
||||||
<view contentMode="scaleToFill" verticalHuggingPriority="249" translatesAutoresizingMaskIntoConstraints="NO" id="ve3-Y1-NQH">
|
<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>
|
<subviews>
|
||||||
<imageView contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="QMP-j2-HLn">
|
<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"/>
|
<rect key="frame" x="0.0" y="0.0" width="50" height="50"/>
|
||||||
|
@ -37,7 +37,7 @@
|
||||||
</constraints>
|
</constraints>
|
||||||
</imageView>
|
</imageView>
|
||||||
<stackView opaque="NO" contentMode="scaleToFill" verticalCompressionResistancePriority="751" axis="vertical" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="gIY-Wp-RSk">
|
<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>
|
<subviews>
|
||||||
<stackView opaque="NO" contentMode="scaleToFill" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="3Sm-P0-ySf">
|
<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"/>
|
<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"/>
|
<action selector="collapseButtonPressed" destination="iN0-l3-epB" eventType="touchUpInside" id="JaH-xX-UOD"/>
|
||||||
</connections>
|
</connections>
|
||||||
</button>
|
</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">
|
<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="20.5"/>
|
<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"/>
|
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||||
<nil key="textColor"/>
|
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
|
||||||
<nil key="highlightedColor"/>
|
</textView>
|
||||||
</label>
|
|
||||||
</subviews>
|
</subviews>
|
||||||
</stackView>
|
</stackView>
|
||||||
</subviews>
|
</subviews>
|
||||||
|
@ -125,17 +126,17 @@
|
||||||
</constraints>
|
</constraints>
|
||||||
</view>
|
</view>
|
||||||
<view hidden="YES" contentMode="scaleToFill" verticalCompressionResistancePriority="1" translatesAutoresizingMaskIntoConstraints="NO" id="nbq-yr-2mA" customClass="AttachmentsContainerView" customModule="Tusker" customModuleProvider="target">
|
<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"/>
|
<color key="backgroundColor" systemColor="secondarySystemBackgroundColor" red="0.94901960780000005" green="0.94901960780000005" blue="0.96862745100000003" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
<constraints>
|
<constraints>
|
||||||
<constraint firstAttribute="height" priority="999" constant="200" id="J42-49-2MU"/>
|
<constraint firstAttribute="height" priority="999" constant="200" id="J42-49-2MU"/>
|
||||||
</constraints>
|
</constraints>
|
||||||
</view>
|
</view>
|
||||||
<stackView opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" distribution="equalSpacing" alignment="bottom" translatesAutoresizingMaskIntoConstraints="NO" id="Zlb-yt-NTw">
|
<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>
|
<subviews>
|
||||||
<button opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" contentHorizontalAlignment="leading" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="rKF-yF-KIa">
|
<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"/>
|
<accessibility key="accessibilityConfiguration" label="Reply"/>
|
||||||
<state key="normal" image="arrowshape.turn.up.left.fill" catalog="system"/>
|
<state key="normal" image="arrowshape.turn.up.left.fill" catalog="system"/>
|
||||||
<connections>
|
<connections>
|
||||||
|
@ -143,7 +144,7 @@
|
||||||
</connections>
|
</connections>
|
||||||
</button>
|
</button>
|
||||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="x0t-TR-jJ4">
|
<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"/>
|
<accessibility key="accessibilityConfiguration" label="Favorite"/>
|
||||||
<state key="normal" image="star.fill" catalog="system"/>
|
<state key="normal" image="star.fill" catalog="system"/>
|
||||||
<connections>
|
<connections>
|
||||||
|
@ -151,7 +152,7 @@
|
||||||
</connections>
|
</connections>
|
||||||
</button>
|
</button>
|
||||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="6tW-z8-Qh9">
|
<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"/>
|
<accessibility key="accessibilityConfiguration" label="Reblog"/>
|
||||||
<state key="normal" image="repeat" catalog="system"/>
|
<state key="normal" image="repeat" catalog="system"/>
|
||||||
<connections>
|
<connections>
|
||||||
|
@ -159,7 +160,7 @@
|
||||||
</connections>
|
</connections>
|
||||||
</button>
|
</button>
|
||||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="982-J4-NGl">
|
<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"/>
|
<accessibility key="accessibilityConfiguration" label="More Actions"/>
|
||||||
<state key="normal" image="ellipsis" catalog="system"/>
|
<state key="normal" image="ellipsis" catalog="system"/>
|
||||||
<connections>
|
<connections>
|
||||||
|
@ -189,7 +190,7 @@
|
||||||
<outlet property="attachmentsView" destination="nbq-yr-2mA" id="SVm-zl-mPb"/>
|
<outlet property="attachmentsView" destination="nbq-yr-2mA" id="SVm-zl-mPb"/>
|
||||||
<outlet property="avatarImageView" destination="QMP-j2-HLn" id="xfS-v8-Gzu"/>
|
<outlet property="avatarImageView" destination="QMP-j2-HLn" id="xfS-v8-Gzu"/>
|
||||||
<outlet property="collapseButton" destination="O0E-Vf-XYR" id="nWd-gg-st8"/>
|
<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="contentWarningLabel" destination="inI-Og-YiU" id="C7a-eK-qcx"/>
|
||||||
<outlet property="displayNameLabel" destination="gll-xe-FSr" id="vVS-WM-Wqx"/>
|
<outlet property="displayNameLabel" destination="gll-xe-FSr" id="vVS-WM-Wqx"/>
|
||||||
<outlet property="favoriteButton" destination="x0t-TR-jJ4" id="guV-yz-Lm6"/>
|
<outlet property="favoriteButton" destination="x0t-TR-jJ4" id="guV-yz-Lm6"/>
|
||||||
|
|
|
@ -1,22 +1,23 @@
|
||||||
//
|
//
|
||||||
// StatusContentLabel.swift
|
// StatusContentTextView.swift
|
||||||
// Tusker
|
// Tusker
|
||||||
//
|
//
|
||||||
// Created by Shadowfacts on 10/1/18.
|
// Created by Shadowfacts on 1/18/20.
|
||||||
// Copyright © 2018 Shadowfacts. All rights reserved.
|
// Copyright © 2020 Shadowfacts. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
|
|
||||||
class StatusContentLabel: ContentLabel {
|
class StatusContentTextView: ContentTextView {
|
||||||
|
|
||||||
var mastodonController: MastodonController? { navigationDelegate?.apiController }
|
|
||||||
|
|
||||||
var statusID: String? {
|
var statusID: String? {
|
||||||
didSet {
|
didSet {
|
||||||
guard let statusID = statusID, let mastodonController = mastodonController else { return }
|
guard let statusID = statusID else { return }
|
||||||
guard let status = mastodonController.cache.status(for: statusID) else { fatalError("Can't set StatusContentLabel text without cached status \(statusID)") }
|
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)
|
setTextFromHtml(status.content)
|
||||||
setEmojis(status.emojis)
|
setEmojis(status.emojis)
|
||||||
}
|
}
|
Loading…
Reference in New Issue