Compare commits

...

7 Commits

41 changed files with 1116 additions and 500 deletions

View File

@ -39,3 +39,9 @@ extension Emoji: CustomDebugStringConvertible {
return ":\(shortcode):" return ":\(shortcode):"
} }
} }
extension Emoji: Equatable {
public static func ==(lhs: Emoji, rhs: Emoji) -> Bool {
return lhs.shortcode == rhs.shortcode && lhs.url == rhs.url
}
}

View File

@ -19,8 +19,6 @@
04DACE8E212CC7CC009840C4 /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DACE8D212CC7CC009840C4 /* ImageCache.swift */; }; 04DACE8E212CC7CC009840C4 /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DACE8D212CC7CC009840C4 /* ImageCache.swift */; };
04ED00B121481ED800567C53 /* SteppedProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04ED00B021481ED800567C53 /* SteppedProgressView.swift */; }; 04ED00B121481ED800567C53 /* SteppedProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04ED00B021481ED800567C53 /* SteppedProgressView.swift */; };
D6093F9B25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6093F9A25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift */; }; D6093F9B25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6093F9A25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift */; };
D6093FB025BE0B01004811E6 /* TrendingHashtagTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6093FAE25BE0B01004811E6 /* TrendingHashtagTableViewCell.swift */; };
D6093FB125BE0B01004811E6 /* TrendingHashtagTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6093FAF25BE0B01004811E6 /* TrendingHashtagTableViewCell.xib */; };
D6093FB725BE0CF3004811E6 /* TrendHistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6093FB625BE0CF3004811E6 /* TrendHistoryView.swift */; }; D6093FB725BE0CF3004811E6 /* TrendHistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6093FB625BE0CF3004811E6 /* TrendHistoryView.swift */; };
D60CFFDB24A290BA00D00083 /* SwiftSoup in Frameworks */ = {isa = PBXBuildFile; productRef = D60CFFDA24A290BA00D00083 /* SwiftSoup */; }; D60CFFDB24A290BA00D00083 /* SwiftSoup in Frameworks */ = {isa = PBXBuildFile; productRef = D60CFFDA24A290BA00D00083 /* SwiftSoup */; };
D60D2B8223844C71001B87A3 /* BaseStatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60D2B8123844C71001B87A3 /* BaseStatusTableViewCell.swift */; }; D60D2B8223844C71001B87A3 /* BaseStatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60D2B8123844C71001B87A3 /* BaseStatusTableViewCell.swift */; };
@ -221,13 +219,11 @@
D6AEBB4523216AF800E5038B /* FollowAccountActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6AEBB4423216AF800E5038B /* FollowAccountActivity.swift */; }; D6AEBB4523216AF800E5038B /* FollowAccountActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6AEBB4423216AF800E5038B /* FollowAccountActivity.swift */; };
D6AEBB4823216B1D00E5038B /* AccountActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6AEBB4723216B1D00E5038B /* AccountActivity.swift */; }; D6AEBB4823216B1D00E5038B /* AccountActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6AEBB4723216B1D00E5038B /* AccountActivity.swift */; };
D6AEBB4A23216F0400E5038B /* UnfollowAccountActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6AEBB4923216F0400E5038B /* UnfollowAccountActivity.swift */; }; D6AEBB4A23216F0400E5038B /* UnfollowAccountActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6AEBB4923216F0400E5038B /* UnfollowAccountActivity.swift */; };
D6B0539F23BD2BA300A066FA /* SheetController in Frameworks */ = {isa = PBXBuildFile; productRef = D6B0539E23BD2BA300A066FA /* SheetController */; };
D6B053A223BD2C0600A066FA /* AssetPickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B053A123BD2C0600A066FA /* AssetPickerViewController.swift */; }; D6B053A223BD2C0600A066FA /* AssetPickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B053A123BD2C0600A066FA /* AssetPickerViewController.swift */; };
D6B053A423BD2C8100A066FA /* AssetCollectionsListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B053A323BD2C8100A066FA /* AssetCollectionsListViewController.swift */; }; D6B053A423BD2C8100A066FA /* AssetCollectionsListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B053A323BD2C8100A066FA /* AssetCollectionsListViewController.swift */; };
D6B053A623BD2D0C00A066FA /* AssetCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B053A523BD2D0C00A066FA /* AssetCollectionViewController.swift */; }; D6B053A623BD2D0C00A066FA /* AssetCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B053A523BD2D0C00A066FA /* AssetCollectionViewController.swift */; };
D6B053AB23BD2F1400A066FA /* AssetCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B053A923BD2F1400A066FA /* AssetCollectionViewCell.swift */; }; D6B053AB23BD2F1400A066FA /* AssetCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B053A923BD2F1400A066FA /* AssetCollectionViewCell.swift */; };
D6B053AC23BD2F1400A066FA /* AssetCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6B053AA23BD2F1400A066FA /* AssetCollectionViewCell.xib */; }; D6B053AC23BD2F1400A066FA /* AssetCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6B053AA23BD2F1400A066FA /* AssetCollectionViewCell.xib */; };
D6B053AE23BD322B00A066FA /* AssetPickerSheetContainerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B053AD23BD322B00A066FA /* AssetPickerSheetContainerViewController.swift */; };
D6B17255254F88B800128392 /* OppositeCollapseKeywordsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B17254254F88B800128392 /* OppositeCollapseKeywordsView.swift */; }; D6B17255254F88B800128392 /* OppositeCollapseKeywordsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B17254254F88B800128392 /* OppositeCollapseKeywordsView.swift */; };
D6B22A0F2560D52D004D82EF /* TabbedPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B22A0E2560D52D004D82EF /* TabbedPageViewController.swift */; }; D6B22A0F2560D52D004D82EF /* TabbedPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B22A0E2560D52D004D82EF /* TabbedPageViewController.swift */; };
D6B30E09254BAF63009CAEE5 /* ImageGrayscalifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B30E08254BAF63009CAEE5 /* ImageGrayscalifier.swift */; }; D6B30E09254BAF63009CAEE5 /* ImageGrayscalifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B30E08254BAF63009CAEE5 /* ImageGrayscalifier.swift */; };
@ -282,7 +278,6 @@
D6DFC69E242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DFC69D242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift */; }; D6DFC69E242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DFC69D242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift */; };
D6DFC6A0242C4CCC00ACC392 /* WeakArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DFC69F242C4CCC00ACC392 /* WeakArray.swift */; }; D6DFC6A0242C4CCC00ACC392 /* WeakArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DFC69F242C4CCC00ACC392 /* WeakArray.swift */; };
D6E0DC8E216EDF1E00369478 /* Previewing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E0DC8D216EDF1E00369478 /* Previewing.swift */; }; D6E0DC8E216EDF1E00369478 /* Previewing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E0DC8D216EDF1E00369478 /* Previewing.swift */; };
D6E1EEF4285443EF00D20549 /* UIAction+Subtitle.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E1EEF3285443EF00D20549 /* UIAction+Subtitle.swift */; };
D6E343AB265AAD6B00C4AA01 /* Media.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D6E343AA265AAD6B00C4AA01 /* Media.xcassets */; }; D6E343AB265AAD6B00C4AA01 /* Media.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D6E343AA265AAD6B00C4AA01 /* Media.xcassets */; };
D6E343AD265AAD6B00C4AA01 /* ActionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E343AC265AAD6B00C4AA01 /* ActionViewController.swift */; }; D6E343AD265AAD6B00C4AA01 /* ActionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E343AC265AAD6B00C4AA01 /* ActionViewController.swift */; };
D6E343B0265AAD6B00C4AA01 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D6E343AE265AAD6B00C4AA01 /* MainInterface.storyboard */; }; D6E343B0265AAD6B00C4AA01 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D6E343AE265AAD6B00C4AA01 /* MainInterface.storyboard */; };
@ -293,6 +288,10 @@
D6E426AD25334DA500C02E1C /* FuzzyMatcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E426AC25334DA500C02E1C /* FuzzyMatcherTests.swift */; }; D6E426AD25334DA500C02E1C /* FuzzyMatcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E426AC25334DA500C02E1C /* FuzzyMatcherTests.swift */; };
D6E426B325337C7000C02E1C /* CustomEmojiImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E426B225337C7000C02E1C /* CustomEmojiImageView.swift */; }; D6E426B325337C7000C02E1C /* CustomEmojiImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E426B225337C7000C02E1C /* CustomEmojiImageView.swift */; };
D6E57FA325C26FAB00341037 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = D6E57FA525C26FAB00341037 /* Localizable.stringsdict */; }; D6E57FA325C26FAB00341037 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = D6E57FA525C26FAB00341037 /* Localizable.stringsdict */; };
D6E77D09286D25FA00D8B732 /* TrendingHashtagCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E77D08286D25FA00D8B732 /* TrendingHashtagCollectionViewCell.swift */; };
D6E77D0B286D426E00D8B732 /* TrendingLinkCardCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E77D0A286D426E00D8B732 /* TrendingLinkCardCollectionViewCell.swift */; };
D6E77D0D286E6B7300D8B732 /* TrendingLinkCardCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6E77D0C286E6B7300D8B732 /* TrendingLinkCardCollectionViewCell.xib */; };
D6E77D0F286F773900D8B732 /* SplitNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E77D0E286F773900D8B732 /* SplitNavigationController.swift */; };
D6E9CDA8281A427800BBC98E /* PostService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E9CDA7281A427800BBC98E /* PostService.swift */; }; D6E9CDA8281A427800BBC98E /* PostService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E9CDA7281A427800BBC98E /* PostService.swift */; };
D6EAE0DB2550CC8A002DB0AC /* FocusableTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EAE0DA2550CC8A002DB0AC /* FocusableTextField.swift */; }; D6EAE0DB2550CC8A002DB0AC /* FocusableTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EAE0DA2550CC8A002DB0AC /* FocusableTextField.swift */; };
D6EBF01523C55C0900AE061B /* UIApplication+Scenes.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EBF01423C55C0900AE061B /* UIApplication+Scenes.swift */; }; D6EBF01523C55C0900AE061B /* UIApplication+Scenes.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EBF01423C55C0900AE061B /* UIApplication+Scenes.swift */; };
@ -367,8 +366,6 @@
04DACE8D212CC7CC009840C4 /* ImageCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCache.swift; sourceTree = "<group>"; }; 04DACE8D212CC7CC009840C4 /* ImageCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCache.swift; sourceTree = "<group>"; };
04ED00B021481ED800567C53 /* SteppedProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SteppedProgressView.swift; sourceTree = "<group>"; }; 04ED00B021481ED800567C53 /* SteppedProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SteppedProgressView.swift; sourceTree = "<group>"; };
D6093F9A25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagSearchResultsViewController.swift; sourceTree = "<group>"; }; D6093F9A25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagSearchResultsViewController.swift; sourceTree = "<group>"; };
D6093FAE25BE0B01004811E6 /* TrendingHashtagTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingHashtagTableViewCell.swift; sourceTree = "<group>"; };
D6093FAF25BE0B01004811E6 /* TrendingHashtagTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TrendingHashtagTableViewCell.xib; sourceTree = "<group>"; };
D6093FB625BE0CF3004811E6 /* TrendHistoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendHistoryView.swift; sourceTree = "<group>"; }; D6093FB625BE0CF3004811E6 /* TrendHistoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendHistoryView.swift; sourceTree = "<group>"; };
D60D2B8123844C71001B87A3 /* BaseStatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseStatusTableViewCell.swift; sourceTree = "<group>"; }; D60D2B8123844C71001B87A3 /* BaseStatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseStatusTableViewCell.swift; sourceTree = "<group>"; };
D60E2F232442372B005F8713 /* StatusMO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusMO.swift; sourceTree = "<group>"; }; D60E2F232442372B005F8713 /* StatusMO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusMO.swift; sourceTree = "<group>"; };
@ -573,7 +570,6 @@
D6B053A523BD2D0C00A066FA /* AssetCollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetCollectionViewController.swift; sourceTree = "<group>"; }; D6B053A523BD2D0C00A066FA /* AssetCollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetCollectionViewController.swift; sourceTree = "<group>"; };
D6B053A923BD2F1400A066FA /* AssetCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetCollectionViewCell.swift; sourceTree = "<group>"; }; D6B053A923BD2F1400A066FA /* AssetCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetCollectionViewCell.swift; sourceTree = "<group>"; };
D6B053AA23BD2F1400A066FA /* AssetCollectionViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AssetCollectionViewCell.xib; sourceTree = "<group>"; }; D6B053AA23BD2F1400A066FA /* AssetCollectionViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AssetCollectionViewCell.xib; sourceTree = "<group>"; };
D6B053AD23BD322B00A066FA /* AssetPickerSheetContainerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetPickerSheetContainerViewController.swift; sourceTree = "<group>"; };
D6B17254254F88B800128392 /* OppositeCollapseKeywordsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OppositeCollapseKeywordsView.swift; sourceTree = "<group>"; }; D6B17254254F88B800128392 /* OppositeCollapseKeywordsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OppositeCollapseKeywordsView.swift; sourceTree = "<group>"; };
D6B22A0E2560D52D004D82EF /* TabbedPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabbedPageViewController.swift; sourceTree = "<group>"; }; D6B22A0E2560D52D004D82EF /* TabbedPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabbedPageViewController.swift; sourceTree = "<group>"; };
D6B30E08254BAF63009CAEE5 /* ImageGrayscalifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageGrayscalifier.swift; sourceTree = "<group>"; }; D6B30E08254BAF63009CAEE5 /* ImageGrayscalifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageGrayscalifier.swift; sourceTree = "<group>"; };
@ -634,7 +630,6 @@
D6DFC69D242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackpadScrollGestureRecognizer.swift; sourceTree = "<group>"; }; D6DFC69D242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackpadScrollGestureRecognizer.swift; sourceTree = "<group>"; };
D6DFC69F242C4CCC00ACC392 /* WeakArray.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeakArray.swift; sourceTree = "<group>"; }; D6DFC69F242C4CCC00ACC392 /* WeakArray.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeakArray.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>"; };
D6E1EEF3285443EF00D20549 /* UIAction+Subtitle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIAction+Subtitle.swift"; sourceTree = "<group>"; };
D6E343A8265AAD6B00C4AA01 /* OpenInTusker.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = OpenInTusker.appex; sourceTree = BUILT_PRODUCTS_DIR; }; D6E343A8265AAD6B00C4AA01 /* OpenInTusker.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = OpenInTusker.appex; sourceTree = BUILT_PRODUCTS_DIR; };
D6E343AA265AAD6B00C4AA01 /* Media.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Media.xcassets; sourceTree = "<group>"; }; D6E343AA265AAD6B00C4AA01 /* Media.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Media.xcassets; sourceTree = "<group>"; };
D6E343AC265AAD6B00C4AA01 /* ActionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionViewController.swift; sourceTree = "<group>"; }; D6E343AC265AAD6B00C4AA01 /* ActionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionViewController.swift; sourceTree = "<group>"; };
@ -648,6 +643,10 @@
D6E426B225337C7000C02E1C /* CustomEmojiImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomEmojiImageView.swift; sourceTree = "<group>"; }; D6E426B225337C7000C02E1C /* CustomEmojiImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomEmojiImageView.swift; sourceTree = "<group>"; };
D6E4885C24A2890C0011C13E /* Tusker.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Tusker.entitlements; sourceTree = "<group>"; }; D6E4885C24A2890C0011C13E /* Tusker.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Tusker.entitlements; sourceTree = "<group>"; };
D6E57FA425C26FAB00341037 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Localizable.stringsdict; sourceTree = "<group>"; }; D6E57FA425C26FAB00341037 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
D6E77D08286D25FA00D8B732 /* TrendingHashtagCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingHashtagCollectionViewCell.swift; sourceTree = "<group>"; };
D6E77D0A286D426E00D8B732 /* TrendingLinkCardCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingLinkCardCollectionViewCell.swift; sourceTree = "<group>"; };
D6E77D0C286E6B7300D8B732 /* TrendingLinkCardCollectionViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TrendingLinkCardCollectionViewCell.xib; sourceTree = "<group>"; };
D6E77D0E286F773900D8B732 /* SplitNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitNavigationController.swift; sourceTree = "<group>"; };
D6E9CDA7281A427800BBC98E /* PostService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostService.swift; sourceTree = "<group>"; }; D6E9CDA7281A427800BBC98E /* PostService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostService.swift; sourceTree = "<group>"; };
D6EAE0DA2550CC8A002DB0AC /* FocusableTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FocusableTextField.swift; sourceTree = "<group>"; }; D6EAE0DA2550CC8A002DB0AC /* FocusableTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FocusableTextField.swift; sourceTree = "<group>"; };
D6EBF01423C55C0900AE061B /* UIApplication+Scenes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIApplication+Scenes.swift"; sourceTree = "<group>"; }; D6EBF01423C55C0900AE061B /* UIApplication+Scenes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIApplication+Scenes.swift"; sourceTree = "<group>"; };
@ -667,7 +666,6 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
D674A50927F9128D00BA03AC /* Pachyderm in Frameworks */, D674A50927F9128D00BA03AC /* Pachyderm in Frameworks */,
D6B0539F23BD2BA300A066FA /* SheetController in Frameworks */,
D69CCBBF249E6EFD000AF167 /* CrashReporter in Frameworks */, D69CCBBF249E6EFD000AF167 /* CrashReporter in Frameworks */,
D60CFFDB24A290BA00D00083 /* SwiftSoup in Frameworks */, D60CFFDB24A290BA00D00083 /* SwiftSoup in Frameworks */,
D6676CA527A8D0020052936B /* WebURLFoundationExtras in Frameworks */, D6676CA527A8D0020052936B /* WebURLFoundationExtras in Frameworks */,
@ -716,8 +714,7 @@
children = ( children = (
D611C2CD232DC61100C86A49 /* HashtagTableViewCell.swift */, D611C2CD232DC61100C86A49 /* HashtagTableViewCell.swift */,
D611C2CE232DC61100C86A49 /* HashtagTableViewCell.xib */, D611C2CE232DC61100C86A49 /* HashtagTableViewCell.xib */,
D6093FAE25BE0B01004811E6 /* TrendingHashtagTableViewCell.swift */, D6E77D08286D25FA00D8B732 /* TrendingHashtagCollectionViewCell.swift */,
D6093FAF25BE0B01004811E6 /* TrendingHashtagTableViewCell.xib */,
); );
path = "Hashtag Cell"; path = "Hashtag Cell";
sourceTree = "<group>"; sourceTree = "<group>";
@ -803,6 +800,8 @@
D693A72725CF282E003A14E2 /* TrendingHashtagsViewController.swift */, D693A72725CF282E003A14E2 /* TrendingHashtagsViewController.swift */,
D6114E1027F899B30080E273 /* TrendingLinksViewController.swift */, D6114E1027F899B30080E273 /* TrendingLinksViewController.swift */,
D6114E1227F89B440080E273 /* TrendingLinkTableViewCell.swift */, D6114E1227F89B440080E273 /* TrendingLinkTableViewCell.swift */,
D6E77D0A286D426E00D8B732 /* TrendingLinkCardCollectionViewCell.swift */,
D6E77D0C286E6B7300D8B732 /* TrendingLinkCardCollectionViewCell.xib */,
D693A72925CF8C1E003A14E2 /* ProfileDirectoryViewController.swift */, D693A72925CF8C1E003A14E2 /* ProfileDirectoryViewController.swift */,
D693A72D25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift */, D693A72D25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift */,
D693A72E25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.xib */, D693A72E25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.xib */,
@ -1123,7 +1122,6 @@
D6620ACD2511A0ED00312CA0 /* StatusStateResolver.swift */, D6620ACD2511A0ED00312CA0 /* StatusStateResolver.swift */,
D69693F32585941A00F4E116 /* UIWindowSceneDelegate+Close.swift */, D69693F32585941A00F4E116 /* UIWindowSceneDelegate+Close.swift */,
D62E9984279CA23900C26176 /* URLSession+Development.swift */, D62E9984279CA23900C26176 /* URLSession+Development.swift */,
D6E1EEF3285443EF00D20549 /* UIAction+Subtitle.swift */,
); );
path = Extensions; path = Extensions;
sourceTree = "<group>"; sourceTree = "<group>";
@ -1242,7 +1240,6 @@
D6B053A523BD2D0C00A066FA /* AssetCollectionViewController.swift */, D6B053A523BD2D0C00A066FA /* AssetCollectionViewController.swift */,
D626493E23C101C500612E6E /* AlbumAssetCollectionViewController.swift */, D626493E23C101C500612E6E /* AlbumAssetCollectionViewController.swift */,
D64BC18523C1253A000D0238 /* AssetPreviewViewController.swift */, D64BC18523C1253A000D0238 /* AssetPreviewViewController.swift */,
D6B053AD23BD322B00A066FA /* AssetPickerSheetContainerViewController.swift */,
); );
path = "Asset Picker"; path = "Asset Picker";
sourceTree = "<group>"; sourceTree = "<group>";
@ -1303,6 +1300,7 @@
D653F410267D1E32004E32B1 /* DiffableTimelineLikeTableViewController.swift */, D653F410267D1E32004E32B1 /* DiffableTimelineLikeTableViewController.swift */,
D625E4812588262A0074BB2B /* DraggableTableViewCell.swift */, D625E4812588262A0074BB2B /* DraggableTableViewCell.swift */,
D693DE5623FE1A6A0061E07D /* EnhancedNavigationViewController.swift */, D693DE5623FE1A6A0061E07D /* EnhancedNavigationViewController.swift */,
D6E77D0E286F773900D8B732 /* SplitNavigationController.swift */,
D6BC8747219738E1006163F1 /* EnhancedTableViewController.swift */, D6BC8747219738E1006163F1 /* EnhancedTableViewController.swift */,
D693DE5823FE24300061E07D /* InteractivePushTransition.swift */, D693DE5823FE24300061E07D /* InteractivePushTransition.swift */,
D6C693FB2162FE6F007D6A6D /* LoadingViewController.swift */, D6C693FB2162FE6F007D6A6D /* LoadingViewController.swift */,
@ -1504,7 +1502,6 @@
); );
name = Tusker; name = Tusker;
packageProductDependencies = ( packageProductDependencies = (
D6B0539E23BD2BA300A066FA /* SheetController */,
D69CCBBE249E6EFD000AF167 /* CrashReporter */, D69CCBBE249E6EFD000AF167 /* CrashReporter */,
D60CFFDA24A290BA00D00083 /* SwiftSoup */, D60CFFDA24A290BA00D00083 /* SwiftSoup */,
D6676CA427A8D0020052936B /* WebURLFoundationExtras */, D6676CA427A8D0020052936B /* WebURLFoundationExtras */,
@ -1614,7 +1611,6 @@
); );
mainGroup = D6D4DDC3212518A000E1C4BB; mainGroup = D6D4DDC3212518A000E1C4BB;
packageReferences = ( packageReferences = (
D6B0539D23BD2BA300A066FA /* XCRemoteSwiftPackageReference "SheetController" */,
D69CCBBD249E6EFD000AF167 /* XCRemoteSwiftPackageReference "plcrashreporter" */, D69CCBBD249E6EFD000AF167 /* XCRemoteSwiftPackageReference "plcrashreporter" */,
D60CFFD924A290BA00D00083 /* XCRemoteSwiftPackageReference "SwiftSoup" */, D60CFFD924A290BA00D00083 /* XCRemoteSwiftPackageReference "SwiftSoup" */,
D6676CA127A8D0020052936B /* XCRemoteSwiftPackageReference "swift-url" */, D6676CA127A8D0020052936B /* XCRemoteSwiftPackageReference "swift-url" */,
@ -1637,6 +1633,7 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
D611C2D0232DC61100C86A49 /* HashtagTableViewCell.xib in Resources */, D611C2D0232DC61100C86A49 /* HashtagTableViewCell.xib in Resources */,
D6E77D0D286E6B7300D8B732 /* TrendingLinkCardCollectionViewCell.xib in Resources */,
D693A73025CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.xib in Resources */, D693A73025CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.xib in Resources */,
D61AC1D9232EA42D00C54D2D /* InstanceTableViewCell.xib in Resources */, D61AC1D9232EA42D00C54D2D /* InstanceTableViewCell.xib in Resources */,
D626493923C0FD0000612E6E /* AllPhotosTableViewCell.xib in Resources */, D626493923C0FD0000612E6E /* AllPhotosTableViewCell.xib in Resources */,
@ -1653,7 +1650,6 @@
D663625D2135C74800C9CBA2 /* ConversationMainStatusTableViewCell.xib in Resources */, D663625D2135C74800C9CBA2 /* ConversationMainStatusTableViewCell.xib in Resources */,
D640D76922BAF5E6004FBE69 /* DomainBlocks.plist in Resources */, D640D76922BAF5E6004FBE69 /* DomainBlocks.plist in Resources */,
D6289E84217B795D0003D1D7 /* LargeImageViewController.xib in Resources */, D6289E84217B795D0003D1D7 /* LargeImageViewController.xib in Resources */,
D6093FB125BE0B01004811E6 /* TrendingHashtagTableViewCell.xib in Resources */,
D627FF79217E950100CC0648 /* DraftsTableViewController.xib in Resources */, D627FF79217E950100CC0648 /* DraftsTableViewController.xib in Resources */,
D626493D23C1000300612E6E /* AlbumTableViewCell.xib in Resources */, D626493D23C1000300612E6E /* AlbumTableViewCell.xib in Resources */,
D627944723A6AC9300D38C68 /* BasicTableViewCell.xib in Resources */, D627944723A6AC9300D38C68 /* BasicTableViewCell.xib in Resources */,
@ -1764,7 +1760,6 @@
D64BC18F23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.swift in Sources */, D64BC18F23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.swift in Sources */,
D62E9989279DB2D100C26176 /* InstanceFeatures.swift in Sources */, D62E9989279DB2D100C26176 /* InstanceFeatures.swift in Sources */,
D693DE5923FE24310061E07D /* InteractivePushTransition.swift in Sources */, D693DE5923FE24310061E07D /* InteractivePushTransition.swift in Sources */,
D6E1EEF4285443EF00D20549 /* UIAction+Subtitle.swift in Sources */,
D69693FA25859A8000F4E116 /* ComposeSceneDelegate.swift in Sources */, D69693FA25859A8000F4E116 /* ComposeSceneDelegate.swift in Sources */,
D6A3BC8A2321F79B00FD64D5 /* AccountTableViewCell.swift in Sources */, D6A3BC8A2321F79B00FD64D5 /* AccountTableViewCell.swift in Sources */,
D66A77BB233838DC0058F1EC /* UIFont+Traits.swift in Sources */, D66A77BB233838DC0058F1EC /* UIFont+Traits.swift in Sources */,
@ -1812,6 +1807,7 @@
D662AEF2263A4BE10082A153 /* ComposePollView.swift in Sources */, D662AEF2263A4BE10082A153 /* ComposePollView.swift in Sources */,
D677284A24ECBDF400C732D3 /* ComposeCurrentAccount.swift in Sources */, D677284A24ECBDF400C732D3 /* ComposeCurrentAccount.swift in Sources */,
D62D2424217ABF3F005076CC /* NSUserActivity+Extensions.swift in Sources */, D62D2424217ABF3F005076CC /* NSUserActivity+Extensions.swift in Sources */,
D6E77D0F286F773900D8B732 /* SplitNavigationController.swift in Sources */,
D6B30E09254BAF63009CAEE5 /* ImageGrayscalifier.swift in Sources */, D6B30E09254BAF63009CAEE5 /* ImageGrayscalifier.swift in Sources */,
D6A6C10F25B62D2400298D0F /* DiskCache.swift in Sources */, D6A6C10F25B62D2400298D0F /* DiskCache.swift in Sources */,
D6B81F3C2560365300F6E31D /* RefreshableViewController.swift in Sources */, D6B81F3C2560365300F6E31D /* RefreshableViewController.swift in Sources */,
@ -1857,7 +1853,6 @@
D6EAE0DB2550CC8A002DB0AC /* FocusableTextField.swift in Sources */, D6EAE0DB2550CC8A002DB0AC /* FocusableTextField.swift in Sources */,
D623A53D2635F5590095BD04 /* StatusPollView.swift in Sources */, D623A53D2635F5590095BD04 /* StatusPollView.swift in Sources */,
D64AAE9726C88DC400FC57FB /* ToastConfiguration.swift in Sources */, D64AAE9726C88DC400FC57FB /* ToastConfiguration.swift in Sources */,
D6093FB025BE0B01004811E6 /* TrendingHashtagTableViewCell.swift in Sources */,
D63661C02381C144004B9E16 /* PreferencesNavigationController.swift in Sources */, D63661C02381C144004B9E16 /* PreferencesNavigationController.swift in Sources */,
D6E9CDA8281A427800BBC98E /* PostService.swift in Sources */, D6E9CDA8281A427800BBC98E /* PostService.swift in Sources */,
D6B053A223BD2C0600A066FA /* AssetPickerViewController.swift in Sources */, D6B053A223BD2C0600A066FA /* AssetPickerViewController.swift in Sources */,
@ -1888,7 +1883,6 @@
D64D0AAD2128D88B005A6F37 /* LocalData.swift in Sources */, D64D0AAD2128D88B005A6F37 /* LocalData.swift in Sources */,
D6945C2F23AC47C3005C403C /* SavedDataManager.swift in Sources */, D6945C2F23AC47C3005C403C /* SavedDataManager.swift in Sources */,
D6C94D892139E6EC00CB5196 /* AttachmentView.swift in Sources */, D6C94D892139E6EC00CB5196 /* AttachmentView.swift in Sources */,
D6B053AE23BD322B00A066FA /* AssetPickerSheetContainerViewController.swift in Sources */,
D6C693EF216192C2007D6A6D /* TuskerNavigationDelegate.swift in Sources */, D6C693EF216192C2007D6A6D /* TuskerNavigationDelegate.swift in Sources */,
D6C94D872139E62700CB5196 /* LargeImageViewController.swift in Sources */, D6C94D872139E62700CB5196 /* LargeImageViewController.swift in Sources */,
D6B053AB23BD2F1400A066FA /* AssetCollectionViewCell.swift in Sources */, D6B053AB23BD2F1400A066FA /* AssetCollectionViewCell.swift in Sources */,
@ -1929,6 +1923,7 @@
D64F80E2215875CC00BEF393 /* XCBActionType.swift in Sources */, D64F80E2215875CC00BEF393 /* XCBActionType.swift in Sources */,
04586B4322B301470021BD04 /* AppearancePrefsView.swift in Sources */, 04586B4322B301470021BD04 /* AppearancePrefsView.swift in Sources */,
D6FF9860255C717400845181 /* AccountSwitchingContainerViewController.swift in Sources */, D6FF9860255C717400845181 /* AccountSwitchingContainerViewController.swift in Sources */,
D6E77D0B286D426E00D8B732 /* TrendingLinkCardCollectionViewCell.swift in Sources */,
D6114E1127F899B30080E273 /* TrendingLinksViewController.swift in Sources */, D6114E1127F899B30080E273 /* TrendingLinksViewController.swift in Sources */,
D670F8B62537DC890046588A /* EmojiPickerWrapper.swift in Sources */, D670F8B62537DC890046588A /* EmojiPickerWrapper.swift in Sources */,
D6B81F442560390300F6E31D /* MenuController.swift in Sources */, D6B81F442560390300F6E31D /* MenuController.swift in Sources */,
@ -1972,6 +1967,7 @@
D6C99FCB24FADC91005C74D3 /* MainComposeTextView.swift in Sources */, D6C99FCB24FADC91005C74D3 /* MainComposeTextView.swift in Sources */,
D6093F9B25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift in Sources */, D6093F9B25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift in Sources */,
D6E4267725327FB400C02E1C /* ComposeAutocompleteView.swift in Sources */, D6E4267725327FB400C02E1C /* ComposeAutocompleteView.swift in Sources */,
D6E77D09286D25FA00D8B732 /* TrendingHashtagCollectionViewCell.swift in Sources */,
D6757A7E2157E02600721E32 /* XCBRequestSpec.swift in Sources */, D6757A7E2157E02600721E32 /* XCBRequestSpec.swift in Sources */,
D677284E24ECC01D00C732D3 /* Draft.swift in Sources */, D677284E24ECC01D00C732D3 /* Draft.swift in Sources */,
D667E5F12134D5050057A976 /* UIViewController+Delegates.swift in Sources */, D667E5F12134D5050057A976 /* UIViewController+Delegates.swift in Sources */,
@ -2205,7 +2201,7 @@
CURRENT_PROJECT_VERSION = 31; CURRENT_PROJECT_VERSION = 31;
DEVELOPMENT_TEAM = V4WK9KR9U2; DEVELOPMENT_TEAM = V4WK9KR9U2;
INFOPLIST_FILE = Tusker/Info.plist; INFOPLIST_FILE = Tusker/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.3; IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
@ -2235,7 +2231,7 @@
CURRENT_PROJECT_VERSION = 31; CURRENT_PROJECT_VERSION = 31;
DEVELOPMENT_TEAM = V4WK9KR9U2; DEVELOPMENT_TEAM = V4WK9KR9U2;
INFOPLIST_FILE = Tusker/Info.plist; INFOPLIST_FILE = Tusker/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.3; IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
@ -2463,14 +2459,6 @@
minimumVersion = 1.8.0; minimumVersion = 1.8.0;
}; };
}; };
D6B0539D23BD2BA300A066FA /* XCRemoteSwiftPackageReference "SheetController" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://git.shadowfacts.net/shadowfacts/SheetController.git";
requirement = {
branch = master;
kind = branch;
};
};
/* End XCRemoteSwiftPackageReference section */ /* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */ /* Begin XCSwiftPackageProductDependency section */
@ -2493,11 +2481,6 @@
package = D69CCBBD249E6EFD000AF167 /* XCRemoteSwiftPackageReference "plcrashreporter" */; package = D69CCBBD249E6EFD000AF167 /* XCRemoteSwiftPackageReference "plcrashreporter" */;
productName = CrashReporter; productName = CrashReporter;
}; };
D6B0539E23BD2BA300A066FA /* SheetController */ = {
isa = XCSwiftPackageProductDependency;
package = D6B0539D23BD2BA300A066FA /* XCRemoteSwiftPackageReference "SheetController" */;
productName = SheetController;
};
/* End XCSwiftPackageProductDependency section */ /* End XCSwiftPackageProductDependency section */
/* Begin XCVersionGroup section */ /* Begin XCVersionGroup section */

