Compare commits

..

7 Commits

41 changed files with 1116 additions and 500 deletions

View File

@ -39,3 +39,9 @@ extension Emoji: CustomDebugStringConvertible {
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 */; };
04ED00B121481ED800567C53 /* SteppedProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04ED00B021481ED800567C53 /* SteppedProgressView.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 */; };
D60CFFDB24A290BA00D00083 /* SwiftSoup in Frameworks */ = {isa = PBXBuildFile; productRef = D60CFFDA24A290BA00D00083 /* SwiftSoup */; };
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 */; };
D6AEBB4823216B1D00E5038B /* AccountActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6AEBB4723216B1D00E5038B /* AccountActivity.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 */; };
D6B053A423BD2C8100A066FA /* AssetCollectionsListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B053A323BD2C8100A066FA /* AssetCollectionsListViewController.swift */; };
D6B053A623BD2D0C00A066FA /* AssetCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B053A523BD2D0C00A066FA /* AssetCollectionViewController.swift */; };
D6B053AB23BD2F1400A066FA /* AssetCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B053A923BD2F1400A066FA /* AssetCollectionViewCell.swift */; };
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 */; };
D6B22A0F2560D52D004D82EF /* TabbedPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B22A0E2560D52D004D82EF /* TabbedPageViewController.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 */; };
D6DFC6A0242C4CCC00ACC392 /* WeakArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DFC69F242C4CCC00ACC392 /* WeakArray.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 */; };
D6E343AD265AAD6B00C4AA01 /* ActionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E343AC265AAD6B00C4AA01 /* ActionViewController.swift */; };
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 */; };
D6E426B325337C7000C02E1C /* CustomEmojiImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E426B225337C7000C02E1C /* CustomEmojiImageView.swift */; };
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 */; };
D6EAE0DB2550CC8A002DB0AC /* FocusableTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EAE0DA2550CC8A002DB0AC /* FocusableTextField.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>"; };
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>"; };
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>"; };
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>"; };
@ -573,7 +570,6 @@
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>"; };
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>"; };
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>"; };
@ -634,7 +630,6 @@
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>"; };
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; };
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>"; };
@ -648,6 +643,10 @@
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>"; };
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>"; };
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>"; };
@ -667,7 +666,6 @@
buildActionMask = 2147483647;
files = (
D674A50927F9128D00BA03AC /* Pachyderm in Frameworks */,
D6B0539F23BD2BA300A066FA /* SheetController in Frameworks */,
D69CCBBF249E6EFD000AF167 /* CrashReporter in Frameworks */,
D60CFFDB24A290BA00D00083 /* SwiftSoup in Frameworks */,
D6676CA527A8D0020052936B /* WebURLFoundationExtras in Frameworks */,
@ -716,8 +714,7 @@
children = (
D611C2CD232DC61100C86A49 /* HashtagTableViewCell.swift */,
D611C2CE232DC61100C86A49 /* HashtagTableViewCell.xib */,
D6093FAE25BE0B01004811E6 /* TrendingHashtagTableViewCell.swift */,
D6093FAF25BE0B01004811E6 /* TrendingHashtagTableViewCell.xib */,
D6E77D08286D25FA00D8B732 /* TrendingHashtagCollectionViewCell.swift */,
);
path = "Hashtag Cell";
sourceTree = "<group>";
@ -803,6 +800,8 @@
D693A72725CF282E003A14E2 /* TrendingHashtagsViewController.swift */,
D6114E1027F899B30080E273 /* TrendingLinksViewController.swift */,
D6114E1227F89B440080E273 /* TrendingLinkTableViewCell.swift */,
D6E77D0A286D426E00D8B732 /* TrendingLinkCardCollectionViewCell.swift */,
D6E77D0C286E6B7300D8B732 /* TrendingLinkCardCollectionViewCell.xib */,
D693A72925CF8C1E003A14E2 /* ProfileDirectoryViewController.swift */,
D693A72D25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift */,
D693A72E25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.xib */,
@ -1123,7 +1122,6 @@
D6620ACD2511A0ED00312CA0 /* StatusStateResolver.swift */,
D69693F32585941A00F4E116 /* UIWindowSceneDelegate+Close.swift */,
D62E9984279CA23900C26176 /* URLSession+Development.swift */,
D6E1EEF3285443EF00D20549 /* UIAction+Subtitle.swift */,
);
path = Extensions;
sourceTree = "<group>";
@ -1242,7 +1240,6 @@
D6B053A523BD2D0C00A066FA /* AssetCollectionViewController.swift */,
D626493E23C101C500612E6E /* AlbumAssetCollectionViewController.swift */,
D64BC18523C1253A000D0238 /* AssetPreviewViewController.swift */,
D6B053AD23BD322B00A066FA /* AssetPickerSheetContainerViewController.swift */,
);
path = "Asset Picker";
sourceTree = "<group>";
@ -1303,6 +1300,7 @@
D653F410267D1E32004E32B1 /* DiffableTimelineLikeTableViewController.swift */,
D625E4812588262A0074BB2B /* DraggableTableViewCell.swift */,
D693DE5623FE1A6A0061E07D /* EnhancedNavigationViewController.swift */,
D6E77D0E286F773900D8B732 /* SplitNavigationController.swift */,
D6BC8747219738E1006163F1 /* EnhancedTableViewController.swift */,
D693DE5823FE24300061E07D /* InteractivePushTransition.swift */,
D6C693FB2162FE6F007D6A6D /* LoadingViewController.swift */,
@ -1504,7 +1502,6 @@
);
name = Tusker;
packageProductDependencies = (
D6B0539E23BD2BA300A066FA /* SheetController */,
D69CCBBE249E6EFD000AF167 /* CrashReporter */,
D60CFFDA24A290BA00D00083 /* SwiftSoup */,
D6676CA427A8D0020052936B /* WebURLFoundationExtras */,
@ -1614,7 +1611,6 @@
);
mainGroup = D6D4DDC3212518A000E1C4BB;
packageReferences = (
D6B0539D23BD2BA300A066FA /* XCRemoteSwiftPackageReference "SheetController" */,
D69CCBBD249E6EFD000AF167 /* XCRemoteSwiftPackageReference "plcrashreporter" */,
D60CFFD924A290BA00D00083 /* XCRemoteSwiftPackageReference "SwiftSoup" */,
D6676CA127A8D0020052936B /* XCRemoteSwiftPackageReference "swift-url" */,
@ -1637,6 +1633,7 @@
buildActionMask = 2147483647;
files = (
D611C2D0232DC61100C86A49 /* HashtagTableViewCell.xib in Resources */,
D6E77D0D286E6B7300D8B732 /* TrendingLinkCardCollectionViewCell.xib in Resources */,
D693A73025CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.xib in Resources */,
D61AC1D9232EA42D00C54D2D /* InstanceTableViewCell.xib in Resources */,
D626493923C0FD0000612E6E /* AllPhotosTableViewCell.xib in Resources */,
@ -1653,7 +1650,6 @@
D663625D2135C74800C9CBA2 /* ConversationMainStatusTableViewCell.xib in Resources */,
D640D76922BAF5E6004FBE69 /* DomainBlocks.plist in Resources */,
D6289E84217B795D0003D1D7 /* LargeImageViewController.xib in Resources */,
D6093FB125BE0B01004811E6 /* TrendingHashtagTableViewCell.xib in Resources */,
D627FF79217E950100CC0648 /* DraftsTableViewController.xib in Resources */,
D626493D23C1000300612E6E /* AlbumTableViewCell.xib in Resources */,
D627944723A6AC9300D38C68 /* BasicTableViewCell.xib in Resources */,
@ -1764,7 +1760,6 @@
D64BC18F23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.swift in Sources */,
D62E9989279DB2D100C26176 /* InstanceFeatures.swift in Sources */,
D693DE5923FE24310061E07D /* InteractivePushTransition.swift in Sources */,
D6E1EEF4285443EF00D20549 /* UIAction+Subtitle.swift in Sources */,
D69693FA25859A8000F4E116 /* ComposeSceneDelegate.swift in Sources */,
D6A3BC8A2321F79B00FD64D5 /* AccountTableViewCell.swift in Sources */,
D66A77BB233838DC0058F1EC /* UIFont+Traits.swift in Sources */,
@ -1812,6 +1807,7 @@
D662AEF2263A4BE10082A153 /* ComposePollView.swift in Sources */,
D677284A24ECBDF400C732D3 /* ComposeCurrentAccount.swift in Sources */,
D62D2424217ABF3F005076CC /* NSUserActivity+Extensions.swift in Sources */,
D6E77D0F286F773900D8B732 /* SplitNavigationController.swift in Sources */,
D6B30E09254BAF63009CAEE5 /* ImageGrayscalifier.swift in Sources */,
D6A6C10F25B62D2400298D0F /* DiskCache.swift in Sources */,
D6B81F3C2560365300F6E31D /* RefreshableViewController.swift in Sources */,
@ -1857,7 +1853,6 @@
D6EAE0DB2550CC8A002DB0AC /* FocusableTextField.swift in Sources */,
D623A53D2635F5590095BD04 /* StatusPollView.swift in Sources */,
D64AAE9726C88DC400FC57FB /* ToastConfiguration.swift in Sources */,
D6093FB025BE0B01004811E6 /* TrendingHashtagTableViewCell.swift in Sources */,
D63661C02381C144004B9E16 /* PreferencesNavigationController.swift in Sources */,
D6E9CDA8281A427800BBC98E /* PostService.swift in Sources */,
D6B053A223BD2C0600A066FA /* AssetPickerViewController.swift in Sources */,
@ -1888,7 +1883,6 @@
D64D0AAD2128D88B005A6F37 /* LocalData.swift in Sources */,
D6945C2F23AC47C3005C403C /* SavedDataManager.swift in Sources */,
D6C94D892139E6EC00CB5196 /* AttachmentView.swift in Sources */,
D6B053AE23BD322B00A066FA /* AssetPickerSheetContainerViewController.swift in Sources */,
D6C693EF216192C2007D6A6D /* TuskerNavigationDelegate.swift in Sources */,
D6C94D872139E62700CB5196 /* LargeImageViewController.swift in Sources */,
D6B053AB23BD2F1400A066FA /* AssetCollectionViewCell.swift in Sources */,
@ -1929,6 +1923,7 @@
D64F80E2215875CC00BEF393 /* XCBActionType.swift in Sources */,
04586B4322B301470021BD04 /* AppearancePrefsView.swift in Sources */,
D6FF9860255C717400845181 /* AccountSwitchingContainerViewController.swift in Sources */,
D6E77D0B286D426E00D8B732 /* TrendingLinkCardCollectionViewCell.swift in Sources */,
D6114E1127F899B30080E273 /* TrendingLinksViewController.swift in Sources */,
D670F8B62537DC890046588A /* EmojiPickerWrapper.swift in Sources */,
D6B81F442560390300F6E31D /* MenuController.swift in Sources */,
@ -1972,6 +1967,7 @@
D6C99FCB24FADC91005C74D3 /* MainComposeTextView.swift in Sources */,
D6093F9B25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift in Sources */,
D6E4267725327FB400C02E1C /* ComposeAutocompleteView.swift in Sources */,
D6E77D09286D25FA00D8B732 /* TrendingHashtagCollectionViewCell.swift in Sources */,
D6757A7E2157E02600721E32 /* XCBRequestSpec.swift in Sources */,
D677284E24ECC01D00C732D3 /* Draft.swift in Sources */,
D667E5F12134D5050057A976 /* UIViewController+Delegates.swift in Sources */,
@ -2205,7 +2201,7 @@
CURRENT_PROJECT_VERSION = 31;
DEVELOPMENT_TEAM = V4WK9KR9U2;
INFOPLIST_FILE = Tusker/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.3;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -2235,7 +2231,7 @@
CURRENT_PROJECT_VERSION = 31;
DEVELOPMENT_TEAM = V4WK9KR9U2;
INFOPLIST_FILE = Tusker/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.3;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -2463,14 +2459,6 @@
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 */
/* Begin XCSwiftPackageProductDependency section */
@ -2493,11 +2481,6 @@
package = D69CCBBD249E6EFD000AF167 /* XCRemoteSwiftPackageReference "plcrashreporter" */;
productName = CrashReporter;
};
D6B0539E23BD2BA300A066FA /* SheetController */ = {
isa = XCSwiftPackageProductDependency;
package = D6B0539D23BD2BA300A066FA /* XCRemoteSwiftPackageReference "SheetController" */;
productName = SheetController;
};
/* End XCSwiftPackageProductDependency section */
/* Begin XCVersionGroup section */

View File

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

View File

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

View File

@ -48,7 +48,7 @@ class MastodonController: ObservableObject {
@Published private(set) var instanceFeatures = InstanceFeatures()
private(set) var customEmojis: [Emoji]?
private var pendingOwnInstanceRequestCallbacks = [(Instance) -> Void]()
private var pendingOwnInstanceRequestCallbacks = [(Result<Instance, Client.Error>) -> Void]()
private var ownInstanceRequest: URLSessionTask?
var loggedIn: Bool {
@ -159,15 +159,28 @@ class MastodonController: ObservableObject {
}
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
assert(Thread.isMainThread)
if let instance = self.instance {
completion?(instance)
completion?(.success(instance))
} else {
if let completion = completion {
pendingOwnInstanceRequestCallbacks.append(completion)
@ -177,7 +190,7 @@ class MastodonController: ObservableObject {
let request = Client.getInstance()
ownInstanceRequest = run(request) { (response) in
switch response {
case .failure(_):
case .failure(let error):
let delay: DispatchTimeInterval
switch retryAttempt {
case 0:
@ -190,6 +203,10 @@ class MastodonController: ObservableObject {
delay = .seconds(60)
default:
// if we've failed four times, just give up :/
for completion in self.pendingOwnInstanceRequestCallbacks {
completion(.failure(error))
}
self.pendingOwnInstanceRequestCallbacks = []
return
}
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
@ -204,7 +221,7 @@ class MastodonController: ObservableObject {
self.instanceFeatures.update(instance: instance, nodeInfo: self.nodeInfo)
for completion in self.pendingOwnInstanceRequestCallbacks {
completion(instance)
completion(.success(instance))
}
self.pendingOwnInstanceRequestCallbacks = []
}

View File

@ -22,7 +22,7 @@ struct MenuController {
let data: Any
if case let .tab(tab) = item {
data = tab.rawValue
} else if case .search = item {
} else if case .explore = item {
data = "search"
} else if case .bookmarks = item {
data = "bookmarks"
@ -42,7 +42,7 @@ struct MenuController {
static let sidebarItemKeyCommands: [UIKeyCommand] = [
sidebarCommand(item: .tab(.timelines), command: "1"),
sidebarCommand(item: .tab(.notifications), command: "2"),
sidebarCommand(item: .search, command: "3"),
sidebarCommand(item: .explore, command: "3"),
sidebarCommand(item: .bookmarks, command: "4"),
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
}
var trends: Bool {
instanceType == .mastodon
}
var trendingStatusesAndLinks: Bool {
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) {
Label("Delete", systemImage: "trash")
}.foregroundStyle(.red)
} else {
Button(action: self.removeAttachment) {
Label("Delete", systemImage: "trash")
}
}
Button(action: self.removeAttachment) {
Label("Delete", systemImage: "trash")
}.foregroundStyle(.red)
} previewIfAvailable: {
ComposeAttachmentImage(attachment: attachment, fullSize: true)
}

View File

@ -191,16 +191,6 @@ struct ComposeAttachmentsList: 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)
@ViewBuilder
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 {
suggestionsView
// animate changes of the scroll view items
.animation(.default)
.background(backgroundColor)
.overlay(borderColor.frame(height: 0.5), alignment: .top)
}
@ -85,8 +83,8 @@ struct ComposeAutocompleteMentionsView: View {
}
.frame(height: 30)
.padding(.vertical, 8)
.animation(.linear(duration: 0.1))
}
.animation(.linear(duration: 0.1), value: accounts)
Spacer()
}
@ -167,7 +165,7 @@ struct ComposeAutocompleteMentionsView: View {
.map(\.0)
}
private enum EitherAccount {
private enum EitherAccount: Equatable {
case pachyderm(Account)
case coreData(AccountMO)
@ -197,6 +195,10 @@ struct ComposeAutocompleteMentionsView: View {
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) {
if case let .emoji(query) = uiState.autocompleteState {
emojiList(query: query)
.animation(.default)
.animation(.default, value: expanded)
.transition(.move(edge: .bottom))
} else {
// when the autocomplete view is animating out, the autocomplete state is nil
@ -259,8 +261,8 @@ struct ComposeAutocompleteEmojisView: View {
}
.frame(height: 30)
.padding(.vertical, 8)
.animation(.linear(duration: 0.2))
}
.animation(.linear(duration: 0.2), value: emojis)
Spacer(minLength: 30)
}
@ -319,8 +321,8 @@ struct ComposeAutocompleteHashtagsView: View {
}
.frame(height: 30)
.padding(.vertical, 8)
.animation(.linear(duration: 0.1))
}
.animation(.linear(duration: 0.1), value: hashtags)
Spacer()
}

View File

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

View File

@ -60,7 +60,9 @@ struct ComposePollView: View {
HStack {
// 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("Single choice").tag(false)
}
@ -154,8 +156,7 @@ struct ComposePollOption: View {
var body: some View {
HStack(spacing: 4) {
Checkbox(radiusFraction: poll.multiple ? 0.1 : 0.5, borderWidth: 2)
.animation(.default)
.animation(.default, value: poll.multiple)
textField

View File

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

View File

@ -119,7 +119,7 @@ struct ComposeView: View {
}
}
.transition(.move(edge: .bottom))
.animation(.default)
.animation(.default, value: uiState.autocompleteState)
}
func mainStack(outerMinY: CGFloat) -> some View {
@ -147,7 +147,6 @@ struct ComposeView: View {
if let poll = draft.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))))
.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.isSelected = showStatusesAutomatically
} else {
let initialImage = showStatusesAutomatically ? ConversationTableViewController.hidePostsImage : ConversationTableViewController.showPostsImage
visibilityBarButtonItem = UIBarButtonItem(image: initialImage, style: .plain, target: self, action: #selector(toggleVisibilityButtonPressed))
}
visibilityBarButtonItem = UIBarButtonItem(image: ConversationTableViewController.showPostsImage, style: .plain, target: self, action: #selector(toggleVisibilityButtonPressed))
visibilityBarButtonItem.isSelected = showStatusesAutomatically
navigationItem.rightBarButtonItem = visibilityBarButtonItem
// 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)
@ -396,15 +391,7 @@ class ConversationTableViewController: EnhancedTableViewController {
tableView.beginUpdates()
tableView.endUpdates()
if #available(iOS 15.0, *) {
visibilityBarButtonItem.isSelected = showStatusesAutomatically
} else {
if showStatusesAutomatically {
visibilityBarButtonItem.image = ConversationTableViewController.hidePostsImage
} else {
visibilityBarButtonItem.image = ConversationTableViewController.showPostsImage
}
}
visibilityBarButtonItem.isSelected = showStatusesAutomatically
}
}

View File

@ -9,19 +9,20 @@
import UIKit
import Pachyderm
class AddSavedHashtagViewController: EnhancedTableViewController {
class AddSavedHashtagViewController: UIViewController {
weak var mastodonController: MastodonController!
var resultsController: SearchResultsViewController!
var searchController: UISearchController!
var dataSource: UITableViewDiffableDataSource<Section, Item>!
private var collectionView: UICollectionView!
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
init(mastodonController: MastodonController) {
self.mastodonController = mastodonController
super.init(style: .grouped)
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
@ -33,18 +34,38 @@ class AddSavedHashtagViewController: EnhancedTableViewController {
title = NSLocalizedString("Search", comment: "search screen title")
tableView.register(UINib(nibName: "TrendingHashtagTableViewCell", bundle: .main), forCellReuseIdentifier: "trendingTagCell")
tableView.rowHeight = 60 // 44 for content + 2 * 8 spacing
dataSource = DataSource(tableView: tableView, cellProvider: { (tableView, indexPath, item) -> UITableViewCell? in
view.backgroundColor = .systemGroupedBackground
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 {
case let .tag(hashtag):
let cell = tableView.dequeueReusableCell(withIdentifier: "trendingTagCell", for: indexPath) as! TrendingHashtagTableViewCell
cell.updateUI(hashtag: hashtag)
return cell
return collectionView.dequeueConfiguredReusableCell(using: registration, for: indexPath, item: hashtag)
}
})
}
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.delegate = self
resultsController.exploreNavigationController = self.navigationController!
@ -92,17 +113,6 @@ class AddSavedHashtagViewController: EnhancedTableViewController {
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
@objc func cancelButtonPressed() {
@ -115,14 +125,23 @@ extension AddSavedHashtagViewController {
enum Section {
case trendingTags
}
enum Item: Hashable {
case tag(Hashtag)
}
class DataSource: UITableViewDiffableDataSource<Section, Item> {
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return NSLocalizedString("Trending Hashtags", comment: "trending hashtags seciton title")
// class DataSource: UITableViewDiffableDataSource<Section, Item> {
// override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
// return
// }
// }
}
extension AddSavedHashtagViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
switch dataSource.itemIdentifier(for: indexPath) {
case nil:
return
case let .tag(hashtag):
selectHashtag(hashtag)
}
}
}

View File

@ -9,18 +9,17 @@
import UIKit
import Pachyderm
class TrendingHashtagsViewController: EnhancedTableViewController {
class TrendingHashtagsViewController: UIViewController {
weak var mastodonController: MastodonController!
private var dataSource: UITableViewDiffableDataSource<Section, Item>!
private var collectionView: UICollectionView!
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
init(mastodonController: MastodonController) {
self.mastodonController = mastodonController
super.init(style: .grouped)
dragEnabled = true
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
@ -32,15 +31,24 @@ class TrendingHashtagsViewController: EnhancedTableViewController {
title = NSLocalizedString("Trending Hashtags", comment: "trending hashtags screen title")
tableView.register(UINib(nibName: "TrendingHashtagTableViewCell", bundle: .main), forCellReuseIdentifier: "trendingTagCell")
tableView.rowHeight = 60 // 44 for content + 2 * 8 spacing
view.backgroundColor = .systemGroupedBackground
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 {
case let .tag(hashtag):
let cell = tableView.dequeueReusableCell(withIdentifier: "trendingTagCell", for: indexPath) as! TrendingHashtagTableViewCell
cell.updateUI(hashtag: hashtag)
return cell
return collectionView.dequeueConfiguredReusableCell(using: registration, for: indexPath, item: hashtag)
}
}
}
@ -59,42 +67,6 @@ class TrendingHashtagsViewController: EnhancedTableViewController {
await dataSource.apply(snapshot)
}
}
// MARK: - Table View Delegate
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let item = dataSource.itemIdentifier(for: indexPath),
case let .tag(hashtag) = item else {
return
}
show(HashtagTimelineViewController(for: hashtag, mastodonController: mastodonController), sender: nil)
}
override func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
guard let item = dataSource.itemIdentifier(for: indexPath),
case let .tag(hashtag) = item else {
return nil
}
return UIContextMenuConfiguration(identifier: nil) {
HashtagTimelineViewController(for: hashtag, mastodonController: self.mastodonController)
} actionProvider: { (_) in
UIMenu(children: self.actionsForHashtag(hashtag, sourceView: self.tableView.cellForRow(at: indexPath)))
}
}
override func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
guard let item = dataSource.itemIdentifier(for: indexPath),
case let .tag(hashtag) = item else {
return []
}
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)]
}
}
@ -107,6 +79,44 @@ extension TrendingHashtagsViewController {
}
}
extension TrendingHashtagsViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
guard let item = dataSource.itemIdentifier(for: indexPath),
case let .tag(hashtag) = item else {
return
}
show(HashtagTimelineViewController(for: hashtag, mastodonController: mastodonController), sender: nil)
}
func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
guard let item = dataSource.itemIdentifier(for: indexPath),
case let .tag(hashtag) = item else {
return nil
}
return UIContextMenuConfiguration(identifier: nil) {
HashtagTimelineViewController(for: hashtag, mastodonController: self.mastodonController)
} actionProvider: { (_) in
UIMenu(children: self.actionsForHashtag(hashtag, sourceView: self.collectionView.cellForItem(at: indexPath)))
}
}
}
extension TrendingHashtagsViewController: UICollectionViewDragDelegate {
func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
guard let item = dataSource.itemIdentifier(for: indexPath),
case let .tag(hashtag) = item else {
return []
}
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)]
}
}
extension TrendingHashtagsViewController: TuskerNavigationDelegate {
var apiController: MastodonController { mastodonController }
}

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() {
if isGrayscale != Preferences.shared.grayscaleImages,
let card {
updateGrayscaleableUI(card: card)
}
}
private func updateGrayscaleableUI(card: Card) {

View File

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

View File

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

View File

@ -20,8 +20,11 @@ class MainSplitViewController: UISplitViewController {
private var tabBarViewController: MainTabBarViewController!
private var secondaryNavController: UINavigationController! {
viewController(for: .secondary) as? UINavigationController
// private var secondaryNavController: UINavigationController! {
// viewController(for: .secondary) as? UINavigationController
// }
private var secondaryNavController: SplitNavigationController! {
viewController(for: .secondary) as? SplitNavigationController
}
init(mastodonController: MastodonController) {
@ -46,9 +49,10 @@ class MainSplitViewController: UISplitViewController {
setViewController(sidebar, for: .primary)
primaryBackgroundStyle = .sidebar
let secondaryNav = EnhancedNavigationViewController()
secondaryNav.useBrowserStyleNavigation = true
setViewController(secondaryNav, for: .secondary)
// let secondaryNav = EnhancedNavigationViewController()
// secondaryNav.useBrowserStyleNavigation = true
let splitNav = SplitNavigationController()
setViewController(splitNav, for: .secondary)
// 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
if traitCollection.horizontalSizeClass != .compact {
@ -100,7 +104,7 @@ class MainSplitViewController: UISplitViewController {
item = .tab(MainTabBarViewController.Tab(rawValue: index)!)
} else if let str = command.propertyList as? String {
if str == "search" {
item = .search
item = .explore
} else if str == "bookmarks" {
item = .bookmarks
} else {
@ -171,7 +175,7 @@ extension MainSplitViewController: UISplitViewControllerDelegate {
$0.1 > $1.1
}
if let mostRecentExploreItem = mostRecentExploreItem?.0,
mostRecentExploreItem != .search {
mostRecentExploreItem != .explore {
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
exploreNav.popToRootViewController(animated: false)
@ -188,7 +192,7 @@ extension MainSplitViewController: UISplitViewControllerDelegate {
// sidebar items that map 1 <-> 1 can be transferred directly
tabBarViewController.select(tab: tab)
case .search:
case .explore:
// 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
// so that explore items aren't shown multiple times.
@ -217,11 +221,11 @@ extension MainSplitViewController: UISplitViewControllerDelegate {
explore.resultsController.loadResults(from: search.resultsController)
// 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)
case .bookmarks, .trendingStatuses, .trendingTags, .trendingLinks, .profileDirectory, .list(_), .savedHashtag(_), .savedInstance(_):
case .bookmarks, .trendingStatuses, .profileDirectory, .list(_), .savedHashtag(_), .savedInstance(_):
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
// 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.
// Search screen has special considerations, all others can be transferred directly.
if tabNavigationStack.count == 1 || ((tabNavigationStack.first as? ExploreViewController)?.searchController?.isActive ?? false) {
exploreItem = .search
exploreItem = .explore
let searchVC = SearchViewController(mastodonController: mastodonController)
searchVC.loadViewIfNeeded()
let explore = tabNavigationStack.first as! ExploreViewController
@ -300,9 +304,9 @@ extension MainSplitViewController: UISplitViewControllerDelegate {
case is TrendingStatusesViewController:
exploreItem = .trendingStatuses
case is TrendingHashtagsViewController:
exploreItem = .trendingTags
exploreItem = .explore
case is TrendingLinksViewController:
exploreItem = .trendingLinks
exploreItem = .explore
case is ProfileDirectoryViewController:
exploreItem = .profileDirectory
default:
@ -354,16 +358,12 @@ fileprivate extension MainSidebarViewController.Item {
switch self {
case let .tab(tab):
return tab.createViewController(mastodonController)
case .search:
case .explore:
return SearchViewController(mastodonController: mastodonController)
case .bookmarks:
return BookmarksTableViewController(mastodonController: mastodonController)
case .trendingStatuses:
return TrendingStatusesViewController(mastodonController: mastodonController)
case .trendingTags:
return TrendingHashtagsViewController(mastodonController: mastodonController)
case .trendingLinks:
return TrendingLinksViewController(mastodonController: mastodonController)
case .profileDirectory:
return ProfileDirectoryViewController(mastodonController: mastodonController)
case let .list(list):
@ -380,7 +380,7 @@ fileprivate extension MainSidebarViewController.Item {
extension MainSplitViewController: TuskerRootViewController {
@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 options = UIWindowScene.ActivationRequestOptions()
options.preferredPresentationStyle = .prominent
@ -435,8 +435,8 @@ extension MainSplitViewController: TuskerRootViewController {
return
}
if sidebar.selectedItem != .search {
select(item: .search)
if sidebar.selectedItem != .explore {
select(item: .explore)
}
guard let searchViewController = secondaryNavController.viewControllers.first as? SearchViewController else {

View File

@ -142,7 +142,7 @@ class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate {
return vc
} else {
let nav = EnhancedNavigationViewController(rootViewController: vc)
nav.useBrowserStyleNavigation = true
// nav.useBrowserStyleNavigation = true
return nav
}
}
@ -230,7 +230,7 @@ extension MainTabBarViewController: FastAccountSwitcherViewControllerDelegate {
extension MainTabBarViewController: TuskerRootViewController {
@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 options = UIWindowScene.ActivationRequestOptions()
options.preferredPresentationStyle = .prominent

View File

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

View File

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

View File

@ -7,11 +7,16 @@
//
import UIKit
import Pachyderm
import SafariServices
class SearchViewController: UIViewController {
weak var mastodonController: MastodonController!
private var collectionView: UICollectionView!
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
var resultsController: SearchResultsViewController!
var searchController: UISearchController!
@ -22,7 +27,7 @@ class SearchViewController: UIViewController {
super.init(nibName: nil, bundle: nil)
title = NSLocalizedString("Search", comment: "search tab title")
title = NSLocalizedString("Explore", comment: "explore tab title")
}
required init?(coder: NSCoder) {
@ -32,12 +37,46 @@ class SearchViewController: UIViewController {
override func 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.exploreNavigationController = self.navigationController
searchController = UISearchController(searchResultsController: resultsController)
searchController.obscuresBackgroundDuringPresentation = false
searchController.obscuresBackgroundDuringPresentation = true
searchController.searchBar.autocapitalizationType = .none
searchController.searchBar.delegate = resultsController
searchController.hidesNavigationBarDuringPresentation = false
@ -48,6 +87,18 @@ class SearchViewController: UIViewController {
if #available(iOS 16.0, *) {
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) {
@ -61,5 +112,214 @@ class SearchViewController: UIViewController {
searchControllerStatusOnAppearance = nil
}
}
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
}
if #available(iOS 15.0, *),
Preferences.shared.disableInfiniteScrolling && !didConfirmLoadMore {
if Preferences.shared.disableInfiniteScrolling && !didConfirmLoadMore {
var snapshot = currentSnapshot()
guard !snapshot.itemIdentifiers(inSection: .footer).contains(.confirmLoadMore) else {
// todo: need something more accurate than "success"/"failure"

View File

@ -59,7 +59,7 @@ extension MenuActionProvider {
draft.visibility = .direct
self.navigationDelegate?.compose(editing: draft)
}),
UIDeferredMenuElement.uncachedIfPossible({ (elementHandler) in
UIDeferredMenuElement.uncached({ (elementHandler) in
Task { @MainActor in
if let action = await self.followAction(for: accountID, mastodonController: mastodonController) {
elementHandler([action])
@ -358,21 +358,13 @@ extension MenuActionProvider {
}
private func addOpenInNewWindow(actions: inout [UIAction], activity: @escaping @autoclosure () -> NSUserActivity) {
if #available(iOS 15.0, *) {
let options = UIWindowScene.ActivationRequestOptions()
options.preferredPresentationStyle = .automatic
actions.append(UIWindowScene.ActivationAction { (_) in
let activity = activity()
activity.displaysAuxiliaryScene = true
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)
}))
}
let options = UIWindowScene.ActivationRequestOptions()
options.preferredPresentationStyle = .automatic
actions.append(UIWindowScene.ActivationAction { (_) in
let activity = activity()
activity.displaysAuxiliaryScene = true
return .init(userActivity: activity, options: options, preview: nil)
})
}
private func followAction(for accountID: String, mastodonController: MastodonController) async -> UIMenuElement? {
@ -423,13 +415,3 @@ extension SFSafariViewController: CustomPreviewPresenting {
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
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 let oldParent = newChild.parent, oldParent != self {
newChild.beginAppearanceTransition(false, animated: false)
@ -36,7 +36,7 @@ extension UIViewController {
newChild.beginAppearanceTransition(true, animated: false)
addChild(newChild)
newChild.didMove(toParent: self)
targetContainer.embedSubview(newChild.view)
targetContainer.embedSubview(newChild.view, layout: layout)
newChild.endAppearanceTransition()
} else {
// the view controller is already a child
@ -45,7 +45,7 @@ extension UIViewController {
// we don't do the appearance transition stuff here,
// because the vc is already a child, so *presumably*
// 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
extension UIView {
func embedSubview(_ subview: UIView) {
func embedSubview(_ subview: UIView, layout: Bool = true) {
if subview.superview == self { return }
if subview.superview != nil {
subview.removeFromSuperview()
}
subview.frame = bounds
addSubview(subview)
NSLayoutConstraint.activate([
subview.leadingAnchor.constraint(equalTo: leadingAnchor),
subview.trailingAnchor.constraint(equalTo: trailingAnchor),
subview.topAnchor.constraint(equalTo: topAnchor),
subview.bottomAnchor.constraint(equalTo: bottomAnchor)
if layout {
subview.frame = bounds
NSLayoutConstraint.activate([
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 {

View File

@ -89,7 +89,7 @@ extension TuskerNavigationDelegate {
}
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 options = UIWindowScene.ActivationRequestOptions()
options.preferredPresentationStyle = .prominent

View File

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

View File

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

View File

@ -42,7 +42,10 @@ class TrendHistoryView: UIView {
private func createLayers() {
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