View File

@ -99,6 +99,11 @@
value = "1" value = "1"
isEnabled = "NO"> isEnabled = "NO">
</EnvironmentVariable> </EnvironmentVariable>
<EnvironmentVariable
key = "CG_NUMERICS_SHOW_BACKTRACE"
value = ""
isEnabled = "NO">
</EnvironmentVariable>
<EnvironmentVariable <EnvironmentVariable
key = "DEBUG_BLUR_HASH" key = "DEBUG_BLUR_HASH"
value = "1" value = "1"

View File

@ -41,20 +41,14 @@ class ImageCache {
let wrappedCompletion: ((Data?, UIImage?) -> Void)? let wrappedCompletion: ((Data?, UIImage?) -> Void)?
if let completion = completion { if let completion = completion {
wrappedCompletion = { (data, image) in wrappedCompletion = { (data, image) in
if #available(iOS 15.0, *) { if !loadOriginal,
if !loadOriginal, let size = self.desiredPixelSize {
let size = self.desiredPixelSize { image?.prepareThumbnail(of: size, completionHandler: {
image?.prepareThumbnail(of: size, completionHandler: { completion(data, $0)
completion(data, $0) })
})
} else {
image?.prepareForDisplay {
completion(data, $0)
}
}
} else { } else {
self.backgroundQueue.async { image?.prepareForDisplay {
completion(data, image) completion(data, $0)
} }
} }
} }

View File

@ -48,7 +48,7 @@ class MastodonController: ObservableObject {
@Published private(set) var instanceFeatures = InstanceFeatures() @Published private(set) var instanceFeatures = InstanceFeatures()
private(set) var customEmojis: [Emoji]? private(set) var customEmojis: [Emoji]?
private var pendingOwnInstanceRequestCallbacks = [(Instance) -> Void]() private var pendingOwnInstanceRequestCallbacks = [(Result<Instance, Client.Error>) -> Void]()
private var ownInstanceRequest: URLSessionTask? private var ownInstanceRequest: URLSessionTask?
var loggedIn: Bool { var loggedIn: Bool {
@ -159,15 +159,28 @@ class MastodonController: ObservableObject {
} }
func getOwnInstance(completion: ((Instance) -> Void)? = nil) { func getOwnInstance(completion: ((Instance) -> Void)? = nil) {
getOwnInstanceInternal(retryAttempt: 0, completion: completion) getOwnInstanceInternal(retryAttempt: 0) {
if case let .success(instance) = $0 {
completion?(instance)
}
}
} }
private func getOwnInstanceInternal(retryAttempt: Int, completion: ((Instance) -> Void)?) { @MainActor
func getOwnInstance() async throws -> Instance {
return try await withCheckedThrowingContinuation({ continuation in
getOwnInstanceInternal(retryAttempt: 0) { result in
continuation.resume(with: result)
}
})
}
private func getOwnInstanceInternal(retryAttempt: Int, completion: ((Result<Instance, Client.Error>) -> Void)?) {
// this is main thread only to prevent concurrent access to ownInstanceRequest and pendingOwnInstanceRequestCallbacks // this is main thread only to prevent concurrent access to ownInstanceRequest and pendingOwnInstanceRequestCallbacks
assert(Thread.isMainThread) assert(Thread.isMainThread)
if let instance = self.instance { if let instance = self.instance {
completion?(instance) completion?(.success(instance))
} else { } else {
if let completion = completion { if let completion = completion {
pendingOwnInstanceRequestCallbacks.append(completion) pendingOwnInstanceRequestCallbacks.append(completion)
@ -177,7 +190,7 @@ class MastodonController: ObservableObject {
let request = Client.getInstance() let request = Client.getInstance()
ownInstanceRequest = run(request) { (response) in ownInstanceRequest = run(request) { (response) in
switch response { switch response {
case .failure(_): case .failure(let error):
let delay: DispatchTimeInterval let delay: DispatchTimeInterval
switch retryAttempt { switch retryAttempt {
case 0: case 0:
@ -190,6 +203,10 @@ class MastodonController: ObservableObject {
delay = .seconds(60) delay = .seconds(60)
default: default:
// if we've failed four times, just give up :/ // if we've failed four times, just give up :/
for completion in self.pendingOwnInstanceRequestCallbacks {
completion(.failure(error))
}
self.pendingOwnInstanceRequestCallbacks = []
return return
} }
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
@ -204,7 +221,7 @@ class MastodonController: ObservableObject {
self.instanceFeatures.update(instance: instance, nodeInfo: self.nodeInfo) self.instanceFeatures.update(instance: instance, nodeInfo: self.nodeInfo)
for completion in self.pendingOwnInstanceRequestCallbacks { for completion in self.pendingOwnInstanceRequestCallbacks {
completion(instance) completion(.success(instance))
} }
self.pendingOwnInstanceRequestCallbacks = [] self.pendingOwnInstanceRequestCallbacks = []
} }

View File

@ -22,7 +22,7 @@ struct MenuController {
let data: Any let data: Any
if case let .tab(tab) = item { if case let .tab(tab) = item {
data = tab.rawValue data = tab.rawValue
} else if case .search = item { } else if case .explore = item {
data = "search" data = "search"
} else if case .bookmarks = item { } else if case .bookmarks = item {
data = "bookmarks" data = "bookmarks"
@ -42,7 +42,7 @@ struct MenuController {
static let sidebarItemKeyCommands: [UIKeyCommand] = [ static let sidebarItemKeyCommands: [UIKeyCommand] = [
sidebarCommand(item: .tab(.timelines), command: "1"), sidebarCommand(item: .tab(.timelines), command: "1"),
sidebarCommand(item: .tab(.notifications), command: "2"), sidebarCommand(item: .tab(.notifications), command: "2"),
sidebarCommand(item: .search, command: "3"), sidebarCommand(item: .explore, command: "3"),
sidebarCommand(item: .bookmarks, command: "4"), sidebarCommand(item: .bookmarks, command: "4"),
sidebarCommand(item: .tab(.myProfile), command: "5"), sidebarCommand(item: .tab(.myProfile), command: "5"),
] ]

View File

@ -1,19 +0,0 @@
//
// UIAction+Subtitle.swift
// Tusker
//
// Created by Shadowfacts on 6/10/22.
// Copyright © 2022 Shadowfacts. All rights reserved.
//
import UIKit
extension UIAction {
convenience init(title: String, subtitle: String?, image: UIImage?, state: UIAction.State, handler: @escaping UIActionHandler) {
if #available(iOS 15.0, *) {
self.init(title: title, subtitle: subtitle, image: image, identifier: nil, discoverabilityTitle: nil, attributes: [], state: state, handler: handler)
} else {
self.init(title: title, image: image, identifier: nil, discoverabilityTitle: nil, attributes: [], state: state, handler: handler)
}
}
}

View File

@ -34,6 +34,10 @@ struct InstanceFeatures {
instanceType != .pixelfed instanceType != .pixelfed
} }
var trends: Bool {
instanceType == .mastodon
}
var trendingStatusesAndLinks: Bool { var trendingStatusesAndLinks: Bool {
instanceType == .mastodon && version != nil && version! >= Version(3, 5, 0) instanceType == .mastodon && version != nil && version! >= Version(3, 5, 0)
} }

View File

@ -1,63 +0,0 @@
//
// AssetPickerSheetContainerViewController.swift
// Tusker
//
// Created by Shadowfacts on 1/1/20.
// Copyright © 2020 Shadowfacts. All rights reserved.
//
import UIKit
import SheetController
import Photos
class AssetPickerSheetContainerViewController: SheetContainerViewController {
let assetPicker = AssetPickerViewController()
init() {
super.init(content: assetPicker)
assetPicker.view.translatesAutoresizingMaskIntoConstraints = false
assetPicker.view.layer.masksToBounds = true
delegate = self
assetPicker.delegate = self
detents = [.bottom, .middle, .top]
overrideUserInterfaceStyle = .dark
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
assetPicker.view.layer.cornerRadius = view.bounds.width * 0.02
// don't round bottom corners, since they'll always be cut off by the device
assetPicker.view.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
super.viewDidLoad()
}
}
extension AssetPickerSheetContainerViewController: SheetContainerViewControllerDelegate {
func sheetContainerContentScrollView(_ sheetContainer: SheetContainerViewController) -> UIScrollView? {
if let vc = assetPicker.visibleViewController as? UITableViewController {
return vc.tableView
} else if let vc = assetPicker.visibleViewController as? UICollectionViewController {
return vc.collectionView
}
return nil
}
func sheetContainer(_ sheetContainer: SheetContainerViewController, topContentOffsetForScrollView scrollView: UIScrollView) -> CGFloat {
return assetPicker.navigationBar.bounds.height
}
}
extension AssetPickerSheetContainerViewController: UINavigationControllerDelegate {
func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
contentScrollViewChanged()
// viewController.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(donePressed))
}
}

View File

@ -37,15 +37,9 @@ struct ComposeAttachmentRow: View {
} }
} }
if #available(iOS 15.0, *) { Button(action: self.removeAttachment) {
Button(action: self.removeAttachment) { Label("Delete", systemImage: "trash")
Label("Delete", systemImage: "trash") }.foregroundStyle(.red)
}.foregroundStyle(.red)
} else {
Button(action: self.removeAttachment) {
Label("Delete", systemImage: "trash")
}
}
} previewIfAvailable: { } previewIfAvailable: {
ComposeAttachmentImage(attachment: attachment, fullSize: true) ComposeAttachmentImage(attachment: attachment, fullSize: true)
} }

View File

@ -191,16 +191,6 @@ struct ComposeAttachmentsList: View {
} }
fileprivate extension View { fileprivate extension View {
@available(iOS, obsoleted: 15.0)
@ViewBuilder
func onDragWithPreviewIfAvailable<V>(_ data: @escaping () -> NSItemProvider, preview: () -> V) -> some View where V : View {
if #available(iOS 15.0, *) {
self.onDrag(data, preview: preview)
} else {
self.onDrag(data)
}
}
@available(iOS, obsoleted: 16.0) @available(iOS, obsoleted: 16.0)
@ViewBuilder @ViewBuilder
func sheetOrPopover(isPresented: Binding<Bool>, @ViewBuilder content: @escaping () -> some View) -> some View { func sheetOrPopover(isPresented: Binding<Bool>, @ViewBuilder content: @escaping () -> some View) -> some View {

View File

@ -25,8 +25,6 @@ struct ComposeAutocompleteView: View {
var body: some View { var body: some View {
suggestionsView suggestionsView
// animate changes of the scroll view items
.animation(.default)
.background(backgroundColor) .background(backgroundColor)
.overlay(borderColor.frame(height: 0.5), alignment: .top) .overlay(borderColor.frame(height: 0.5), alignment: .top)
} }
@ -85,8 +83,8 @@ struct ComposeAutocompleteMentionsView: View {
} }
.frame(height: 30) .frame(height: 30)
.padding(.vertical, 8) .padding(.vertical, 8)
.animation(.linear(duration: 0.1))
} }
.animation(.linear(duration: 0.1), value: accounts)
Spacer() Spacer()
} }
@ -167,7 +165,7 @@ struct ComposeAutocompleteMentionsView: View {
.map(\.0) .map(\.0)
} }
private enum EitherAccount { private enum EitherAccount: Equatable {
case pachyderm(Account) case pachyderm(Account)
case coreData(AccountMO) case coreData(AccountMO)
@ -197,6 +195,10 @@ struct ComposeAutocompleteMentionsView: View {
return account.avatar return account.avatar
} }
} }
static func ==(lhs: EitherAccount, rhs: EitherAccount) -> Bool {
return lhs.id == rhs.id
}
} }
} }
@ -212,7 +214,7 @@ struct ComposeAutocompleteEmojisView: View {
HStack(alignment: expanded ? .top : .center, spacing: 0) { HStack(alignment: expanded ? .top : .center, spacing: 0) {
if case let .emoji(query) = uiState.autocompleteState { if case let .emoji(query) = uiState.autocompleteState {
emojiList(query: query) emojiList(query: query)
.animation(.default) .animation(.default, value: expanded)
.transition(.move(edge: .bottom)) .transition(.move(edge: .bottom))
} else { } else {
// when the autocomplete view is animating out, the autocomplete state is nil // when the autocomplete view is animating out, the autocomplete state is nil
@ -259,8 +261,8 @@ struct ComposeAutocompleteEmojisView: View {
} }
.frame(height: 30) .frame(height: 30)
.padding(.vertical, 8) .padding(.vertical, 8)
.animation(.linear(duration: 0.2))
} }
.animation(.linear(duration: 0.2), value: emojis)
Spacer(minLength: 30) Spacer(minLength: 30)
} }
@ -319,8 +321,8 @@ struct ComposeAutocompleteHashtagsView: View {
} }
.frame(height: 30) .frame(height: 30)
.padding(.vertical, 8) .padding(.vertical, 8)
.animation(.linear(duration: 0.1))
} }
.animation(.linear(duration: 0.1), value: hashtags)
Spacer() Spacer()
} }

View File

@ -339,24 +339,14 @@ extension ComposeHostingController: ComposeUIStateDelegate {
} }
func presentAssetPickerSheet() { func presentAssetPickerSheet() {
if #available(iOS 15.0, *) { let picker = AssetPickerViewController()
let picker = AssetPickerViewController() picker.assetPickerDelegate = self
picker.assetPickerDelegate = self picker.modalPresentationStyle = .pageSheet
picker.modalPresentationStyle = .pageSheet picker.overrideUserInterfaceStyle = .dark
picker.overrideUserInterfaceStyle = .dark let sheet = picker.sheetPresentationController!
let sheet = picker.sheetPresentationController! sheet.detents = [.medium(), .large()]
sheet.detents = [.medium(), .large()] sheet.prefersEdgeAttachedInCompactHeight = true
sheet.prefersEdgeAttachedInCompactHeight = true self.present(picker, animated: true)
self.present(picker, animated: true)
} else {
presentOldAssetPickerSheet()
}
}
private func presentOldAssetPickerSheet() {
let sheetContainer = AssetPickerSheetContainerViewController()
sheetContainer.assetPicker.assetPickerDelegate = self
self.present(sheetContainer, animated: true)
} }
func presentComposeDrawing() { func presentComposeDrawing() {

View File

@ -60,7 +60,9 @@ struct ComposePollView: View {
HStack { HStack {
// use .animation(nil) on pickers so frame doesn't have a size change animation when the text changes // use .animation(nil) on pickers so frame doesn't have a size change animation when the text changes
Picker(selection: $poll.multiple, label: Text(poll.multiple ? "Allow multiple choices" : "Single choice")) { // this is deprecated in iOS 15, but using .animation(nil, value: poll.multiple) does not work (it still animates)
// nor does setting that on the Text rather than the Picker
Picker(selection: $poll.multiple, label: Text(poll.multiple ? "Allow multiple choice" : "Single choice")) {
Text("Allow multiple choices").tag(true) Text("Allow multiple choices").tag(true)
Text("Single choice").tag(false) Text("Single choice").tag(false)
} }
@ -154,8 +156,7 @@ struct ComposePollOption: View {
var body: some View { var body: some View {
HStack(spacing: 4) { HStack(spacing: 4) {
Checkbox(radiusFraction: poll.multiple ? 0.1 : 0.5, borderWidth: 2) Checkbox(radiusFraction: poll.multiple ? 0.1 : 0.5, borderWidth: 2)
.animation(.default) .animation(.default, value: poll.multiple)
textField textField

View File

@ -49,7 +49,7 @@ extension ComposeUIState {
} }
extension ComposeUIState { extension ComposeUIState {
enum AutocompleteState { enum AutocompleteState: Equatable {
case mention(String) case mention(String)
case emoji(String) case emoji(String)
case hashtag(String) case hashtag(String)

View File

@ -119,7 +119,7 @@ struct ComposeView: View {
} }
} }
.transition(.move(edge: .bottom)) .transition(.move(edge: .bottom))
.animation(.default) .animation(.default, value: uiState.autocompleteState)
} }
func mainStack(outerMinY: CGFloat) -> some View { func mainStack(outerMinY: CGFloat) -> some View {
@ -147,7 +147,6 @@ struct ComposeView: View {
if let poll = draft.poll { if let poll = draft.poll {
ComposePollView(draft: draft, poll: poll) ComposePollView(draft: draft, poll: poll)
.transition(.opacity.combined(with: .asymmetric(insertion: .scale(scale: 0.5, anchor: .leading), removal: .scale(scale: 0.5, anchor: .trailing)))) .transition(.opacity.combined(with: .asymmetric(insertion: .scale(scale: 0.5, anchor: .leading), removal: .scale(scale: 0.5, anchor: .trailing))))
.animation(.default)
} }

View File

@ -124,13 +124,8 @@ class ConversationTableViewController: EnhancedTableViewController {
} }
}) })
if #available(iOS 15.0, *) { visibilityBarButtonItem = UIBarButtonItem(image: ConversationTableViewController.showPostsImage, style: .plain, target: self, action: #selector(toggleVisibilityButtonPressed))
visibilityBarButtonItem = UIBarButtonItem(image: ConversationTableViewController.showPostsImage, style: .plain, target: self, action: #selector(toggleVisibilityButtonPressed)) visibilityBarButtonItem.isSelected = showStatusesAutomatically
visibilityBarButtonItem.isSelected = showStatusesAutomatically
} else {
let initialImage = showStatusesAutomatically ? ConversationTableViewController.hidePostsImage : ConversationTableViewController.showPostsImage
visibilityBarButtonItem = UIBarButtonItem(image: initialImage, style: .plain, target: self, action: #selector(toggleVisibilityButtonPressed))
}
navigationItem.rightBarButtonItem = visibilityBarButtonItem navigationItem.rightBarButtonItem = visibilityBarButtonItem
// disable transparent background when scroll to top because it looks weird when items earlier in the thread load in // disable transparent background when scroll to top because it looks weird when items earlier in the thread load in
// (it remains transparent slightly too long, resulting in a flash of the content under the transparent bar) // (it remains transparent slightly too long, resulting in a flash of the content under the transparent bar)
@ -396,15 +391,7 @@ class ConversationTableViewController: EnhancedTableViewController {
tableView.beginUpdates() tableView.beginUpdates()
tableView.endUpdates() tableView.endUpdates()
if #available(iOS 15.0, *) { visibilityBarButtonItem.isSelected = showStatusesAutomatically
visibilityBarButtonItem.isSelected = showStatusesAutomatically
} else {
if showStatusesAutomatically {
visibilityBarButtonItem.image = ConversationTableViewController.hidePostsImage
} else {
visibilityBarButtonItem.image = ConversationTableViewController.showPostsImage
}
}
} }
} }

View File

@ -9,19 +9,20 @@
import UIKit import UIKit
import Pachyderm import Pachyderm
class AddSavedHashtagViewController: EnhancedTableViewController { class AddSavedHashtagViewController: UIViewController {
weak var mastodonController: MastodonController! weak var mastodonController: MastodonController!
var resultsController: SearchResultsViewController! var resultsController: SearchResultsViewController!
var searchController: UISearchController! var searchController: UISearchController!
var dataSource: UITableViewDiffableDataSource<Section, Item>! private var collectionView: UICollectionView!
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
init(mastodonController: MastodonController) { init(mastodonController: MastodonController) {
self.mastodonController = mastodonController self.mastodonController = mastodonController
super.init(style: .grouped) super.init(nibName: nil, bundle: nil)
} }
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
@ -33,17 +34,37 @@ class AddSavedHashtagViewController: EnhancedTableViewController {
title = NSLocalizedString("Search", comment: "search screen title") title = NSLocalizedString("Search", comment: "search screen title")
tableView.register(UINib(nibName: "TrendingHashtagTableViewCell", bundle: .main), forCellReuseIdentifier: "trendingTagCell") view.backgroundColor = .systemGroupedBackground
tableView.rowHeight = 60 // 44 for content + 2 * 8 spacing
dataSource = DataSource(tableView: tableView, cellProvider: { (tableView, indexPath, item) -> UITableViewCell? in var config = UICollectionLayoutListConfiguration(appearance: .grouped)
config.headerMode = .supplementary
let layout = UICollectionViewCompositionalLayout.list(using: config)
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
collectionView.delegate = self
view.addSubview(collectionView)
let sectionHeaderCell = UICollectionView.SupplementaryRegistration<UICollectionViewListCell>(elementKind: UICollectionView.elementKindSectionHeader) { (headerView, collectionView, indexPath) in
var config = headerView.defaultContentConfiguration()
config.text = NSLocalizedString("Trending Hashtags", comment: "trending hashtags section title")
headerView.contentConfiguration = config
}
let registration = UICollectionView.CellRegistration<TrendingHashtagCollectionViewCell, Hashtag> { cell, indexPath, hashtag in
cell.updateUI(hashtag: hashtag)
}
dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) { (collectionView, indexPath, item) in
switch item { switch item {
case let .tag(hashtag): case let .tag(hashtag):
let cell = tableView.dequeueReusableCell(withIdentifier: "trendingTagCell", for: indexPath) as! TrendingHashtagTableViewCell return collectionView.dequeueConfiguredReusableCell(using: registration, for: indexPath, item: hashtag)
cell.updateUI(hashtag: hashtag)
return cell
} }
}) }
dataSource.supplementaryViewProvider = { (collectionView, elementKind, indexPath) in
if elementKind == UICollectionView.elementKindSectionHeader {
return collectionView.dequeueConfiguredReusableSupplementary(using: sectionHeaderCell, for: indexPath)
} else {
return nil
}
}
resultsController = HashtagSearchResultsViewController(mastodonController: mastodonController) resultsController = HashtagSearchResultsViewController(mastodonController: mastodonController)
resultsController.delegate = self resultsController.delegate = self
@ -92,17 +113,6 @@ class AddSavedHashtagViewController: EnhancedTableViewController {
presentingViewController!.dismiss(animated: true) presentingViewController!.dismiss(animated: true)
} }
// MARK: - Table View Delegate
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
switch dataSource.itemIdentifier(for: indexPath) {
case nil:
return
case let .tag(hashtag):
selectHashtag(hashtag)
}
}
// MARK: - Interaction // MARK: - Interaction
@objc func cancelButtonPressed() { @objc func cancelButtonPressed() {
@ -115,14 +125,23 @@ extension AddSavedHashtagViewController {
enum Section { enum Section {
case trendingTags case trendingTags
} }
enum Item: Hashable { enum Item: Hashable {
case tag(Hashtag) case tag(Hashtag)
} }
// class DataSource: UITableViewDiffableDataSource<Section, Item> {
// override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
// return
// }
// }
}
class DataSource: UITableViewDiffableDataSource<Section, Item> { extension AddSavedHashtagViewController: UICollectionViewDelegate {
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
return NSLocalizedString("Trending Hashtags", comment: "trending hashtags seciton title") switch dataSource.itemIdentifier(for: indexPath) {
case nil:
return
case let .tag(hashtag):
selectHashtag(hashtag)
} }
} }
} }

View File

@ -9,18 +9,17 @@
import UIKit import UIKit
import Pachyderm import Pachyderm
class TrendingHashtagsViewController: EnhancedTableViewController { class TrendingHashtagsViewController: UIViewController {
weak var mastodonController: MastodonController! weak var mastodonController: MastodonController!
private var dataSource: UITableViewDiffableDataSource<Section, Item>! private var collectionView: UICollectionView!
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
init(mastodonController: MastodonController) { init(mastodonController: MastodonController) {
self.mastodonController = mastodonController self.mastodonController = mastodonController
super.init(style: .grouped) super.init(nibName: nil, bundle: nil)
dragEnabled = true
} }
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
@ -32,15 +31,24 @@ class TrendingHashtagsViewController: EnhancedTableViewController {
title = NSLocalizedString("Trending Hashtags", comment: "trending hashtags screen title") title = NSLocalizedString("Trending Hashtags", comment: "trending hashtags screen title")
tableView.register(UINib(nibName: "TrendingHashtagTableViewCell", bundle: .main), forCellReuseIdentifier: "trendingTagCell") view.backgroundColor = .systemGroupedBackground
tableView.rowHeight = 60 // 44 for content + 2 * 8 spacing
dataSource = UITableViewDiffableDataSource<Section, Item>(tableView: tableView) { (tableView, indexPath, item) in let config = UICollectionLayoutListConfiguration(appearance: .grouped)
let layout = UICollectionViewCompositionalLayout.list(using: config)
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
collectionView.delegate = self
collectionView.dragDelegate = self
view.addSubview(collectionView)
let registration = UICollectionView.CellRegistration<TrendingHashtagCollectionViewCell, Hashtag> { cell, indexPath, hashtag in
cell.updateUI(hashtag: hashtag)
}
dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) { (collectionView, indexPath, item) in
switch item { switch item {
case let .tag(hashtag): case let .tag(hashtag):
let cell = tableView.dequeueReusableCell(withIdentifier: "trendingTagCell", for: indexPath) as! TrendingHashtagTableViewCell return collectionView.dequeueConfiguredReusableCell(using: registration, for: indexPath, item: hashtag)
cell.updateUI(hashtag: hashtag)
return cell
} }
} }
} }
@ -60,9 +68,19 @@ class TrendingHashtagsViewController: EnhancedTableViewController {
} }
} }
// MARK: - Table View Delegate }
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { extension TrendingHashtagsViewController {
enum Section {
case trendingTags
}
enum Item: Hashable {
case tag(Hashtag)
}
}
extension TrendingHashtagsViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
guard let item = dataSource.itemIdentifier(for: indexPath), guard let item = dataSource.itemIdentifier(for: indexPath),
case let .tag(hashtag) = item else { case let .tag(hashtag) = item else {
return return
@ -71,7 +89,7 @@ class TrendingHashtagsViewController: EnhancedTableViewController {
show(HashtagTimelineViewController(for: hashtag, mastodonController: mastodonController), sender: nil) show(HashtagTimelineViewController(for: hashtag, mastodonController: mastodonController), sender: nil)
} }
override func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? { func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
guard let item = dataSource.itemIdentifier(for: indexPath), guard let item = dataSource.itemIdentifier(for: indexPath),
case let .tag(hashtag) = item else { case let .tag(hashtag) = item else {
return nil return nil
@ -79,11 +97,13 @@ class TrendingHashtagsViewController: EnhancedTableViewController {
return UIContextMenuConfiguration(identifier: nil) { return UIContextMenuConfiguration(identifier: nil) {
HashtagTimelineViewController(for: hashtag, mastodonController: self.mastodonController) HashtagTimelineViewController(for: hashtag, mastodonController: self.mastodonController)
} actionProvider: { (_) in } actionProvider: { (_) in
UIMenu(children: self.actionsForHashtag(hashtag, sourceView: self.tableView.cellForRow(at: indexPath))) UIMenu(children: self.actionsForHashtag(hashtag, sourceView: self.collectionView.cellForItem(at: indexPath)))
} }
} }
}
override func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] { extension TrendingHashtagsViewController: UICollectionViewDragDelegate {
func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
guard let item = dataSource.itemIdentifier(for: indexPath), guard let item = dataSource.itemIdentifier(for: indexPath),
case let .tag(hashtag) = item else { case let .tag(hashtag) = item else {
return [] return []
@ -95,16 +115,6 @@ class TrendingHashtagsViewController: EnhancedTableViewController {
} }
return [UIDragItem(itemProvider: provider)] return [UIDragItem(itemProvider: provider)]
} }
}
extension TrendingHashtagsViewController {
enum Section {
case trendingTags
}
enum Item: Hashable {
case tag(Hashtag)
}
} }
extension TrendingHashtagsViewController: TuskerNavigationDelegate { extension TrendingHashtagsViewController: TuskerNavigationDelegate {

View File

@ -0,0 +1,141 @@
//
// TrendingLinkCardCollectionViewCell.swift
// Tusker
//
// Created by Shadowfacts on 6/29/22.
// Copyright © 2022 Shadowfacts. All rights reserved.
//
import UIKit
import Pachyderm
class TrendingLinkCardCollectionViewCell: UICollectionViewCell {
private var card: Card?
private var isGrayscale = false
private var thumbnailRequest: ImageCache.Request?
@IBOutlet weak var thumbnailView: UIImageView!
@IBOutlet weak var titleLabel: UILabel!
@IBOutlet weak var providerLabel: UILabel!
@IBOutlet weak var activityLabel: UILabel!
@IBOutlet weak var historyView: TrendHistoryView!
override func awakeFromNib() {
super.awakeFromNib()
layer.shadowOpacity = 0.2
layer.shadowRadius = 8
layer.shadowOffset = .zero
layer.masksToBounds = false
updateLayerColors()
NotificationCenter.default.addObserver(self, selector: #selector(updateUIForPreferences), name: .preferencesChanged, object: nil)
}
override func layoutSubviews() {
super.layoutSubviews()
contentView.layer.cornerRadius = 0.05 * bounds.width
thumbnailView.layer.cornerRadius = 0.05 * bounds.width
}
func updateUI(card: Card) {
self.card = card
self.thumbnailView.image = nil
updateGrayscaleableUI(card: card)
updateUIForPreferences()
let title = card.title.trimmingCharacters(in: .whitespacesAndNewlines)
titleLabel.text = title
let provider = card.providerName!.trimmingCharacters(in: .whitespacesAndNewlines)
providerLabel.text = provider
let sorted = card.history!.sorted(by: { $0.day < $1.day })
let lastTwo = sorted[(sorted.count - 2)...]
let accounts = lastTwo.map(\.accounts).reduce(0, +)
let uses = lastTwo.map(\.uses).reduce(0, +)
// U+2009 THIN SPACE
let activityStr = NSMutableAttributedString(string: "\(accounts.formatted())\u{2009}")
activityStr.append(NSAttributedString(attachment: NSTextAttachment(image: UIImage(systemName: "person")!)))
activityStr.append(NSAttributedString(string: ", \(uses.formatted())\u{2009}"))
activityStr.append(NSAttributedString(attachment: NSTextAttachment(image: UIImage(systemName: "square.text.square")!)))
activityLabel.attributedText = activityStr
historyView.setHistory(card.history)
historyView.isHidden = card.history == nil || card.history!.count < 2
}
@objc private func updateUIForPreferences() {
if isGrayscale != Preferences.shared.grayscaleImages,
let card {
updateGrayscaleableUI(card: card)
}
}
private func updateGrayscaleableUI(card: Card) {
isGrayscale = Preferences.shared.grayscaleImages
if let imageURL = card.image,
let url = URL(imageURL) {
thumbnailRequest = ImageCache.attachments.get(url, completion: { _, image in
guard let image,
let transformedImage = ImageGrayscalifier.convertIfNecessary(url: url, image: image) else {
return
}
DispatchQueue.main.async {
self.thumbnailView.image = transformedImage
}
})
if thumbnailRequest != nil {
loadBlurHash(card: card)
}
}
}
private func loadBlurHash(card: Card) {
guard let hash = card.blurhash else {
return
}
let imageViewSize = self.thumbnailView.bounds.size
AttachmentView.queue.async { [weak self] in
let size: CGSize
if let width = card.width, let height = card.height {
size = CGSize(width: width, height: height)
} else {
size = imageViewSize
}
guard let preview = UIImage(blurHash: hash, size: size) else {
return
}
DispatchQueue.main.async { [weak self] in
guard let self,
self.card?.url == card.url,
self.thumbnailView.image == nil else {
return
}
self.thumbnailView.image = preview
}
}
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
updateLayerColors()
}
private func updateLayerColors() {
if traitCollection.userInterfaceStyle == .dark {
// clippingView.layer.borderColor = UIColor.darkGray.withAlphaComponent(0.5).cgColor
layer.shadowColor = UIColor.darkGray.cgColor
} else {
// clippingView.layer.borderColor = UIColor.lightGray.withAlphaComponent(0.5).cgColor
layer.shadowColor = UIColor.black.cgColor
}
}
}

View File

@ -0,0 +1,87 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="21179.7" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_0" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21169.4"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="collection view cell content view" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" id="izA-ZZ-g7F" customClass="TrendingLinkCardCollectionViewCell" customModule="Tusker" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="300" height="400"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<collectionViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="Zb0-aW-Sen">
<rect key="frame" x="0.0" y="0.0" width="300" height="400"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="h3b-Mf-lD6">
<rect key="frame" x="0.0" y="0.0" width="300" height="225"/>
<constraints>
<constraint firstAttribute="width" secondItem="h3b-Mf-lD6" secondAttribute="height" multiplier="4:3" id="QDY-8a-LYC"/>
</constraints>
</imageView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Ho3-cU-IGi">
<rect key="frame" x="16" y="330.66666666666674" width="268" height="20.333333333333314"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleHeadline"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Provider" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="O9r-10-LDD">
<rect key="frame" x="16.000000000000004" y="355" width="57.333333333333343" height="18"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleSubhead"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Activity" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ULe-Gd-t1S">
<rect key="frame" x="16" y="377" width="43" height="15"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleCaption1"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<view contentMode="scaleToFill" ambiguous="YES" translatesAutoresizingMaskIntoConstraints="NO" id="LZj-Ii-63i" customClass="TrendHistoryView" customModule="Tusker" customModuleProvider="target">
<rect key="frame" x="200" y="355" width="100" height="44"/>
<constraints>
<constraint firstAttribute="width" constant="100" id="cUc-p7-aLH"/>
</constraints>
</view>
</subviews>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints>
<constraint firstAttribute="bottomMargin" secondItem="ULe-Gd-t1S" secondAttribute="bottom" id="6UL-8b-Aia"/>
<constraint firstItem="h3b-Mf-lD6" firstAttribute="top" secondItem="Zb0-aW-Sen" secondAttribute="top" id="EFg-Yr-vdt"/>
<constraint firstItem="Ho3-cU-IGi" firstAttribute="leading" secondItem="Zb0-aW-Sen" secondAttribute="leadingMargin" id="Ga8-LQ-f4N"/>
<constraint firstItem="ULe-Gd-t1S" firstAttribute="top" secondItem="O9r-10-LDD" secondAttribute="bottom" constant="4" id="HPD-qN-k3z"/>
<constraint firstAttribute="bottom" secondItem="LZj-Ii-63i" secondAttribute="bottom" constant="1" id="HWu-In-Uem"/>
<constraint firstItem="O9r-10-LDD" firstAttribute="leading" secondItem="Zb0-aW-Sen" secondAttribute="leadingMargin" id="Hz8-Bw-jpl"/>
<constraint firstAttribute="trailing" secondItem="LZj-Ii-63i" secondAttribute="trailing" id="J9c-CF-3EF"/>
<constraint firstItem="ULe-Gd-t1S" firstAttribute="leading" secondItem="Zb0-aW-Sen" secondAttribute="leadingMargin" id="KEj-En-StX"/>
<constraint firstItem="Ho3-cU-IGi" firstAttribute="top" secondItem="h3b-Mf-lD6" secondAttribute="bottom" constant="4" id="PjW-V1-oDs"/>
<constraint firstItem="LZj-Ii-63i" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="O9r-10-LDD" secondAttribute="trailing" id="WNr-ZP-o9a"/>
<constraint firstItem="LZj-Ii-63i" firstAttribute="top" secondItem="Ho3-cU-IGi" secondAttribute="bottom" constant="4" id="fpM-Hp-Oyf"/>
<constraint firstAttribute="trailing" secondItem="h3b-Mf-lD6" secondAttribute="trailing" id="kBD-1R-bh7"/>
<constraint firstItem="LZj-Ii-63i" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="ULe-Gd-t1S" secondAttribute="trailing" id="ruZ-p8-n0x"/>
<constraint firstAttribute="trailingMargin" secondItem="Ho3-cU-IGi" secondAttribute="trailing" id="ubj-f6-bXE"/>
<constraint firstItem="h3b-Mf-lD6" firstAttribute="leading" secondItem="Zb0-aW-Sen" secondAttribute="leading" id="wF1-Gm-nVQ"/>
<constraint firstItem="O9r-10-LDD" firstAttribute="top" secondItem="Ho3-cU-IGi" secondAttribute="bottom" constant="4" id="yPq-dT-uib"/>
</constraints>
</collectionViewCellContentView>
<connections>
<outlet property="activityLabel" destination="ULe-Gd-t1S" id="wqe-G6-IB3"/>
<outlet property="historyView" destination="LZj-Ii-63i" id="MVF-az-uyA"/>
<outlet property="providerLabel" destination="O9r-10-LDD" id="xAF-NW-ymm"/>
<outlet property="thumbnailView" destination="h3b-Mf-lD6" id="4mF-bJ-ALY"/>
<outlet property="titleLabel" destination="Ho3-cU-IGi" id="ltu-ey-chT"/>
</connections>
<point key="canvasLocation" x="0.0" y="-13.507109004739336"/>
</collectionViewCell>
</objects>
<resources>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
</resources>
</document>

View File

@ -113,6 +113,10 @@ class TrendingLinkTableViewCell: UITableViewCell {
} }
@objc private func updateUIForPreferences() { @objc private func updateUIForPreferences() {
if isGrayscale != Preferences.shared.grayscaleImages,
let card {
updateGrayscaleableUI(card: card)
}
} }
private func updateGrayscaleableUI(card: Card) { private func updateGrayscaleableUI(card: Card) {

View File

@ -19,8 +19,10 @@ protocol LargeImageContentView: UIView {
class LargeImageImageContentView: UIImageView, LargeImageContentView { class LargeImageImageContentView: UIImageView, LargeImageContentView {
#if !targetEnvironment(macCatalyst)
@available(iOS 16.0, *) @available(iOS 16.0, *)
private static let analyzer = ImageAnalyzer() private static let analyzer = ImageAnalyzer()
#endif
var animationImage: UIImage? { image! } var animationImage: UIImage? { image! }
@ -39,6 +41,7 @@ class LargeImageImageContentView: UIImageView, LargeImageContentView {
contentMode = .scaleAspectFit contentMode = .scaleAspectFit
isUserInteractionEnabled = true isUserInteractionEnabled = true
#if !targetEnvironment(macCatalyst)
if #available(iOS 16.0, *), if #available(iOS 16.0, *),
ImageAnalyzer.isSupported { ImageAnalyzer.isSupported {
let interaction = ImageAnalysisInteraction() let interaction = ImageAnalysisInteraction()
@ -54,6 +57,7 @@ class LargeImageImageContentView: UIImageView, LargeImageContentView {
} }
} }
} }
#endif
} }
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
@ -78,12 +82,14 @@ class LargeImageImageContentView: UIImageView, LargeImageContentView {
} }
} }
#if !targetEnvironment(macCatalyst)
@available(iOS 16.0, *) @available(iOS 16.0, *)
extension LargeImageImageContentView: ImageAnalysisInteractionDelegate { extension LargeImageImageContentView: ImageAnalysisInteractionDelegate {
func presentingViewController(for interaction: ImageAnalysisInteraction) -> UIViewController? { func presentingViewController(for interaction: ImageAnalysisInteraction) -> UIViewController? {
return owner return owner
} }
} }
#endif
class LargeImageGifContentView: GIFImageView, LargeImageContentView { class LargeImageGifContentView: GIFImageView, LargeImageContentView {
var animationImage: UIImage? { image } var animationImage: UIImage? { image }

View File

@ -37,7 +37,7 @@ class MainSidebarViewController: UIViewController {
} }
var exploreTabItems: [Item] { var exploreTabItems: [Item] {
var items: [Item] = [.search, .bookmarks, .trendingStatuses, .trendingTags, .trendingLinks, .profileDirectory] var items: [Item] = [.explore, .bookmarks, .trendingStatuses, .profileDirectory]
let snapshot = dataSource.snapshot() let snapshot = dataSource.snapshot()
for case let .list(list) in snapshot.itemIdentifiers(inSection: .lists) { for case let .list(list) in snapshot.itemIdentifiers(inSection: .lists) {
items.append(.list(list)) items.append(.list(list))
@ -154,7 +154,7 @@ class MainSidebarViewController: UIViewController {
snapshot.appendItems([ snapshot.appendItems([
.tab(.timelines), .tab(.timelines),
.tab(.notifications), .tab(.notifications),
.search, .explore,
.bookmarks, .bookmarks,
.tab(.myProfile) .tab(.myProfile)
], toSection: .tabs) ], toSection: .tabs)
@ -177,12 +177,10 @@ class MainSidebarViewController: UIViewController {
var discoverSnapshot = NSDiffableDataSourceSectionSnapshot<Item>() var discoverSnapshot = NSDiffableDataSourceSectionSnapshot<Item>()
discoverSnapshot.append([.discoverHeader]) discoverSnapshot.append([.discoverHeader])
discoverSnapshot.append([ discoverSnapshot.append([
.trendingTags,
.profileDirectory, .profileDirectory,
], to: .discoverHeader) ], to: .discoverHeader)
if mastodonController.instanceFeatures.trendingStatusesAndLinks { if mastodonController.instanceFeatures.trendingStatusesAndLinks {
discoverSnapshot.insert([.trendingStatuses], before: .trendingTags) discoverSnapshot.insert([.trendingStatuses], before: .profileDirectory)
discoverSnapshot.insert([.trendingLinks], after: .trendingTags)
} }
dataSource.apply(discoverSnapshot, to: .discover) dataSource.apply(discoverSnapshot, to: .discover)
} }
@ -345,7 +343,7 @@ class MainSidebarViewController: UIViewController {
return UserActivityManager.checkNotificationsActivity(mode: Preferences.shared.defaultNotificationsMode) return UserActivityManager.checkNotificationsActivity(mode: Preferences.shared.defaultNotificationsMode)
case .tab(.compose): case .tab(.compose):
return UserActivityManager.newPostActivity(accountID: id) return UserActivityManager.newPostActivity(accountID: id)
case .search: case .explore:
return UserActivityManager.searchActivity() return UserActivityManager.searchActivity()
case .bookmarks: case .bookmarks:
return UserActivityManager.bookmarksActivity() return UserActivityManager.bookmarksActivity()
@ -384,8 +382,8 @@ extension MainSidebarViewController {
} }
enum Item: Hashable { enum Item: Hashable {
case tab(MainTabBarViewController.Tab) case tab(MainTabBarViewController.Tab)
case search, bookmarks case explore, bookmarks
case discoverHeader, trendingStatuses, trendingTags, trendingLinks, profileDirectory case discoverHeader, trendingStatuses, profileDirectory
case listsHeader, list(List), addList case listsHeader, list(List), addList
case savedHashtagsHeader, savedHashtag(Hashtag), addSavedHashtag case savedHashtagsHeader, savedHashtag(Hashtag), addSavedHashtag
case savedInstancesHeader, savedInstance(URL), addSavedInstance case savedInstancesHeader, savedInstance(URL), addSavedInstance
@ -394,18 +392,14 @@ extension MainSidebarViewController {
switch self { switch self {
case let .tab(tab): case let .tab(tab):
return tab.title return tab.title
case .search: case .explore:
return "Search" return "Explore"
case .bookmarks: case .bookmarks:
return "Bookmarks" return "Bookmarks"
case .discoverHeader: case .discoverHeader:
return "Discover" return "Discover"
case .trendingStatuses: case .trendingStatuses:
return "Trending Posts" return "Trending Posts"
case .trendingTags:
return "Trending Hashtags"
case .trendingLinks:
return "Trending Links"
case .profileDirectory: case .profileDirectory:
return "Profile Directory" return "Profile Directory"
case .listsHeader: case .listsHeader:
@ -433,16 +427,12 @@ extension MainSidebarViewController {
switch self { switch self {
case let .tab(tab): case let .tab(tab):
return tab.imageName return tab.imageName
case .search: case .explore:
return "magnifyingglass" return "magnifyingglass"
case .bookmarks: case .bookmarks:
return "bookmark" return "bookmark"
case .trendingStatuses: case .trendingStatuses:
return "doc.text.image" return "square.text.square"
case .trendingTags:
return "number"
case .trendingLinks:
return "link"
case .profileDirectory: case .profileDirectory:
return "person.2.fill" return "person.2.fill"
case .list(_): case .list(_):
@ -550,8 +540,7 @@ extension MainSidebarViewController: UICollectionViewDelegate {
} }
func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? { func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
guard #available(iOS 15.0, *), guard let item = dataSource.itemIdentifier(for: indexPath),
let item = dataSource.itemIdentifier(for: indexPath),
let activity = userActivityForItem(item) else { let activity = userActivityForItem(item) else {
return nil return nil
} }

View File

@ -20,8 +20,11 @@ class MainSplitViewController: UISplitViewController {
private var tabBarViewController: MainTabBarViewController! private var tabBarViewController: MainTabBarViewController!
private var secondaryNavController: UINavigationController! { // private var secondaryNavController: UINavigationController! {
viewController(for: .secondary) as? UINavigationController // viewController(for: .secondary) as? UINavigationController
// }
private var secondaryNavController: SplitNavigationController! {
viewController(for: .secondary) as? SplitNavigationController
} }
init(mastodonController: MastodonController) { init(mastodonController: MastodonController) {
@ -46,9 +49,10 @@ class MainSplitViewController: UISplitViewController {
setViewController(sidebar, for: .primary) setViewController(sidebar, for: .primary)
primaryBackgroundStyle = .sidebar primaryBackgroundStyle = .sidebar
let secondaryNav = EnhancedNavigationViewController() // let secondaryNav = EnhancedNavigationViewController()
secondaryNav.useBrowserStyleNavigation = true // secondaryNav.useBrowserStyleNavigation = true
setViewController(secondaryNav, for: .secondary) let splitNav = SplitNavigationController()
setViewController(splitNav, for: .secondary)
// don't unnecesarily construct a content VC unless the we're in actually split mode // don't unnecesarily construct a content VC unless the we're in actually split mode
// when we change from compact -> split for the first time, the VC will be transferred anyways // when we change from compact -> split for the first time, the VC will be transferred anyways
if traitCollection.horizontalSizeClass != .compact { if traitCollection.horizontalSizeClass != .compact {
@ -100,7 +104,7 @@ class MainSplitViewController: UISplitViewController {
item = .tab(MainTabBarViewController.Tab(rawValue: index)!) item = .tab(MainTabBarViewController.Tab(rawValue: index)!)
} else if let str = command.propertyList as? String { } else if let str = command.propertyList as? String {
if str == "search" { if str == "search" {
item = .search item = .explore
} else if str == "bookmarks" { } else if str == "bookmarks" {
item = .bookmarks item = .bookmarks
} else { } else {
@ -171,7 +175,7 @@ extension MainSplitViewController: UISplitViewControllerDelegate {
$0.1 > $1.1 $0.1 > $1.1
} }
if let mostRecentExploreItem = mostRecentExploreItem?.0, if let mostRecentExploreItem = mostRecentExploreItem?.0,
mostRecentExploreItem != .search { mostRecentExploreItem != .explore {
let exploreNav = tabBarViewController.viewController(for: .explore) as! UINavigationController let exploreNav = tabBarViewController.viewController(for: .explore) as! UINavigationController
// Pop back to root, so we're appending to the Explore VC instead of some other VC // Pop back to root, so we're appending to the Explore VC instead of some other VC
exploreNav.popToRootViewController(animated: false) exploreNav.popToRootViewController(animated: false)
@ -188,7 +192,7 @@ extension MainSplitViewController: UISplitViewControllerDelegate {
// sidebar items that map 1 <-> 1 can be transferred directly // sidebar items that map 1 <-> 1 can be transferred directly
tabBarViewController.select(tab: tab) tabBarViewController.select(tab: tab)
case .search: case .explore:
// Search sidebar item maps to the Explore tab with the search controller/results visible // Search sidebar item maps to the Explore tab with the search controller/results visible
// The nav stack can't be copied directly, since the split VC uses a different SearchViewController // The nav stack can't be copied directly, since the split VC uses a different SearchViewController
// so that explore items aren't shown multiple times. // so that explore items aren't shown multiple times.
@ -217,11 +221,11 @@ extension MainSplitViewController: UISplitViewControllerDelegate {
explore.resultsController.loadResults(from: search.resultsController) explore.resultsController.loadResults(from: search.resultsController)
// Transfer the navigation stack, dropping the search VC, to keep anything the user has opened // Transfer the navigation stack, dropping the search VC, to keep anything the user has opened
transferNavigationStack(from: .search, to: exploreNav, dropFirst: true, append: true) transferNavigationStack(from: .explore, to: exploreNav, dropFirst: true, append: true)
tabBarViewController.select(tab: .explore) tabBarViewController.select(tab: .explore)
case .bookmarks, .trendingStatuses, .trendingTags, .trendingLinks, .profileDirectory, .list(_), .savedHashtag(_), .savedInstance(_): case .bookmarks, .trendingStatuses, .profileDirectory, .list(_), .savedHashtag(_), .savedInstance(_):
tabBarViewController.select(tab: .explore) tabBarViewController.select(tab: .explore)
// Make sure the Explore VC doesn't show it's search bar when it appears, in case the user was previously // Make sure the Explore VC doesn't show it's search bar when it appears, in case the user was previously
// in compact mode and performing a search. // in compact mode and performing a search.
@ -272,7 +276,7 @@ extension MainSplitViewController: UISplitViewControllerDelegate {
// For other items, the 2nd VC in the nav stack determines which sidebar item they map to. // For other items, the 2nd VC in the nav stack determines which sidebar item they map to.
// Search screen has special considerations, all others can be transferred directly. // Search screen has special considerations, all others can be transferred directly.
if tabNavigationStack.count == 1 || ((tabNavigationStack.first as? ExploreViewController)?.searchController?.isActive ?? false) { if tabNavigationStack.count == 1 || ((tabNavigationStack.first as? ExploreViewController)?.searchController?.isActive ?? false) {
exploreItem = .search exploreItem = .explore
let searchVC = SearchViewController(mastodonController: mastodonController) let searchVC = SearchViewController(mastodonController: mastodonController)
searchVC.loadViewIfNeeded() searchVC.loadViewIfNeeded()
let explore = tabNavigationStack.first as! ExploreViewController let explore = tabNavigationStack.first as! ExploreViewController
@ -300,9 +304,9 @@ extension MainSplitViewController: UISplitViewControllerDelegate {
case is TrendingStatusesViewController: case is TrendingStatusesViewController:
exploreItem = .trendingStatuses exploreItem = .trendingStatuses
case is TrendingHashtagsViewController: case is TrendingHashtagsViewController:
exploreItem = .trendingTags exploreItem = .explore
case is TrendingLinksViewController: case is TrendingLinksViewController:
exploreItem = .trendingLinks exploreItem = .explore
case is ProfileDirectoryViewController: case is ProfileDirectoryViewController:
exploreItem = .profileDirectory exploreItem = .profileDirectory
default: default:
@ -354,16 +358,12 @@ fileprivate extension MainSidebarViewController.Item {
switch self { switch self {
case let .tab(tab): case let .tab(tab):
return tab.createViewController(mastodonController) return tab.createViewController(mastodonController)
case .search: case .explore:
return SearchViewController(mastodonController: mastodonController) return SearchViewController(mastodonController: mastodonController)
case .bookmarks: case .bookmarks:
return BookmarksTableViewController(mastodonController: mastodonController) return BookmarksTableViewController(mastodonController: mastodonController)
case .trendingStatuses: case .trendingStatuses:
return TrendingStatusesViewController(mastodonController: mastodonController) return TrendingStatusesViewController(mastodonController: mastodonController)
case .trendingTags:
return TrendingHashtagsViewController(mastodonController: mastodonController)
case .trendingLinks:
return TrendingLinksViewController(mastodonController: mastodonController)
case .profileDirectory: case .profileDirectory:
return ProfileDirectoryViewController(mastodonController: mastodonController) return ProfileDirectoryViewController(mastodonController: mastodonController)
case let .list(list): case let .list(list):
@ -380,7 +380,7 @@ fileprivate extension MainSidebarViewController.Item {
extension MainSplitViewController: TuskerRootViewController { extension MainSplitViewController: TuskerRootViewController {
@objc func presentCompose() { @objc func presentCompose() {
if #available(iOS 15.0, *), UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac { if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
let compose = UserActivityManager.newPostActivity(mentioning: nil, accountID: mastodonController.accountInfo!.id) let compose = UserActivityManager.newPostActivity(mentioning: nil, accountID: mastodonController.accountInfo!.id)
let options = UIWindowScene.ActivationRequestOptions() let options = UIWindowScene.ActivationRequestOptions()
options.preferredPresentationStyle = .prominent options.preferredPresentationStyle = .prominent
@ -435,8 +435,8 @@ extension MainSplitViewController: TuskerRootViewController {
return return
} }
if sidebar.selectedItem != .search { if sidebar.selectedItem != .explore {
select(item: .search) select(item: .explore)
} }
guard let searchViewController = secondaryNavController.viewControllers.first as? SearchViewController else { guard let searchViewController = secondaryNavController.viewControllers.first as? SearchViewController else {

View File

@ -142,7 +142,7 @@ class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate {
return vc return vc
} else { } else {
let nav = EnhancedNavigationViewController(rootViewController: vc) let nav = EnhancedNavigationViewController(rootViewController: vc)
nav.useBrowserStyleNavigation = true // nav.useBrowserStyleNavigation = true
return nav return nav
} }
} }
@ -230,7 +230,7 @@ extension MainTabBarViewController: FastAccountSwitcherViewControllerDelegate {
extension MainTabBarViewController: TuskerRootViewController { extension MainTabBarViewController: TuskerRootViewController {
@objc func presentCompose() { @objc func presentCompose() {
if #available(iOS 15.0, *), UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac { if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
let compose = UserActivityManager.newPostActivity(mentioning: nil, accountID: mastodonController.accountInfo!.id) let compose = UserActivityManager.newPostActivity(mentioning: nil, accountID: mastodonController.accountInfo!.id)
let options = UIWindowScene.ActivationRequestOptions() let options = UIWindowScene.ActivationRequestOptions()
options.preferredPresentationStyle = .prominent options.preferredPresentationStyle = .prominent

View File

@ -38,7 +38,7 @@ struct OppositeCollapseKeywordsView: View {
FocusableTextField(placeholder: "Add Keyword", text: $valueToAdd, becomeFirstResponder: $makeAddFieldFirstResponder, onCommit: self.addKeyword) FocusableTextField(placeholder: "Add Keyword", text: $valueToAdd, becomeFirstResponder: $makeAddFieldFirstResponder, onCommit: self.addKeyword)
} }
} }
.animation(.default) .animation(.default, value: keywords.map(\.id))
.listStyle(GroupedListStyle()) .listStyle(GroupedListStyle())
} }
.onAppear(perform: updateAppearance) .onAppear(perform: updateAppearance)

View File

@ -16,9 +16,7 @@ struct WellnessPrefsView: View {
showFavAndReblogCount showFavAndReblogCount
notificationsMode notificationsMode
grayscaleImages grayscaleImages
if #available(iOS 15.0, *) { disableInfiniteScrolling
disableInfiniteScrolling
}
hideDiscover hideDiscover
} }
.listStyle(InsetGroupedListStyle()) .listStyle(InsetGroupedListStyle())

View File

@ -7,11 +7,16 @@
// //
import UIKit import UIKit
import Pachyderm
import SafariServices
class SearchViewController: UIViewController { class SearchViewController: UIViewController {
weak var mastodonController: MastodonController! weak var mastodonController: MastodonController!
private var collectionView: UICollectionView!
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
var resultsController: SearchResultsViewController! var resultsController: SearchResultsViewController!
var searchController: UISearchController! var searchController: UISearchController!
@ -22,7 +27,7 @@ class SearchViewController: UIViewController {
super.init(nibName: nil, bundle: nil) super.init(nibName: nil, bundle: nil)
title = NSLocalizedString("Search", comment: "search tab title") title = NSLocalizedString("Explore", comment: "explore tab title")
} }
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
@ -32,12 +37,46 @@ class SearchViewController: UIViewController {
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
view.backgroundColor = .systemBackground let layout = UICollectionViewCompositionalLayout { [unowned self] sectionIndex, environment in
let sectionIdentifier = self.dataSource.snapshot().sectionIdentifiers[sectionIndex]
switch sectionIdentifier {
case .trendingHashtags:
var listConfig = UICollectionLayoutListConfiguration(appearance: .grouped)
listConfig.headerMode = .supplementary
return .list(using: listConfig, layoutEnvironment: environment)
case .trendingLinks:
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
// todo: i really wish i could just say the height is automatic and let autolayout figure out what it needs to be
// using .estimated(whatever) constrains the height to exactly whatever
let groupSize = NSCollectionLayoutSize(widthDimension: .absolute(250), heightDimension: .estimated(280))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
group.edgeSpacing = NSCollectionLayoutEdgeSpacing(leading: .fixed(8), top: nil, trailing: .fixed(8), bottom: nil)
let section = NSCollectionLayoutSection(group: group)
section.orthogonalScrollingBehavior = .continuousGroupLeadingBoundary
section.boundarySupplementaryItems = [
NSCollectionLayoutBoundarySupplementaryItem(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(12)), elementKind: UICollectionView.elementKindSectionHeader, alignment: .topLeading)
]
return section
default:
fatalError("unimplemented")
}
}
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
collectionView.delegate = self
collectionView.dragDelegate = self
collectionView.backgroundColor = .secondarySystemBackground
view.addSubview(collectionView)
dataSource = createDataSource()
resultsController = SearchResultsViewController(mastodonController: mastodonController) resultsController = SearchResultsViewController(mastodonController: mastodonController)
resultsController.exploreNavigationController = self.navigationController resultsController.exploreNavigationController = self.navigationController
searchController = UISearchController(searchResultsController: resultsController) searchController = UISearchController(searchResultsController: resultsController)
searchController.obscuresBackgroundDuringPresentation = false searchController.obscuresBackgroundDuringPresentation = true
searchController.searchBar.autocapitalizationType = .none searchController.searchBar.autocapitalizationType = .none
searchController.searchBar.delegate = resultsController searchController.searchBar.delegate = resultsController
searchController.hidesNavigationBarDuringPresentation = false searchController.hidesNavigationBarDuringPresentation = false
@ -48,6 +87,18 @@ class SearchViewController: UIViewController {
if #available(iOS 16.0, *) { if #available(iOS 16.0, *) {
navigationItem.preferredSearchBarPlacement = .stacked navigationItem.preferredSearchBarPlacement = .stacked
} }
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
Task(priority: .userInitiated) {
if (try? await mastodonController.getOwnInstance()) != nil {
await applySnapshot()
}
}
} }
override func viewDidAppear(_ animated: Bool) { override func viewDidAppear(_ animated: Bool) {
@ -62,4 +113,213 @@ class SearchViewController: UIViewController {
} }
} }
private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> {
let sectionHeaderCell = UICollectionView.SupplementaryRegistration<UICollectionViewListCell>(elementKind: UICollectionView.elementKindSectionHeader) { [unowned self] (headerView, collectionView, indexPath) in
let section = self.dataSource.snapshot().sectionIdentifiers[indexPath.section]
var config = UIListContentConfiguration.groupedHeader()
config.text = section.title
headerView.contentConfiguration = config
}
let trendingHashtagCell = UICollectionView.CellRegistration<TrendingHashtagCollectionViewCell, Hashtag> { (cell, indexPath, hashtag) in
cell.updateUI(hashtag: hashtag)
}
let trendingLinkCell = UICollectionView.CellRegistration<TrendingLinkCardCollectionViewCell, Card>(cellNib: UINib(nibName: "TrendingLinkCardCollectionViewCell", bundle: .main)) { (cell, indexPath, card) in
cell.updateUI(card: card)
}
let dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) { collectionView, indexPath, item in
switch item {
case let .tag(hashtag):
return collectionView.dequeueConfiguredReusableCell(using: trendingHashtagCell, for: indexPath, item: hashtag)
case let .link(card):
return collectionView.dequeueConfiguredReusableCell(using: trendingLinkCell, for: indexPath, item: card)
default:
fatalError("todo")
}
}
dataSource.supplementaryViewProvider = { (collectionView, elementKind, indexPath) in
if elementKind == UICollectionView.elementKindSectionHeader {
return collectionView.dequeueConfiguredReusableSupplementary(using: sectionHeaderCell, for: indexPath)
} else {
return nil
}
}
return dataSource
}
@MainActor
private func applySnapshot() async {
guard mastodonController.instanceFeatures.trends,
!Preferences.shared.hideDiscover else {
await dataSource.apply(NSDiffableDataSourceSnapshot())
return
}
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
let hashtagsReq = Client.getTrendingHashtags(limit: 5)
async let hashtags = try? mastodonController.run(hashtagsReq).0
let linksReq = Client.getTrendingLinks(limit: 10)
async let links = try? mastodonController.run(linksReq).0
if let hashtags = await hashtags {
snapshot.appendSections([.trendingHashtags])
snapshot.appendItems(hashtags.map { .tag($0) }, toSection: .trendingHashtags)
}
if let links = await links {
snapshot.appendSections([.trendingLinks])
snapshot.appendItems(links.map { .link($0) }, toSection: .trendingLinks)
}
await dataSource.apply(snapshot)
}
@objc private func preferencesChanged() {
Task {
await applySnapshot()
}
}
}
extension SearchViewController {
enum Section {
case trendingHashtags
case trendingLinks
case trendingStatuses
case profileSuggestions
var title: String {
switch self {
case .trendingHashtags:
return "Trending Hashtags"
case .trendingLinks:
return "Trending Links"
case .trendingStatuses:
return "Trending Statuses"
case .profileSuggestions:
return "Suggested Accounts"
}
}
}
enum Item: Equatable, Hashable {
case status(String)
case tag(Hashtag)
case link(Card)
static func == (lhs: SearchViewController.Item, rhs: SearchViewController.Item) -> Bool {
switch (lhs, rhs) {
case let (.status(a), .status(b)):
return a == b
case let (.tag(a), .tag(b)):
return a == b
case let (.link(a), .link(b)):
return a.url == b.url
default:
return false
}
}
func hash(into hasher: inout Hasher) {
switch self {
case let .status(id):
hasher.combine("status")
hasher.combine(id)
case let .tag(tag):
hasher.combine("tag")
hasher.combine(tag.name)
case let .link(card):
hasher.combine("link")
hasher.combine(card.url)
}
}
}
}
extension SearchViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
guard let item = dataSource.itemIdentifier(for: indexPath) else {
return
}
switch item {
case let .tag(hashtag):
show(HashtagTimelineViewController(for: hashtag, mastodonController: mastodonController), sender: nil)
case let .link(card):
if let url = URL(card.url) {
selected(url: url)
}
default:
fatalError("todo")
}
}
func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
guard let item = dataSource.itemIdentifier(for: indexPath) else {
return nil
}
switch item {
case let .tag(hashtag):
return UIContextMenuConfiguration(identifier: nil) {
HashtagTimelineViewController(for: hashtag, mastodonController: self.mastodonController)
} actionProvider: { (_) in
UIMenu(children: self.actionsForHashtag(hashtag, sourceView: self.collectionView.cellForItem(at: indexPath)))
}
case let .link(card):
guard let url = URL(card.url) else {
return nil
}
return UIContextMenuConfiguration {
SFSafariViewController(url: url)
} actionProvider: { _ in
UIMenu(children: self.actionsForTrendingLink(card: card))
}
default:
fatalError("todo")
}
}
}
extension SearchViewController: UICollectionViewDragDelegate {
func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
guard let item = dataSource.itemIdentifier(for: indexPath) else {
return []
}
switch item {
case let .tag(hashtag):
let provider = NSItemProvider(object: hashtag.url as NSURL)
if let activity = UserActivityManager.showTimelineActivity(timeline: .tag(hashtag: hashtag.name), accountID: mastodonController.accountInfo!.id) {
activity.displaysAuxiliaryScene = true
provider.registerObject(activity, visibility: .all)
}
return [UIDragItem(itemProvider: provider)]
case let .link(card):
guard let url = URL(card.url) else {
return []
}
return [UIDragItem(itemProvider: NSItemProvider(object: url as NSURL))]
default:
fatalError("todo")
}
}
}
extension SearchViewController: TuskerNavigationDelegate {
var apiController: MastodonController { mastodonController }
}
extension SearchViewController: ToastableViewController {
}
extension SearchViewController: MenuActionProvider {
} }

View File

@ -164,8 +164,7 @@ class TimelineTableViewController: DiffableTimelineLikeTableViewController<Timel
return return
} }
if #available(iOS 15.0, *), if Preferences.shared.disableInfiniteScrolling && !didConfirmLoadMore {
Preferences.shared.disableInfiniteScrolling && !didConfirmLoadMore {
var snapshot = currentSnapshot() var snapshot = currentSnapshot()
guard !snapshot.itemIdentifiers(inSection: .footer).contains(.confirmLoadMore) else { guard !snapshot.itemIdentifiers(inSection: .footer).contains(.confirmLoadMore) else {
// todo: need something more accurate than "success"/"failure" // todo: need something more accurate than "success"/"failure"

View File

@ -59,7 +59,7 @@ extension MenuActionProvider {
draft.visibility = .direct draft.visibility = .direct
self.navigationDelegate?.compose(editing: draft) self.navigationDelegate?.compose(editing: draft)
}), }),
UIDeferredMenuElement.uncachedIfPossible({ (elementHandler) in UIDeferredMenuElement.uncached({ (elementHandler) in
Task { @MainActor in Task { @MainActor in
if let action = await self.followAction(for: accountID, mastodonController: mastodonController) { if let action = await self.followAction(for: accountID, mastodonController: mastodonController) {
elementHandler([action]) elementHandler([action])
@ -358,21 +358,13 @@ extension MenuActionProvider {
} }
private func addOpenInNewWindow(actions: inout [UIAction], activity: @escaping @autoclosure () -> NSUserActivity) { private func addOpenInNewWindow(actions: inout [UIAction], activity: @escaping @autoclosure () -> NSUserActivity) {
if #available(iOS 15.0, *) { let options = UIWindowScene.ActivationRequestOptions()
let options = UIWindowScene.ActivationRequestOptions() options.preferredPresentationStyle = .automatic
options.preferredPresentationStyle = .automatic actions.append(UIWindowScene.ActivationAction { (_) in
actions.append(UIWindowScene.ActivationAction { (_) in let activity = activity()
let activity = activity() activity.displaysAuxiliaryScene = true
activity.displaysAuxiliaryScene = true return .init(userActivity: activity, options: options, preview: nil)
return .init(userActivity: activity, options: options, preview: nil) })
})
} else if UIApplication.shared.supportsMultipleScenes {
actions.append(createAction(identifier: "new_window", title: "Open in New Window", systemImageName: "rectangle.badge.plus", handler: { (_) in
let activity = activity()
activity.displaysAuxiliaryScene = true
UIApplication.shared.requestSceneSessionActivation(nil, userActivity: activity, options: nil, errorHandler: nil)
}))
}
} }
private func followAction(for accountID: String, mastodonController: MastodonController) async -> UIMenuElement? { private func followAction(for accountID: String, mastodonController: MastodonController) async -> UIMenuElement? {
@ -423,13 +415,3 @@ extension SFSafariViewController: CustomPreviewPresenting {
presenter.present(self, animated: true) presenter.present(self, animated: true)
} }
} }
private extension UIDeferredMenuElement {
static func uncachedIfPossible(_ elementProvider: @escaping (@escaping ([UIMenuElement]) -> Void) -> Void) -> UIDeferredMenuElement {
if #available(iOS 15.0, *) {
return UIDeferredMenuElement.uncached(elementProvider)
} else {
return UIDeferredMenuElement(elementProvider)
}
}
}

View File

@ -0,0 +1,262 @@
//
// SplitNavigationController.swift
// Tusker
//
// Created by Shadowfacts on 7/1/22.
// Copyright © 2022 Shadowfacts. All rights reserved.
//
import UIKit
class SplitNavigationController: UIViewController {
private let rootNav = SplitRootNavigationController()
private let secondaryNav = SplitSecondaryNavigationController()
private let separatorView = UIView()
private var constraints: [NSLayoutConstraint] = []
var viewControllers: [UIViewController] {
get {
return rootNav.viewControllers + secondaryNav.viewControllers
}
set {
if newValue.isEmpty {
rootNav.viewControllers = []
secondaryNav.viewControllers = []
} else if canShowSecondaryNav {
var newValue = newValue
rootNav.viewControllers = [newValue.removeFirst()]
secondaryNav.viewControllers = newValue
} else {
rootNav.viewControllers = newValue
secondaryNav.viewControllers = []
}
updateSecondaryNavVisibility()
}
}
/// This property is only valid after the view has been laid out.
private var canShowSecondaryNav: Bool {
// minimum of 360pt for each column
// this allows split navigation on all ipads in portrait w/ sidebar hidden and in landscape (regardless of sidebar)
(viewIfLoaded?.bounds.width ?? 0) >= 720
}
init(rootViewController: UIViewController? = nil) {
super.init(nibName: nil, bundle: nil)
rootNav.showImpl = { [unowned self] vc, sender in
if self.canShowSecondaryNav {
self.setSecondaryViewControllers([vc], animated: true)
// the split nav shouldn't really be reaching down into the inner VCs like this,
// but I can't think of a cleaner way
if let tableVC = sender as? UITableViewController,
let selectedIndexPath = tableVC.tableView.indexPathForSelectedRow {
tableVC.tableView.deselectRow(at: selectedIndexPath, animated: true)
}
} else {
self.rootNav.pushViewController(vc, animated: true)
}
}
secondaryNav.closeSecondaryImpl = { [unowned self] in
self.popToRootViewController(animated: true)
}
if let rootViewController {
rootNav.viewControllers = [rootViewController]
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
embedChild(rootNav, layout: false)
embedChild(secondaryNav, layout: false)
rootNav.view.translatesAutoresizingMaskIntoConstraints = false
secondaryNav.view.translatesAutoresizingMaskIntoConstraints = false
separatorView.backgroundColor = .separator
separatorView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(separatorView)
NSLayoutConstraint.activate([
rootNav.view.topAnchor.constraint(equalTo: view.topAnchor),
rootNav.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
rootNav.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
separatorView.topAnchor.constraint(equalTo: view.topAnchor),
separatorView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
separatorView.leadingAnchor.constraint(equalTo: rootNav.view.trailingAnchor),
separatorView.widthAnchor.constraint(equalToConstant: 0.5),
secondaryNav.view.topAnchor.constraint(equalTo: view.topAnchor),
secondaryNav.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
secondaryNav.view.leadingAnchor.constraint(equalTo: separatorView.trailingAnchor),
])
updateSecondaryNavVisibility()
}
override func show(_ vc: UIViewController, sender: Any?) {
if !canShowSecondaryNav {
rootNav.pushViewController(vc, animated: true)
} else if rootNav.viewControllers.isEmpty {
rootNav.pushViewController(vc, animated: false)
} else {
secondaryNav.pushViewController(vc, animated: true)
}
updateSecondaryNavVisibility()
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
}
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
if !isLayingOutForAnimation {
updateSecondaryNavVisibility()
}
}
private func updateSecondaryNavVisibility() {
guard isViewLoaded else {
return
}
if canShowSecondaryNav {
if rootNav.viewControllers.count > 1 {
var vcs = rootNav.viewControllers
let root = vcs.removeFirst()
rootNav.viewControllers = [root]
// this shouldn't be necessary since the vcs are removed from their parent vc by setting rootNav.viewControllers
// but it doesn't remove the views from their superview (until the next runloop iteration?)
// so we need to do that ourselves before we can set them on the secondary nav (otherwise it raises an exception)
vcs.forEach { $0.removeViewAndController() }
secondaryNav.viewControllers = vcs
}
} else {
if !secondaryNav.viewControllers.isEmpty {
let firstSecondary = secondaryNav.viewControllers.first!
// remove the left bar button item so that the builtin Back item shows
if firstSecondary.navigationItem.leftBarButtonItem?.tag == ViewTags.splitNavCloseSecondaryButton {
firstSecondary.navigationItem.leftBarButtonItem = nil
}
rootNav.viewControllers.append(contentsOf: secondaryNav.viewControllers)
secondaryNav.viewControllers = []
}
}
setSecondaryVisible(canShowSecondaryNav && !secondaryNav.viewControllers.isEmpty)
}
private func setSecondaryVisible(_ visible: Bool) {
guard isViewLoaded else {
return
}
NSLayoutConstraint.deactivate(constraints)
if visible {
constraints = [
rootNav.view.trailingAnchor.constraint(equalTo: view.centerXAnchor),
secondaryNav.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
]
} else {
constraints = [
rootNav.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
secondaryNav.view.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.5),
]
}
NSLayoutConstraint.activate(constraints)
}
private func setSecondaryViewControllers(_ vcs: [UIViewController], animated: Bool) {
if animated {
if vcs.isEmpty {
popToRootViewController(animated: true)
} else {
let wasVisible = !secondaryNav.viewControllers.isEmpty
secondaryNav.viewControllers = vcs
secondaryNav.view.frame = CGRect(x: view.bounds.width, y: 0, width: view.bounds.width / 2, height: view.bounds.height)
secondaryNav.view.layoutIfNeeded()
if !wasVisible {
let animator = UIViewPropertyAnimator(duration: 0.35, curve: .easeInOut) {
self.updateSecondaryNavVisibility()
self.view.layoutIfNeeded()
}
animator.startAnimation()
}
}
} else {
secondaryNav.viewControllers = vcs
updateSecondaryNavVisibility()
}
}
private var isLayingOutForAnimation = false
func popToRootViewController(animated: Bool) {
if animated {
// we don't update secondaryNav.viewControllers until after the animation is completed
// otherwise the secondary nav's contents disappear immediately, rather than sliding off-screen
let animator = UIViewPropertyAnimator(duration: 0.35, curve: .easeInOut) {
self.isLayingOutForAnimation = true
self.setSecondaryVisible(false)
self.view.layoutIfNeeded()
}
animator.addCompletion { _ in
self.secondaryNav.viewControllers = []
self.isLayingOutForAnimation = false
// self.updateSecondaryNavVisibility()
}
animator.startAnimation()
} else {
self.secondaryNav.viewControllers = []
self.updateSecondaryNavVisibility()
}
}
}
private class SplitRootNavigationController: UINavigationController {
fileprivate var showImpl: ((UIViewController, Any?) -> Void)!
override func show(_ vc: UIViewController, sender: Any?) {
showImpl(vc, sender)
}
}
private class SplitSecondaryNavigationController: EnhancedNavigationViewController {
fileprivate var closeSecondaryImpl: (() -> Void)!
override var viewControllers: [UIViewController] {
didSet {
if let first = viewControllers.first {
configureSecondarySplitCloseButton(for: first)
}
}
}
private func configureSecondarySplitCloseButton(for viewController: UIViewController) {
guard viewController.navigationItem.leftBarButtonItem?.tag != ViewTags.splitNavCloseSecondaryButton else {
return
}
let item = UIBarButtonItem(title: "Close", style: .done, target: self, action: #selector(closeSecondary))
item.tag = ViewTags.splitNavCloseSecondaryButton
viewController.navigationItem.leftBarButtonItem = item
}
@objc private func closeSecondary() {
closeSecondaryImpl()
}
}

View File

@ -10,7 +10,7 @@ import UIKit
// Based on MVCTodo by Dave DeLong: https://github.com/davedelong/MVCTodo/blob/841649dd6aa31bacda3ad7ef9a9a836f66281e50/MVCTodo/Extensions/UIViewController.swift // Based on MVCTodo by Dave DeLong: https://github.com/davedelong/MVCTodo/blob/841649dd6aa31bacda3ad7ef9a9a836f66281e50/MVCTodo/Extensions/UIViewController.swift
extension UIViewController { extension UIViewController {
func embedChild(_ newChild: UIViewController, in container: UIView? = nil) { func embedChild(_ newChild: UIViewController, in container: UIView? = nil, layout: Bool = true) {
// if the view controller is already a child of something else, remove it // if the view controller is already a child of something else, remove it
if let oldParent = newChild.parent, oldParent != self { if let oldParent = newChild.parent, oldParent != self {
newChild.beginAppearanceTransition(false, animated: false) newChild.beginAppearanceTransition(false, animated: false)
@ -36,7 +36,7 @@ extension UIViewController {
newChild.beginAppearanceTransition(true, animated: false) newChild.beginAppearanceTransition(true, animated: false)
addChild(newChild) addChild(newChild)
newChild.didMove(toParent: self) newChild.didMove(toParent: self)
targetContainer.embedSubview(newChild.view) targetContainer.embedSubview(newChild.view, layout: layout)
newChild.endAppearanceTransition() newChild.endAppearanceTransition()
} else { } else {
// the view controller is already a child // the view controller is already a child
@ -45,7 +45,7 @@ extension UIViewController {
// we don't do the appearance transition stuff here, // we don't do the appearance transition stuff here,
// because the vc is already a child, so *presumably* // because the vc is already a child, so *presumably*
// that transition stuff has already appened // that transition stuff has already appened
targetContainer.embedSubview(newChild.view) targetContainer.embedSubview(newChild.view, layout: layout)
} }
} }
@ -57,22 +57,25 @@ extension UIViewController {
// Based on MVCTodo by Dave DeLong: https://github.com/davedelong/MVCTodo/blob/841649dd6aa31bacda3ad7ef9a9a836f66281e50/MVCTodo/Extensions/UIView.swift // Based on MVCTodo by Dave DeLong: https://github.com/davedelong/MVCTodo/blob/841649dd6aa31bacda3ad7ef9a9a836f66281e50/MVCTodo/Extensions/UIView.swift
extension UIView { extension UIView {
func embedSubview(_ subview: UIView) { func embedSubview(_ subview: UIView, layout: Bool = true) {
if subview.superview == self { return } if subview.superview == self { return }
if subview.superview != nil { if subview.superview != nil {
subview.removeFromSuperview() subview.removeFromSuperview()
} }
subview.frame = bounds
addSubview(subview) addSubview(subview)
NSLayoutConstraint.activate([ if layout {
subview.leadingAnchor.constraint(equalTo: leadingAnchor), subview.frame = bounds
subview.trailingAnchor.constraint(equalTo: trailingAnchor),
subview.topAnchor.constraint(equalTo: topAnchor), NSLayoutConstraint.activate([
subview.bottomAnchor.constraint(equalTo: bottomAnchor) subview.leadingAnchor.constraint(equalTo: leadingAnchor),
subview.trailingAnchor.constraint(equalTo: trailingAnchor),
subview.topAnchor.constraint(equalTo: topAnchor),
subview.bottomAnchor.constraint(equalTo: bottomAnchor)
]) ])
}
} }
func isContainedWithin(_ other: UIView) -> Bool { func isContainedWithin(_ other: UIView) -> Bool {

View File

@ -89,7 +89,7 @@ extension TuskerNavigationDelegate {
} }
func compose(editing draft: Draft) { func compose(editing draft: Draft) {
if #available(iOS 15.0, *), UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac { if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
let compose = UserActivityManager.editDraftActivity(id: draft.id, accountID: apiController.accountInfo!.id) let compose = UserActivityManager.editDraftActivity(id: draft.id, accountID: apiController.accountInfo!.id)
let options = UIWindowScene.ActivationRequestOptions() let options = UIWindowScene.ActivationRequestOptions()
options.preferredPresentationStyle = .prominent options.preferredPresentationStyle = .prominent

View File

@ -16,4 +16,5 @@ struct ViewTags {
static let navBackBarButton = 42003 static let navBackBarButton = 42003
static let navForwardBarButton = 42004 static let navForwardBarButton = 42004
static let navEmptyTitleView = 42005 static let navEmptyTitleView = 42005
static let splitNavCloseSecondaryButton = 42006
} }

View File

@ -23,15 +23,13 @@ class ConfirmLoadMoreTableViewCell: UITableViewCell {
override func awakeFromNib() { override func awakeFromNib() {
super.awakeFromNib() super.awakeFromNib()
if #available(iOS 15.0, *) { var config = UIButton.Configuration.tinted()
var config = UIButton.Configuration.tinted() config.title = "Load More"
config.title = "Load More" config.showsActivityIndicator = false
config.showsActivityIndicator = false config.imagePadding = 4
config.imagePadding = 4 confirmButton.configuration = config
confirmButton.configuration = config confirmButton.configurationUpdateHandler = { [unowned self] button in
confirmButton.configurationUpdateHandler = { [unowned self] button in button.configuration?.showsActivityIndicator = self.isLoading
button.configuration?.showsActivityIndicator = self.isLoading
}
} }
} }
@ -39,17 +37,13 @@ class ConfirmLoadMoreTableViewCell: UITableViewCell {
super.prepareForReuse() super.prepareForReuse()
isLoading = false isLoading = false
if #available(iOS 15.0, *) { confirmButton.setNeedsUpdateConfiguration()
confirmButton.setNeedsUpdateConfiguration()
}
} }
@IBAction func loadMorePressed(_ sender: Any) { @IBAction func loadMorePressed(_ sender: Any) {
confirmLoadMore?() confirmLoadMore?()
if #available(iOS 15.0, *) { isLoading = true
isLoading = true confirmButton.setNeedsUpdateConfiguration()
confirmButton.setNeedsUpdateConfiguration()
}
} }
} }

View File

@ -0,0 +1,80 @@
//
// TrendingHashtagCollectionViewCell.swift
// Tusker
//
// Created by Shadowfacts on 6/29/22.
// Copyright © 2022 Shadowfacts. All rights reserved.
//
import UIKit
import Pachyderm
class TrendingHashtagCollectionViewCell: UICollectionViewCell {
private let hashtagLabel = UILabel()
private let peopleTodayLabel = UILabel()
private let historyView = TrendHistoryView()
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .systemBackground
hashtagLabel.font = .preferredFont(forTextStyle: .title2)
peopleTodayLabel.font = .preferredFont(forTextStyle: .caption1)
let vStack = UIStackView(arrangedSubviews: [
hashtagLabel,
peopleTodayLabel,
])
vStack.axis = .vertical
vStack.alignment = .fill
vStack.distribution = .fill
vStack.spacing = 0
let hStack = UIStackView(arrangedSubviews: [
vStack,
historyView,
])
hStack.axis = .horizontal
hStack.alignment = .center
hStack.distribution = .fill
hStack.spacing = 8
hStack.translatesAutoresizingMaskIntoConstraints = false
addSubview(hStack)
NSLayoutConstraint.activate([
hStack.leadingAnchor.constraint(equalToSystemSpacingAfter: leadingAnchor, multiplier: 1),
trailingAnchor.constraint(equalToSystemSpacingAfter: hStack.trailingAnchor, multiplier: 1),
hStack.topAnchor.constraint(equalTo: topAnchor, constant: 8),
hStack.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8),
historyView.widthAnchor.constraint(equalToConstant: 100),
historyView.heightAnchor.constraint(equalToConstant: 44),
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func updateUI(hashtag: Hashtag) {
hashtagLabel.text = "#\(hashtag.name)"
historyView.setHistory(hashtag.history)
historyView.isHidden = hashtag.history == nil || hashtag.history!.count < 2
if let history = hashtag.history {
let sorted = history.sorted(by: { $0.day < $1.day })
let lastTwo = sorted[(sorted.count - 2)...]
let accounts = lastTwo.map(\.accounts).reduce(0, +)
let uses = lastTwo.map(\.uses).reduce(0, +)
let format = NSLocalizedString("trending hashtag info", comment: "trending hashtag posts and people")
peopleTodayLabel.text = String.localizedStringWithFormat(format, accounts, uses)
peopleTodayLabel.isHidden = false
} else {
peopleTodayLabel.isHidden = true
}
}
}

View File

@ -1,41 +0,0 @@
//
// TrendingHashtagTableViewCell.swift
// Tusker
//
// Created by Shadowfacts on 1/24/21.
// Copyright © 2021 Shadowfacts. All rights reserved.
//
import UIKit
import Pachyderm
class TrendingHashtagTableViewCell: UITableViewCell {
@IBOutlet weak var hashtagLabel: UILabel!
@IBOutlet weak var peopleTodayLabel: UILabel!
@IBOutlet weak var historyView: TrendHistoryView!
override func awakeFromNib() {
super.awakeFromNib()
}
func updateUI(hashtag: Hashtag) {
hashtagLabel.text = "#\(hashtag.name)"
historyView.setHistory(hashtag.history)
historyView.isHidden = hashtag.history == nil || hashtag.history!.count < 2
if let history = hashtag.history {
let sorted = history.sorted(by: { $0.day < $1.day })
let lastTwo = sorted[(sorted.count - 2)...]
let accounts = lastTwo.map(\.accounts).reduce(0, +)
let uses = lastTwo.map(\.uses).reduce(0, +)
let format = NSLocalizedString("trending hashtag info", comment: "trending hashtag posts and people")
peopleTodayLabel.text = String.localizedStringWithFormat(format, accounts, uses)
peopleTodayLabel.isHidden = false
} else {
peopleTodayLabel.isHidden = true
}
}
}

View File

@ -1,67 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="20037" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="20020"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<tableViewCell contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" id="KGk-i7-Jjw" customClass="TrendingHashtagTableViewCell" customModule="Tusker" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="320" height="66"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="KGk-i7-Jjw" id="H2p-sc-9uM">
<rect key="frame" x="0.0" y="0.0" width="320" height="66"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="tEP-en-vHK">
<rect key="frame" x="16" y="0.0" width="288" height="66"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="iCc-do-llt">
<rect key="frame" x="0.0" y="15" width="180" height="36.5"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="#hashtag" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="SIS-9e-Paj">
<rect key="frame" x="0.0" y="0.0" width="180" height="23"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleTitle2"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="6 people today" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Kc5-BL-bmu">
<rect key="frame" x="0.0" y="23" width="180" height="13.5"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleCaption1"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</stackView>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Xrw-2v-ybZ" customClass="TrendHistoryView" customModule="Tusker" customModuleProvider="target">
<rect key="frame" x="188" y="11" width="100" height="44"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="height" constant="44" id="W4C-uw-zWg"/>
<constraint firstAttribute="width" constant="100" id="XHb-vd-qNk"/>
</constraints>
</view>
</subviews>
</stackView>
</subviews>
<constraints>
<constraint firstItem="tEP-en-vHK" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="top" id="3Qd-rF-nGk"/>
<constraint firstAttribute="trailingMargin" secondItem="tEP-en-vHK" secondAttribute="trailing" id="Ws6-oZ-9Es"/>
<constraint firstItem="tEP-en-vHK" firstAttribute="leading" secondItem="H2p-sc-9uM" secondAttribute="leadingMargin" id="if4-Ea-awg"/>
<constraint firstAttribute="bottom" secondItem="tEP-en-vHK" secondAttribute="bottom" id="nTV-Ih-vTj"/>
</constraints>
</tableViewCellContentView>
<viewLayoutGuide key="safeArea" id="njF-e1-oar"/>
<connections>
<outlet property="hashtagLabel" destination="SIS-9e-Paj" id="1UK-Va-3rL"/>
<outlet property="historyView" destination="Xrw-2v-ybZ" id="OIh-K9-gSk"/>
<outlet property="peopleTodayLabel" destination="Kc5-BL-bmu" id="5L8-aO-zt4"/>
</connections>
<point key="canvasLocation" x="132" y="132"/>
</tableViewCell>
</objects>
</document>

View File

@ -24,11 +24,7 @@ class PublicTimelineDescriptionTableViewCell: UITableViewCell {
override func awakeFromNib() { override func awakeFromNib() {
super.awakeFromNib() super.awakeFromNib()
if #available(iOS 15.0, *) { contentView.backgroundColor = .tintColor
contentView.backgroundColor = .tintColor
} else {
contentView.backgroundColor = .systemBlue
}
} }
private func updateLabel() { private func updateLabel() {

View File

@ -42,7 +42,10 @@ class TrendHistoryView: UIView {
private func createLayers() { private func createLayers() {
guard let history = history, guard let history = history,
history.count >= 2 else { return } history.count >= 2,
!bounds.isEmpty else {
return
}
let maxUses = history.max(by: { $0.uses < $1.uses })!.uses let maxUses = history.max(by: { $0.uses < $1.uses })!.uses