Compare commits
No commits in common. "afed69e43ea03ccd32de6b4fc03076f7eeb6be60" and "474064669db8f9172c566a77907b886f4d3e3f80" have entirely different histories.
afed69e43e
...
474064669d
24
CHANGELOG.md
24
CHANGELOG.md
|
@ -1,29 +1,5 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## 2023.4 (72)
|
|
||||||
Features/Improvements:
|
|
||||||
- Consolidate Trends into a single screen
|
|
||||||
- Make attachment description text selectable in gallery
|
|
||||||
- Add long press to copy usernames on profile screen
|
|
||||||
- Add Favorites screen to Explore tab
|
|
||||||
- Optimize conversation loading when opening a conversation that is already fully-loaded
|
|
||||||
- Apply Mastodon poll limits in Compose screen
|
|
||||||
- VoiceOver: Fast account switcher improvements (make the screen modal, select the first account upon opening the switcher, make each account a single item)
|
|
||||||
- VoiceOver: Improve labels for notifications
|
|
||||||
- VoiceOver: Fix custom emoji picker buttons not having labels
|
|
||||||
|
|
||||||
Bugfixes:
|
|
||||||
- Fix trends sometimes appearing in Explore/sidebar on non-Mastodon instances
|
|
||||||
- Fix status favorite/reblog accounts list not resizing on device rotation
|
|
||||||
- Fix bookmarks screen sometimes going haywire
|
|
||||||
- Fix trending statuses not being deselected upon navigating back
|
|
||||||
- Fix crash when tapping My Profile tab too early in app lifecycle
|
|
||||||
- Handle 401 errors on instance timelines properly
|
|
||||||
- Fix potential crash when showing context menu previews for status
|
|
||||||
- Fix follow request accept/reject buttons not matching accent color preference
|
|
||||||
- iPadOS: Fix crash when switching between sidebar and tab bar while on the Explore screen
|
|
||||||
- iOS 15: Fix accent colors not being disaplyed in Preferences
|
|
||||||
|
|
||||||
## 2023.4 (71)
|
## 2023.4 (71)
|
||||||
Features/Improvements:
|
Features/Improvements:
|
||||||
- Allow pinning instance public timelines to the Home tab
|
- Allow pinning instance public timelines to the Home tab
|
||||||
|
|
|
@ -199,10 +199,8 @@ public class Client {
|
||||||
return Request<Account>(method: .get, path: "/api/v1/accounts/verify_credentials")
|
return Request<Account>(method: .get, path: "/api/v1/accounts/verify_credentials")
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func getFavourites(range: RequestRange = .default) -> Request<[Status]> {
|
public static func getFavourites() -> Request<[Status]> {
|
||||||
var request = Request<[Status]>(method: .get, path: "/api/v1/favourites")
|
return Request<[Status]>(method: .get, path: "/api/v1/favourites")
|
||||||
request.range = range
|
|
||||||
return request
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func getRelationships(accounts: [String]? = nil) -> Request<[Relationship]> {
|
public static func getRelationships(accounts: [String]? = nil) -> Request<[Relationship]> {
|
||||||
|
|
|
@ -216,7 +216,7 @@
|
||||||
D68ACE5D279B1ABA001CE8EB /* AssetPickerControlCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68ACE5C279B1ABA001CE8EB /* AssetPickerControlCollectionViewCell.swift */; };
|
D68ACE5D279B1ABA001CE8EB /* AssetPickerControlCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68ACE5C279B1ABA001CE8EB /* AssetPickerControlCollectionViewCell.swift */; };
|
||||||
D68C2AE325869BAB00548EFF /* AuxiliarySceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68C2AE225869BAB00548EFF /* AuxiliarySceneDelegate.swift */; };
|
D68C2AE325869BAB00548EFF /* AuxiliarySceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68C2AE225869BAB00548EFF /* AuxiliarySceneDelegate.swift */; };
|
||||||
D68E525B24A3D77E0054355A /* TuskerRootViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68E525A24A3D77E0054355A /* TuskerRootViewController.swift */; };
|
D68E525B24A3D77E0054355A /* TuskerRootViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68E525A24A3D77E0054355A /* TuskerRootViewController.swift */; };
|
||||||
D68E525D24A3E8F00054355A /* InlineTrendsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68E525C24A3E8F00054355A /* InlineTrendsViewController.swift */; };
|
D68E525D24A3E8F00054355A /* SearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68E525C24A3E8F00054355A /* SearchViewController.swift */; };
|
||||||
D68E6F59253C9969001A1B4C /* MultiSourceEmojiLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68E6F58253C9969001A1B4C /* MultiSourceEmojiLabel.swift */; };
|
D68E6F59253C9969001A1B4C /* MultiSourceEmojiLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68E6F58253C9969001A1B4C /* MultiSourceEmojiLabel.swift */; };
|
||||||
D68E6F5F253C9B2D001A1B4C /* BaseEmojiLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68E6F5E253C9B2D001A1B4C /* BaseEmojiLabel.swift */; };
|
D68E6F5F253C9B2D001A1B4C /* BaseEmojiLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68E6F5E253C9B2D001A1B4C /* BaseEmojiLabel.swift */; };
|
||||||
D68FEC4F232C5BC300C84F23 /* SegmentedPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68FEC4E232C5BC300C84F23 /* SegmentedPageViewController.swift */; };
|
D68FEC4F232C5BC300C84F23 /* SegmentedPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68FEC4E232C5BC300C84F23 /* SegmentedPageViewController.swift */; };
|
||||||
|
@ -295,10 +295,6 @@
|
||||||
D6BED174212667E900F02DA0 /* TimelineStatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BED173212667E900F02DA0 /* TimelineStatusTableViewCell.swift */; };
|
D6BED174212667E900F02DA0 /* TimelineStatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BED173212667E900F02DA0 /* TimelineStatusTableViewCell.swift */; };
|
||||||
D6C143DA253510F4007DC240 /* ComposeEmojiTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C143D9253510F4007DC240 /* ComposeEmojiTextField.swift */; };
|
D6C143DA253510F4007DC240 /* ComposeEmojiTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C143D9253510F4007DC240 /* ComposeEmojiTextField.swift */; };
|
||||||
D6C1B2082545D1EC00DAAA66 /* StatusCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C1B2072545D1EC00DAAA66 /* StatusCardView.swift */; };
|
D6C1B2082545D1EC00DAAA66 /* StatusCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C1B2072545D1EC00DAAA66 /* StatusCardView.swift */; };
|
||||||
D6C3F4F5298ED0890009FCFF /* LocalPredicateStatusesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C3F4F4298ED0890009FCFF /* LocalPredicateStatusesViewController.swift */; };
|
|
||||||
D6C3F4F7298ED7F70009FCFF /* FavoritesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C3F4F6298ED7F70009FCFF /* FavoritesViewController.swift */; };
|
|
||||||
D6C3F4F9298EDBF20009FCFF /* ConversationTree.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C3F4F8298EDBF20009FCFF /* ConversationTree.swift */; };
|
|
||||||
D6C3F4FB299035650009FCFF /* TrendsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C3F4FA299035650009FCFF /* TrendsViewController.swift */; };
|
|
||||||
D6C693EF216192C2007D6A6D /* TuskerNavigationDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */; };
|
D6C693EF216192C2007D6A6D /* TuskerNavigationDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */; };
|
||||||
D6C693FC2162FE6F007D6A6D /* LoadingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693FB2162FE6F007D6A6D /* LoadingViewController.swift */; };
|
D6C693FC2162FE6F007D6A6D /* LoadingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693FB2162FE6F007D6A6D /* LoadingViewController.swift */; };
|
||||||
D6C693FE2162FEEA007D6A6D /* UIViewController+Children.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693FD2162FEEA007D6A6D /* UIViewController+Children.swift */; };
|
D6C693FE2162FEEA007D6A6D /* UIViewController+Children.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693FD2162FEEA007D6A6D /* UIViewController+Children.swift */; };
|
||||||
|
@ -325,7 +321,6 @@
|
||||||
D6D4DDF0212518A200E1C4BB /* TuskerUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D4DDEF212518A200E1C4BB /* TuskerUITests.swift */; };
|
D6D4DDF0212518A200E1C4BB /* TuskerUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D4DDEF212518A200E1C4BB /* TuskerUITests.swift */; };
|
||||||
D6D706A029466649000827ED /* ScrollingSegmentedControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D7069F29466649000827ED /* ScrollingSegmentedControl.swift */; };
|
D6D706A029466649000827ED /* ScrollingSegmentedControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D7069F29466649000827ED /* ScrollingSegmentedControl.swift */; };
|
||||||
D6D706A72948D4D0000827ED /* TimlineState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D706A62948D4D0000827ED /* TimlineState.swift */; };
|
D6D706A72948D4D0000827ED /* TimlineState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D706A62948D4D0000827ED /* TimlineState.swift */; };
|
||||||
D6D9498F298EB79400C59229 /* CopyableLable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D9498E298EB79400C59229 /* CopyableLable.swift */; };
|
|
||||||
D6DD2A3F273C1F4900386A6C /* ComposeAttachmentImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD2A3E273C1F4900386A6C /* ComposeAttachmentImage.swift */; };
|
D6DD2A3F273C1F4900386A6C /* ComposeAttachmentImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD2A3E273C1F4900386A6C /* ComposeAttachmentImage.swift */; };
|
||||||
D6DD2A45273D6C5700386A6C /* GIFImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD2A44273D6C5700386A6C /* GIFImageView.swift */; };
|
D6DD2A45273D6C5700386A6C /* GIFImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD2A44273D6C5700386A6C /* GIFImageView.swift */; };
|
||||||
D6DD353D22F28CD000A9563A /* ContentWarningCopyMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD353C22F28CD000A9563A /* ContentWarningCopyMode.swift */; };
|
D6DD353D22F28CD000A9563A /* ContentWarningCopyMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD353C22F28CD000A9563A /* ContentWarningCopyMode.swift */; };
|
||||||
|
@ -627,7 +622,7 @@
|
||||||
D68ACE5C279B1ABA001CE8EB /* AssetPickerControlCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetPickerControlCollectionViewCell.swift; sourceTree = "<group>"; };
|
D68ACE5C279B1ABA001CE8EB /* AssetPickerControlCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetPickerControlCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||||
D68C2AE225869BAB00548EFF /* AuxiliarySceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuxiliarySceneDelegate.swift; sourceTree = "<group>"; };
|
D68C2AE225869BAB00548EFF /* AuxiliarySceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuxiliarySceneDelegate.swift; sourceTree = "<group>"; };
|
||||||
D68E525A24A3D77E0054355A /* TuskerRootViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TuskerRootViewController.swift; sourceTree = "<group>"; };
|
D68E525A24A3D77E0054355A /* TuskerRootViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TuskerRootViewController.swift; sourceTree = "<group>"; };
|
||||||
D68E525C24A3E8F00054355A /* InlineTrendsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlineTrendsViewController.swift; sourceTree = "<group>"; };
|
D68E525C24A3E8F00054355A /* SearchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewController.swift; sourceTree = "<group>"; };
|
||||||
D68E6F58253C9969001A1B4C /* MultiSourceEmojiLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiSourceEmojiLabel.swift; sourceTree = "<group>"; };
|
D68E6F58253C9969001A1B4C /* MultiSourceEmojiLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiSourceEmojiLabel.swift; sourceTree = "<group>"; };
|
||||||
D68E6F5E253C9B2D001A1B4C /* BaseEmojiLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseEmojiLabel.swift; sourceTree = "<group>"; };
|
D68E6F5E253C9B2D001A1B4C /* BaseEmojiLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseEmojiLabel.swift; sourceTree = "<group>"; };
|
||||||
D68FEC4E232C5BC300C84F23 /* SegmentedPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SegmentedPageViewController.swift; sourceTree = "<group>"; };
|
D68FEC4E232C5BC300C84F23 /* SegmentedPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SegmentedPageViewController.swift; sourceTree = "<group>"; };
|
||||||
|
@ -706,10 +701,6 @@
|
||||||
D6BED173212667E900F02DA0 /* TimelineStatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStatusTableViewCell.swift; sourceTree = "<group>"; };
|
D6BED173212667E900F02DA0 /* TimelineStatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStatusTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
D6C143D9253510F4007DC240 /* ComposeEmojiTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeEmojiTextField.swift; sourceTree = "<group>"; };
|
D6C143D9253510F4007DC240 /* ComposeEmojiTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeEmojiTextField.swift; sourceTree = "<group>"; };
|
||||||
D6C1B2072545D1EC00DAAA66 /* StatusCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusCardView.swift; sourceTree = "<group>"; };
|
D6C1B2072545D1EC00DAAA66 /* StatusCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusCardView.swift; sourceTree = "<group>"; };
|
||||||
D6C3F4F4298ED0890009FCFF /* LocalPredicateStatusesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalPredicateStatusesViewController.swift; sourceTree = "<group>"; };
|
|
||||||
D6C3F4F6298ED7F70009FCFF /* FavoritesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesViewController.swift; sourceTree = "<group>"; };
|
|
||||||
D6C3F4F8298EDBF20009FCFF /* ConversationTree.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationTree.swift; sourceTree = "<group>"; };
|
|
||||||
D6C3F4FA299035650009FCFF /* TrendsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendsViewController.swift; sourceTree = "<group>"; };
|
|
||||||
D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TuskerNavigationDelegate.swift; sourceTree = "<group>"; };
|
D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TuskerNavigationDelegate.swift; sourceTree = "<group>"; };
|
||||||
D6C693FB2162FE6F007D6A6D /* LoadingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingViewController.swift; sourceTree = "<group>"; };
|
D6C693FB2162FE6F007D6A6D /* LoadingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingViewController.swift; sourceTree = "<group>"; };
|
||||||
D6C693FD2162FEEA007D6A6D /* UIViewController+Children.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Children.swift"; sourceTree = "<group>"; };
|
D6C693FD2162FEEA007D6A6D /* UIViewController+Children.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Children.swift"; sourceTree = "<group>"; };
|
||||||
|
@ -743,7 +734,6 @@
|
||||||
D6D7069F29466649000827ED /* ScrollingSegmentedControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollingSegmentedControl.swift; sourceTree = "<group>"; };
|
D6D7069F29466649000827ED /* ScrollingSegmentedControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollingSegmentedControl.swift; sourceTree = "<group>"; };
|
||||||
D6D706A62948D4D0000827ED /* TimlineState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimlineState.swift; sourceTree = "<group>"; };
|
D6D706A62948D4D0000827ED /* TimlineState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimlineState.swift; sourceTree = "<group>"; };
|
||||||
D6D706A829498C82000827ED /* Tusker.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Tusker.xcconfig; sourceTree = "<group>"; };
|
D6D706A829498C82000827ED /* Tusker.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Tusker.xcconfig; sourceTree = "<group>"; };
|
||||||
D6D9498E298EB79400C59229 /* CopyableLable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyableLable.swift; sourceTree = "<group>"; };
|
|
||||||
D6DD2A3E273C1F4900386A6C /* ComposeAttachmentImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeAttachmentImage.swift; sourceTree = "<group>"; };
|
D6DD2A3E273C1F4900386A6C /* ComposeAttachmentImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeAttachmentImage.swift; sourceTree = "<group>"; };
|
||||||
D6DD2A44273D6C5700386A6C /* GIFImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GIFImageView.swift; sourceTree = "<group>"; };
|
D6DD2A44273D6C5700386A6C /* GIFImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GIFImageView.swift; sourceTree = "<group>"; };
|
||||||
D6DD353C22F28CD000A9563A /* ContentWarningCopyMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentWarningCopyMode.swift; sourceTree = "<group>"; };
|
D6DD353C22F28CD000A9563A /* ContentWarningCopyMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentWarningCopyMode.swift; sourceTree = "<group>"; };
|
||||||
|
@ -927,7 +917,6 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
D6C82B4025C5BB7E0017F1E6 /* ExploreViewController.swift */,
|
D6C82B4025C5BB7E0017F1E6 /* ExploreViewController.swift */,
|
||||||
D68E525C24A3E8F00054355A /* InlineTrendsViewController.swift */,
|
|
||||||
D6945C3323AC6431005C403C /* AddSavedHashtagViewController.swift */,
|
D6945C3323AC6431005C403C /* AddSavedHashtagViewController.swift */,
|
||||||
D6093F9A25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift */,
|
D6093F9A25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift */,
|
||||||
D6945C3923AC75E2005C403C /* FindInstanceViewController.swift */,
|
D6945C3923AC75E2005C403C /* FindInstanceViewController.swift */,
|
||||||
|
@ -942,19 +931,16 @@
|
||||||
D693A72925CF8C1E003A14E2 /* ProfileDirectoryViewController.swift */,
|
D693A72925CF8C1E003A14E2 /* ProfileDirectoryViewController.swift */,
|
||||||
D693A72D25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift */,
|
D693A72D25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift */,
|
||||||
D693A72E25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.xib */,
|
D693A72E25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.xib */,
|
||||||
D6C3F4FA299035650009FCFF /* TrendsViewController.swift */,
|
|
||||||
);
|
);
|
||||||
path = Explore;
|
path = Explore;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
D627944823A6AD5100D38C68 /* Local Predicate Statuses List */ = {
|
D627944823A6AD5100D38C68 /* Bookmarks */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
D6C3F4F4298ED0890009FCFF /* LocalPredicateStatusesViewController.swift */,
|
|
||||||
D6DD8FFE2984D327002AD3FD /* BookmarksViewController.swift */,
|
D6DD8FFE2984D327002AD3FD /* BookmarksViewController.swift */,
|
||||||
D6C3F4F6298ED7F70009FCFF /* FavoritesViewController.swift */,
|
|
||||||
);
|
);
|
||||||
path = "Local Predicate Statuses List";
|
path = Bookmarks;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
D627944B23A9A02400D38C68 /* Lists */ = {
|
D627944B23A9A02400D38C68 /* Lists */ = {
|
||||||
|
@ -1017,6 +1003,7 @@
|
||||||
D6A3BC822321F69400FD64D5 /* Account List */,
|
D6A3BC822321F69400FD64D5 /* Account List */,
|
||||||
D6B053A023BD2BED00A066FA /* Asset Picker */,
|
D6B053A023BD2BED00A066FA /* Asset Picker */,
|
||||||
0411610522B457290030A9B7 /* Attachment Gallery */,
|
0411610522B457290030A9B7 /* Attachment Gallery */,
|
||||||
|
D627944823A6AD5100D38C68 /* Bookmarks */,
|
||||||
D641C787213DD862004B4513 /* Compose */,
|
D641C787213DD862004B4513 /* Compose */,
|
||||||
D641C785213DD83B004B4513 /* Conversation */,
|
D641C785213DD83B004B4513 /* Conversation */,
|
||||||
D6F2E960249E772F005846BB /* Crash Reporter */,
|
D6F2E960249E772F005846BB /* Crash Reporter */,
|
||||||
|
@ -1025,7 +1012,6 @@
|
||||||
D61F759729384D4200C0B37F /* Customize Timelines */,
|
D61F759729384D4200C0B37F /* Customize Timelines */,
|
||||||
D641C788213DD86D004B4513 /* Large Image */,
|
D641C788213DD86D004B4513 /* Large Image */,
|
||||||
D627944B23A9A02400D38C68 /* Lists */,
|
D627944B23A9A02400D38C68 /* Lists */,
|
||||||
D627944823A6AD5100D38C68 /* Local Predicate Statuses List */,
|
|
||||||
D641C782213DD7F0004B4513 /* Main */,
|
D641C782213DD7F0004B4513 /* Main */,
|
||||||
D6F6A555291F4F0C00F496A8 /* Mute */,
|
D6F6A555291F4F0C00F496A8 /* Mute */,
|
||||||
D641C786213DD852004B4513 /* Notifications */,
|
D641C786213DD852004B4513 /* Notifications */,
|
||||||
|
@ -1094,7 +1080,6 @@
|
||||||
D65B4B6329771EFF00DABDFB /* ConversationViewController.swift */,
|
D65B4B6329771EFF00DABDFB /* ConversationViewController.swift */,
|
||||||
D601FA5C297B2E6F00A8E8B5 /* ConversationCollectionViewController.swift */,
|
D601FA5C297B2E6F00A8E8B5 /* ConversationCollectionViewController.swift */,
|
||||||
D601FA5E297B339100A8E8B5 /* ExpandThreadCollectionViewCell.swift */,
|
D601FA5E297B339100A8E8B5 /* ExpandThreadCollectionViewCell.swift */,
|
||||||
D6C3F4F8298EDBF20009FCFF /* ConversationTree.swift */,
|
|
||||||
);
|
);
|
||||||
path = Conversation;
|
path = Conversation;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1414,6 +1399,7 @@
|
||||||
D6BC9DD8232D8BCA002CA326 /* Search */ = {
|
D6BC9DD8232D8BCA002CA326 /* Search */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
D68E525C24A3E8F00054355A /* SearchViewController.swift */,
|
||||||
D6BC9DD9232D8BE5002CA326 /* SearchResultsViewController.swift */,
|
D6BC9DD9232D8BE5002CA326 /* SearchResultsViewController.swift */,
|
||||||
);
|
);
|
||||||
path = Search;
|
path = Search;
|
||||||
|
@ -1428,7 +1414,6 @@
|
||||||
D6ADB6EF28ED1F25009924AB /* CachedImageView.swift */,
|
D6ADB6EF28ED1F25009924AB /* CachedImageView.swift */,
|
||||||
D6895DC328D65342006341DA /* ConfirmReblogStatusPreviewView.swift */,
|
D6895DC328D65342006341DA /* ConfirmReblogStatusPreviewView.swift */,
|
||||||
D620483523D38075008A63EF /* ContentTextView.swift */,
|
D620483523D38075008A63EF /* ContentTextView.swift */,
|
||||||
D6D9498E298EB79400C59229 /* CopyableLable.swift */,
|
|
||||||
D6E426B225337C7000C02E1C /* CustomEmojiImageView.swift */,
|
D6E426B225337C7000C02E1C /* CustomEmojiImageView.swift */,
|
||||||
D6969E9F240C8384002843CE /* EmojiLabel.swift */,
|
D6969E9F240C8384002843CE /* EmojiLabel.swift */,
|
||||||
D61F75B8293C15A000C0B37F /* ZeroHeightCollectionViewCell.swift */,
|
D61F75B8293C15A000C0B37F /* ZeroHeightCollectionViewCell.swift */,
|
||||||
|
@ -1951,7 +1936,6 @@
|
||||||
D6285B5321EA708700FE4B39 /* StatusFormat.swift in Sources */,
|
D6285B5321EA708700FE4B39 /* StatusFormat.swift in Sources */,
|
||||||
D6DD353D22F28CD000A9563A /* ContentWarningCopyMode.swift in Sources */,
|
D6DD353D22F28CD000A9563A /* ContentWarningCopyMode.swift in Sources */,
|
||||||
0427033A22B31269000D31B6 /* AdvancedPrefsView.swift in Sources */,
|
0427033A22B31269000D31B6 /* AdvancedPrefsView.swift in Sources */,
|
||||||
D6C3F4F5298ED0890009FCFF /* LocalPredicateStatusesViewController.swift in Sources */,
|
|
||||||
D662AEEF263A3B880082A153 /* PollFinishedTableViewCell.swift in Sources */,
|
D662AEEF263A3B880082A153 /* PollFinishedTableViewCell.swift in Sources */,
|
||||||
D626493C23C1000300612E6E /* AlbumTableViewCell.swift in Sources */,
|
D626493C23C1000300612E6E /* AlbumTableViewCell.swift in Sources */,
|
||||||
D62275A624F1C81800B82A16 /* ComposeReplyView.swift in Sources */,
|
D62275A624F1C81800B82A16 /* ComposeReplyView.swift in Sources */,
|
||||||
|
@ -1962,7 +1946,7 @@
|
||||||
D61F759229365C6C00C0B37F /* CollectionViewController.swift in Sources */,
|
D61F759229365C6C00C0B37F /* CollectionViewController.swift in Sources */,
|
||||||
D6DD8FFD298495A8002AD3FD /* LogoutService.swift in Sources */,
|
D6DD8FFD298495A8002AD3FD /* LogoutService.swift in Sources */,
|
||||||
04DACE8C212CB14B009840C4 /* MainTabBarViewController.swift in Sources */,
|
04DACE8C212CB14B009840C4 /* MainTabBarViewController.swift in Sources */,
|
||||||
D68E525D24A3E8F00054355A /* InlineTrendsViewController.swift in Sources */,
|
D68E525D24A3E8F00054355A /* SearchViewController.swift in Sources */,
|
||||||
D61F75BB293C183100C0B37F /* HTMLConverter.swift in Sources */,
|
D61F75BB293C183100C0B37F /* HTMLConverter.swift in Sources */,
|
||||||
D61F75A5293ABD6F00C0B37F /* EditFilterView.swift in Sources */,
|
D61F75A5293ABD6F00C0B37F /* EditFilterView.swift in Sources */,
|
||||||
D6BC9DB1232C61BC002CA326 /* NotificationsPageViewController.swift in Sources */,
|
D6BC9DB1232C61BC002CA326 /* NotificationsPageViewController.swift in Sources */,
|
||||||
|
@ -2031,7 +2015,6 @@
|
||||||
D6E77D0F286F773900D8B732 /* SplitNavigationController.swift in Sources */,
|
D6E77D0F286F773900D8B732 /* SplitNavigationController.swift in Sources */,
|
||||||
D6B30E09254BAF63009CAEE5 /* ImageGrayscalifier.swift in Sources */,
|
D6B30E09254BAF63009CAEE5 /* ImageGrayscalifier.swift in Sources */,
|
||||||
D6A6C10F25B62D2400298D0F /* DiskCache.swift in Sources */,
|
D6A6C10F25B62D2400298D0F /* DiskCache.swift in Sources */,
|
||||||
D6C3F4F9298EDBF20009FCFF /* ConversationTree.swift in Sources */,
|
|
||||||
D6B81F3C2560365300F6E31D /* RefreshableViewController.swift in Sources */,
|
D6B81F3C2560365300F6E31D /* RefreshableViewController.swift in Sources */,
|
||||||
D646C958213B367000269FB5 /* LargeImageShrinkAnimationController.swift in Sources */,
|
D646C958213B367000269FB5 /* LargeImageShrinkAnimationController.swift in Sources */,
|
||||||
D6674AEA23341F7600E8DF94 /* AppShortcutItems.swift in Sources */,
|
D6674AEA23341F7600E8DF94 /* AppShortcutItems.swift in Sources */,
|
||||||
|
@ -2134,7 +2117,6 @@
|
||||||
D663626221360B1900C9CBA2 /* Preferences.swift in Sources */,
|
D663626221360B1900C9CBA2 /* Preferences.swift in Sources */,
|
||||||
D626493823C0FD0000612E6E /* AllPhotosTableViewCell.swift in Sources */,
|
D626493823C0FD0000612E6E /* AllPhotosTableViewCell.swift in Sources */,
|
||||||
D627943B23A55BA600D38C68 /* NavigableTableViewCell.swift in Sources */,
|
D627943B23A55BA600D38C68 /* NavigableTableViewCell.swift in Sources */,
|
||||||
D6D9498F298EB79400C59229 /* CopyableLable.swift in Sources */,
|
|
||||||
D6333B792139AEFD00CE884A /* Date+TimeAgo.swift in Sources */,
|
D6333B792139AEFD00CE884A /* Date+TimeAgo.swift in Sources */,
|
||||||
D6D706A029466649000827ED /* ScrollingSegmentedControl.swift in Sources */,
|
D6D706A029466649000827ED /* ScrollingSegmentedControl.swift in Sources */,
|
||||||
D653F411267D1E32004E32B1 /* DiffableTimelineLikeTableViewController.swift in Sources */,
|
D653F411267D1E32004E32B1 /* DiffableTimelineLikeTableViewController.swift in Sources */,
|
||||||
|
@ -2165,7 +2147,6 @@
|
||||||
D6D706A72948D4D0000827ED /* TimlineState.swift in Sources */,
|
D6D706A72948D4D0000827ED /* TimlineState.swift in Sources */,
|
||||||
D66C900B28DAB7FD00217BF2 /* TimelineViewController.swift in Sources */,
|
D66C900B28DAB7FD00217BF2 /* TimelineViewController.swift in Sources */,
|
||||||
D61F758D2933C69C00C0B37F /* StatusUpdatedNotificationTableViewCell.swift in Sources */,
|
D61F758D2933C69C00C0B37F /* StatusUpdatedNotificationTableViewCell.swift in Sources */,
|
||||||
D6C3F4F7298ED7F70009FCFF /* FavoritesViewController.swift in Sources */,
|
|
||||||
D6AEBB432321685E00E5038B /* OpenInSafariActivity.swift in Sources */,
|
D6AEBB432321685E00E5038B /* OpenInSafariActivity.swift in Sources */,
|
||||||
D6C693FE2162FEEA007D6A6D /* UIViewController+Children.swift in Sources */,
|
D6C693FE2162FEEA007D6A6D /* UIViewController+Children.swift in Sources */,
|
||||||
D61A45EA28DF51EE002BE511 /* TimelineLikeCollectionViewController.swift in Sources */,
|
D61A45EA28DF51EE002BE511 /* TimelineLikeCollectionViewController.swift in Sources */,
|
||||||
|
@ -2181,7 +2162,6 @@
|
||||||
D6E77D0B286D426E00D8B732 /* TrendingLinkCardCollectionViewCell.swift in Sources */,
|
D6E77D0B286D426E00D8B732 /* TrendingLinkCardCollectionViewCell.swift in Sources */,
|
||||||
D6114E1127F899B30080E273 /* TrendingLinksViewController.swift in Sources */,
|
D6114E1127F899B30080E273 /* TrendingLinksViewController.swift in Sources */,
|
||||||
D65B4B58297203A700DABDFB /* ReportSelectRulesView.swift in Sources */,
|
D65B4B58297203A700DABDFB /* ReportSelectRulesView.swift in Sources */,
|
||||||
D6C3F4FB299035650009FCFF /* TrendsViewController.swift in Sources */,
|
|
||||||
D6B81F442560390300F6E31D /* MenuController.swift in Sources */,
|
D6B81F442560390300F6E31D /* MenuController.swift in Sources */,
|
||||||
D65B4B542971F71D00DABDFB /* EditedReport.swift in Sources */,
|
D65B4B542971F71D00DABDFB /* EditedReport.swift in Sources */,
|
||||||
D65234E12561AA68001AF9CF /* NotificationsTableViewController.swift in Sources */,
|
D65234E12561AA68001AF9CF /* NotificationsTableViewController.swift in Sources */,
|
||||||
|
@ -2408,7 +2388,7 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 72;
|
CURRENT_PROJECT_VERSION = 71;
|
||||||
INFOPLIST_FILE = Tusker/Info.plist;
|
INFOPLIST_FILE = Tusker/Info.plist;
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
|
@ -2473,7 +2453,7 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
|
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 72;
|
CURRENT_PROJECT_VERSION = 71;
|
||||||
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||||
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
|
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
|
||||||
|
@ -2624,7 +2604,7 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 72;
|
CURRENT_PROJECT_VERSION = 71;
|
||||||
INFOPLIST_FILE = Tusker/Info.plist;
|
INFOPLIST_FILE = Tusker/Info.plist;
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
|
@ -2652,7 +2632,7 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 72;
|
CURRENT_PROJECT_VERSION = 71;
|
||||||
INFOPLIST_FILE = Tusker/Info.plist;
|
INFOPLIST_FILE = Tusker/Info.plist;
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
|
@ -2757,7 +2737,7 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
|
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 72;
|
CURRENT_PROJECT_VERSION = 71;
|
||||||
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||||
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
|
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
|
||||||
|
@ -2783,7 +2763,7 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
|
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 72;
|
CURRENT_PROJECT_VERSION = 71;
|
||||||
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||||
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
|
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
|
||||||
|
|
|
@ -32,38 +32,46 @@ class ImageCache {
|
||||||
}
|
}
|
||||||
|
|
||||||
func get(_ url: URL, loadOriginal: Bool = false, completion: ((Data?, UIImage?) -> Void)?) -> Request? {
|
func get(_ url: URL, loadOriginal: Bool = false, completion: ((Data?, UIImage?) -> Void)?) -> Request? {
|
||||||
if !ImageCache.disableCaching,
|
let key = url.absoluteString
|
||||||
let entry = try? cache.get(url.absoluteString, loadOriginal: loadOriginal) {
|
|
||||||
completion?(entry.data, entry.image)
|
let wrappedCompletion: ((Data?, UIImage?) -> Void)?
|
||||||
return nil
|
if let completion = completion {
|
||||||
} else {
|
wrappedCompletion = { (data, image) in
|
||||||
return Task.detached(priority: .userInitiated) {
|
if let image {
|
||||||
let result = await self.fetch(url: url)
|
if !loadOriginal,
|
||||||
switch result {
|
let size = self.desiredPixelSize {
|
||||||
case .data(let data):
|
image.prepareThumbnail(of: size) {
|
||||||
completion?(data, nil)
|
completion(data, $0)
|
||||||
case .dataAndImage(let data, let image):
|
}
|
||||||
completion?(data, image)
|
} else {
|
||||||
case .none:
|
image.prepareForDisplay {
|
||||||
completion?(nil, nil)
|
completion(data, $0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
completion(data, image)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
wrappedCompletion = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ImageCache.disableCaching,
|
||||||
|
let entry = try? cache.get(key, loadOriginal: loadOriginal) {
|
||||||
|
wrappedCompletion?(entry.data, entry.image)
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
let task = dataTask(url: url, completion: wrappedCompletion)
|
||||||
|
task.resume()
|
||||||
|
return task
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func get(_ url: URL, loadOriginal: Bool = false) async -> (Data?, UIImage?) {
|
func get(_ url: URL, loadOriginal: Bool = false) async -> (Data?, UIImage?) {
|
||||||
if !ImageCache.disableCaching,
|
// todo: this should integrate with the task cancellation mechanism somehow
|
||||||
let entry = try? cache.get(url.absoluteString, loadOriginal: loadOriginal) {
|
return await withCheckedContinuation { continuation in
|
||||||
return (entry.data, entry.image)
|
_ = get(url, loadOriginal: loadOriginal) { data, image in
|
||||||
} else {
|
continuation.resume(returning: (data, image))
|
||||||
let result = await self.fetch(url: url)
|
|
||||||
switch result {
|
|
||||||
case .data(let data):
|
|
||||||
return (data, nil)
|
|
||||||
case .dataAndImage(let data, let image):
|
|
||||||
return (data, image)
|
|
||||||
case .none:
|
|
||||||
return (nil, nil)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -73,28 +81,21 @@ class ImageCache {
|
||||||
guard !ImageCache.disableCaching else { return }
|
guard !ImageCache.disableCaching else { return }
|
||||||
|
|
||||||
if !((try? cache.has(url.absoluteString)) ?? false) {
|
if !((try? cache.has(url.absoluteString)) ?? false) {
|
||||||
Task.detached(priority: .medium) {
|
let task = dataTask(url: url, completion: nil)
|
||||||
_ = await self.fetch(url: url)
|
task.resume()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func fetch(url: URL) async -> FetchResult {
|
private func dataTask(url: URL, completion: ((Data?, UIImage?) -> Void)?) -> URLSessionDataTask {
|
||||||
guard let (data, _) = try? await URLSession.shared.data(from: url) else {
|
return URLSession.shared.dataTask(with: url) { data, response, error in
|
||||||
return .none
|
guard error == nil,
|
||||||
|
let data else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let image = UIImage(data: data)
|
||||||
|
try? self.cache.set(url.absoluteString, data: data, image: image)
|
||||||
|
completion?(data, image)
|
||||||
}
|
}
|
||||||
guard let image = UIImage(data: data) else {
|
|
||||||
try? cache.set(url.absoluteString, data: data, image: nil)
|
|
||||||
return .data(data)
|
|
||||||
}
|
|
||||||
let preparedImage: UIImage?
|
|
||||||
if let desiredPixelSize {
|
|
||||||
preparedImage = await image.byPreparingThumbnail(ofSize: desiredPixelSize)
|
|
||||||
} else {
|
|
||||||
preparedImage = await image.byPreparingForDisplay()
|
|
||||||
}
|
|
||||||
try? cache.set(url.absoluteString, data: data, image: preparedImage ?? image)
|
|
||||||
return .dataAndImage(data, preparedImage ?? image)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getData(_ url: URL) -> Data? {
|
func getData(_ url: URL) -> Data? {
|
||||||
|
@ -113,12 +114,6 @@ class ImageCache {
|
||||||
return cache.disk?.getSizeInBytes()
|
return cache.disk?.getSizeInBytes()
|
||||||
}
|
}
|
||||||
|
|
||||||
typealias Request = Task<Void, Never>
|
typealias Request = URLSessionDataTask
|
||||||
|
|
||||||
enum FetchResult {
|
|
||||||
case data(Data)
|
|
||||||
case dataAndImage(Data, UIImage)
|
|
||||||
case none
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,24 +13,20 @@ import os
|
||||||
// to make the lock semantics more clear
|
// to make the lock semantics more clear
|
||||||
@available(iOS, obsoleted: 16.0)
|
@available(iOS, obsoleted: 16.0)
|
||||||
class MultiThreadDictionary<Key: Hashable & Sendable, Value: Sendable> {
|
class MultiThreadDictionary<Key: Hashable & Sendable, Value: Sendable> {
|
||||||
private let lock: any Lock<[Key: Value]>
|
private let lock: LockHolder<[AnyHashable: Any]>
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
if #available(iOS 16.0, *) {
|
self.lock = LockHolder(initialState: [:])
|
||||||
self.lock = OSAllocatedUnfairLock(initialState: [:])
|
|
||||||
} else {
|
|
||||||
self.lock = UnfairLock(initialState: [:])
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
subscript(key: Key) -> Value? {
|
subscript(key: Key) -> Value? {
|
||||||
get {
|
get {
|
||||||
return lock.withLock { dict in
|
return try! lock.withLock { dict in
|
||||||
dict[key]
|
dict[key]
|
||||||
}
|
} as! Value?
|
||||||
}
|
}
|
||||||
set(value) {
|
set(value) {
|
||||||
_ = lock.withLock { dict in
|
_ = try! lock.withLock { dict in
|
||||||
dict[key] = value
|
dict[key] = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -38,21 +34,40 @@ class MultiThreadDictionary<Key: Hashable & Sendable, Value: Sendable> {
|
||||||
|
|
||||||
/// If the result of this function is unused, it is preferable to use `removeValueWithoutReturning` as it executes asynchronously and doesn't block the calling thread.
|
/// If the result of this function is unused, it is preferable to use `removeValueWithoutReturning` as it executes asynchronously and doesn't block the calling thread.
|
||||||
func removeValue(forKey key: Key) -> Value? {
|
func removeValue(forKey key: Key) -> Value? {
|
||||||
return lock.withLock { dict in
|
return try! lock.withLock { dict in
|
||||||
dict.removeValue(forKey: key)
|
dict.removeValue(forKey: key)
|
||||||
}
|
} as! Value?
|
||||||
}
|
}
|
||||||
|
|
||||||
func contains(key: Key) -> Bool {
|
func contains(key: Key) -> Bool {
|
||||||
return lock.withLock { dict in
|
return try! lock.withLock { dict in
|
||||||
dict.keys.contains(key)
|
dict.keys.contains(key)
|
||||||
}
|
} as! Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: this should really be throws/rethrows but the stupid type-erased lock makes that not possible
|
// TODO: this should really be throws/rethrows but the stupid type-erased lock makes that not possible
|
||||||
func withLock<R>(_ body: @Sendable (inout [Key: Value]) throws -> R) rethrows -> R where R: Sendable {
|
func withLock<R>(_ body: @Sendable (inout [Key: Value]) -> R) -> R where R: Sendable {
|
||||||
return try lock.withLock { dict in
|
return try! lock.withLock { dict in
|
||||||
return try body(&dict)
|
var downcasted = dict as! [Key: Value]
|
||||||
|
defer { dict = downcasted }
|
||||||
|
return body(&downcasted)
|
||||||
|
} as! R
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// this type erased struct is necessary due to a compiler bug with stored constrained existential types
|
||||||
|
// see https://github.com/apple/swift/issues/61403
|
||||||
|
// see #178
|
||||||
|
fileprivate struct LockHolder<State> {
|
||||||
|
let withLock: (_ body: @Sendable (inout State) throws -> any Sendable) throws -> any Sendable
|
||||||
|
|
||||||
|
init(initialState: State) {
|
||||||
|
if #available(iOS 16.0, *) {
|
||||||
|
let lock = OSAllocatedUnfairLock(initialState: initialState)
|
||||||
|
self.withLock = lock.withLock(_:)
|
||||||
|
} else {
|
||||||
|
let lock = UnfairLock(initialState: initialState)
|
||||||
|
self.withLock = lock.withLock(_:)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -80,7 +80,7 @@ class Preferences: Codable, ObservableObject {
|
||||||
self.defaultNotificationsMode = try container.decode(NotificationsMode.self, forKey: .defaultNotificationsType)
|
self.defaultNotificationsMode = try container.decode(NotificationsMode.self, forKey: .defaultNotificationsType)
|
||||||
self.grayscaleImages = try container.decodeIfPresent(Bool.self, forKey: .grayscaleImages) ?? false
|
self.grayscaleImages = try container.decodeIfPresent(Bool.self, forKey: .grayscaleImages) ?? false
|
||||||
self.disableInfiniteScrolling = try container.decodeIfPresent(Bool.self, forKey: .disableInfiniteScrolling) ?? false
|
self.disableInfiniteScrolling = try container.decodeIfPresent(Bool.self, forKey: .disableInfiniteScrolling) ?? false
|
||||||
self.hideTrends = try container.decodeIfPresent(Bool.self, forKey: .hideTrends) ?? false
|
self.hideDiscover = try container.decodeIfPresent(Bool.self, forKey: .hideDiscover) ?? false
|
||||||
|
|
||||||
self.statusContentType = try container.decode(StatusContentType.self, forKey: .statusContentType)
|
self.statusContentType = try container.decode(StatusContentType.self, forKey: .statusContentType)
|
||||||
|
|
||||||
|
@ -130,7 +130,7 @@ class Preferences: Codable, ObservableObject {
|
||||||
try container.encode(defaultNotificationsMode, forKey: .defaultNotificationsType)
|
try container.encode(defaultNotificationsMode, forKey: .defaultNotificationsType)
|
||||||
try container.encode(grayscaleImages, forKey: .grayscaleImages)
|
try container.encode(grayscaleImages, forKey: .grayscaleImages)
|
||||||
try container.encode(disableInfiniteScrolling, forKey: .disableInfiniteScrolling)
|
try container.encode(disableInfiniteScrolling, forKey: .disableInfiniteScrolling)
|
||||||
try container.encode(hideTrends, forKey: .hideTrends)
|
try container.encode(hideDiscover, forKey: .hideDiscover)
|
||||||
|
|
||||||
try container.encode(statusContentType, forKey: .statusContentType)
|
try container.encode(statusContentType, forKey: .statusContentType)
|
||||||
|
|
||||||
|
@ -190,7 +190,7 @@ class Preferences: Codable, ObservableObject {
|
||||||
@Published var defaultNotificationsMode = NotificationsMode.allNotifications
|
@Published var defaultNotificationsMode = NotificationsMode.allNotifications
|
||||||
@Published var grayscaleImages = false
|
@Published var grayscaleImages = false
|
||||||
@Published var disableInfiniteScrolling = false
|
@Published var disableInfiniteScrolling = false
|
||||||
@Published var hideTrends = false
|
@Published var hideDiscover = false
|
||||||
|
|
||||||
// MARK: Advanced
|
// MARK: Advanced
|
||||||
@Published var statusContentType: StatusContentType = .plain
|
@Published var statusContentType: StatusContentType = .plain
|
||||||
|
@ -241,7 +241,7 @@ class Preferences: Codable, ObservableObject {
|
||||||
case defaultNotificationsType
|
case defaultNotificationsType
|
||||||
case grayscaleImages
|
case grayscaleImages
|
||||||
case disableInfiniteScrolling
|
case disableInfiniteScrolling
|
||||||
case hideTrends = "hideDiscover"
|
case hideDiscover
|
||||||
|
|
||||||
case statusContentType
|
case statusContentType
|
||||||
|
|
||||||
|
|
|
@ -82,7 +82,7 @@ class AuxiliarySceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||||
return NotificationsPageViewController(initialMode: mode, mastodonController: mastodonController)
|
return NotificationsPageViewController(initialMode: mode, mastodonController: mastodonController)
|
||||||
|
|
||||||
case .search:
|
case .search:
|
||||||
return InlineTrendsViewController(mastodonController: mastodonController)
|
return SearchViewController(mastodonController: mastodonController)
|
||||||
|
|
||||||
case .bookmarks:
|
case .bookmarks:
|
||||||
return BookmarksViewController(mastodonController: mastodonController)
|
return BookmarksViewController(mastodonController: mastodonController)
|
||||||
|
|
|
@ -1,23 +1,20 @@
|
||||||
//
|
//
|
||||||
// LocalPredicateStatusesViewController.swift
|
// BookmarksViewController.swift
|
||||||
// Tusker
|
// Tusker
|
||||||
//
|
//
|
||||||
// Created by Shadowfacts on 2/4/23.
|
// Created by Shadowfacts on 12/15/19.
|
||||||
// Copyright © 2023 Shadowfacts. All rights reserved.
|
// Copyright © 2019 Shadowfacts. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
import CoreData
|
import CoreData
|
||||||
|
|
||||||
class LocalPredicateStatusesViewController: UIViewController, CollectionViewController, RefreshableViewController {
|
class BookmarksViewController: UIViewController, CollectionViewController, RefreshableViewController {
|
||||||
|
|
||||||
private static let pageSize = 40
|
private static let pageSize = 40
|
||||||
|
|
||||||
let mastodonController: MastodonController
|
let mastodonController: MastodonController
|
||||||
private let predicate: (StatusMO) -> Bool
|
|
||||||
private let predicateTitle: String
|
|
||||||
private let request: (RequestRange) -> Request<[Status]>
|
|
||||||
|
|
||||||
var collectionView: UICollectionView! {
|
var collectionView: UICollectionView! {
|
||||||
view as? UICollectionView
|
view as? UICollectionView
|
||||||
|
@ -28,15 +25,12 @@ class LocalPredicateStatusesViewController: UIViewController, CollectionViewCont
|
||||||
private var newer: RequestRange?
|
private var newer: RequestRange?
|
||||||
private var older: RequestRange?
|
private var older: RequestRange?
|
||||||
|
|
||||||
init(predicate: @escaping (StatusMO) -> Bool, predicateTitle: String, request: @escaping (RequestRange) -> Request<[Status]>, mastodonController: MastodonController) {
|
init(mastodonController: MastodonController) {
|
||||||
self.mastodonController = mastodonController
|
self.mastodonController = mastodonController
|
||||||
self.predicate = predicate
|
|
||||||
self.predicateTitle = predicateTitle
|
|
||||||
self.request = request
|
|
||||||
|
|
||||||
super.init(nibName: nil, bundle: nil)
|
super.init(nibName: nil, bundle: nil)
|
||||||
|
|
||||||
self.title = predicateTitle
|
title = NSLocalizedString("Bookmarks", comment: "bookmarks screen title")
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
|
@ -56,7 +50,7 @@ class LocalPredicateStatusesViewController: UIViewController, CollectionViewCont
|
||||||
return sectionConfig
|
return sectionConfig
|
||||||
}
|
}
|
||||||
var config = sectionConfig
|
var config = sectionConfig
|
||||||
if item.hideSeparators {
|
if item.hideIndicators {
|
||||||
config.topSeparatorVisibility = .hidden
|
config.topSeparatorVisibility = .hidden
|
||||||
config.bottomSeparatorVisibility = .hidden
|
config.bottomSeparatorVisibility = .hidden
|
||||||
} else {
|
} else {
|
||||||
|
@ -106,7 +100,7 @@ class LocalPredicateStatusesViewController: UIViewController, CollectionViewCont
|
||||||
collectionView.refreshControl!.addTarget(self, action: #selector(refresh), for: .valueChanged)
|
collectionView.refreshControl!.addTarget(self, action: #selector(refresh), for: .valueChanged)
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
addKeyCommand(MenuController.refreshCommand(discoverabilityTitle: "Refresh \(predicateTitle)"))
|
addKeyCommand(MenuController.refreshCommand(discoverabilityTitle: "Refresh Bookmarks"))
|
||||||
|
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(handleStatusDeleted), name: .statusDeleted, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(handleStatusDeleted), name: .statusDeleted, object: nil)
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(managedObjectsDidChange), name: .NSManagedObjectContextObjectsDidChange, object: mastodonController.persistentContainer.viewContext)
|
NotificationCenter.default.addObserver(self, selector: #selector(managedObjectsDidChange), name: .NSManagedObjectContextObjectsDidChange, object: mastodonController.persistentContainer.viewContext)
|
||||||
|
@ -135,12 +129,12 @@ class LocalPredicateStatusesViewController: UIViewController, CollectionViewCont
|
||||||
state = .loadingInitial
|
state = .loadingInitial
|
||||||
|
|
||||||
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||||
snapshot.appendSections([.statuses])
|
snapshot.appendSections([.bookmarks])
|
||||||
snapshot.appendItems([.loadingIndicator])
|
snapshot.appendItems([.loadingIndicator])
|
||||||
await apply(snapshot: snapshot, animatingDifferences: false)
|
await apply(snapshot: snapshot, animatingDifferences: false)
|
||||||
|
|
||||||
do {
|
do {
|
||||||
let req = request(.count(Self.pageSize))
|
let req = Client.getBookmarks(range: .count(BookmarksViewController.pageSize))
|
||||||
let (statuses, pagination) = try await mastodonController.run(req)
|
let (statuses, pagination) = try await mastodonController.run(req)
|
||||||
newer = pagination?.newer
|
newer = pagination?.newer
|
||||||
older = pagination?.older
|
older = pagination?.older
|
||||||
|
@ -148,14 +142,14 @@ class LocalPredicateStatusesViewController: UIViewController, CollectionViewCont
|
||||||
await mastodonController.persistentContainer.addAll(statuses: statuses)
|
await mastodonController.persistentContainer.addAll(statuses: statuses)
|
||||||
|
|
||||||
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||||
snapshot.appendSections([.statuses])
|
snapshot.appendSections([.bookmarks])
|
||||||
snapshot.appendItems(statuses.map { .status(id: $0.id, state: .unknown, addedLocally: false) })
|
snapshot.appendItems(statuses.map { .status(id: $0.id, state: .unknown, addedLocally: false) })
|
||||||
await apply(snapshot: snapshot, animatingDifferences: true)
|
await apply(snapshot: snapshot, animatingDifferences: true)
|
||||||
|
|
||||||
state = .loaded
|
state = .loaded
|
||||||
|
|
||||||
} catch {
|
} catch {
|
||||||
let config = ToastConfiguration(from: error, with: "Error Loading \(predicateTitle)", in: self) { [weak self] toast in
|
let config = ToastConfiguration(from: error, with: "Error Loading Bookmarks", in: self) { [weak self] toast in
|
||||||
toast.dismissToast(animated: true)
|
toast.dismissToast(animated: true)
|
||||||
await self?.loadInitial()
|
await self?.loadInitial()
|
||||||
}
|
}
|
||||||
|
@ -180,7 +174,7 @@ class LocalPredicateStatusesViewController: UIViewController, CollectionViewCont
|
||||||
await apply(snapshot: snapshot, animatingDifferences: false)
|
await apply(snapshot: snapshot, animatingDifferences: false)
|
||||||
|
|
||||||
do {
|
do {
|
||||||
let req = request(older.withCount(Self.pageSize))
|
let req = Client.getBookmarks(range: older.withCount(BookmarksViewController.pageSize))
|
||||||
let (statuses, pagination) = try await mastodonController.run(req)
|
let (statuses, pagination) = try await mastodonController.run(req)
|
||||||
self.older = pagination?.older
|
self.older = pagination?.older
|
||||||
|
|
||||||
|
@ -190,13 +184,12 @@ class LocalPredicateStatusesViewController: UIViewController, CollectionViewCont
|
||||||
snapshot.appendItems(statuses.map { .status(id: $0.id, state: .unknown, addedLocally: false) })
|
snapshot.appendItems(statuses.map { .status(id: $0.id, state: .unknown, addedLocally: false) })
|
||||||
await apply(snapshot: snapshot, animatingDifferences: true)
|
await apply(snapshot: snapshot, animatingDifferences: true)
|
||||||
} catch {
|
} catch {
|
||||||
let config = ToastConfiguration(from: error, with: "Error Loading Older \(predicateTitle)", in: self) { [weak self] toast in
|
let config = ToastConfiguration(from: error, with: "Error Loading Older Bookmarks", in: self) { [weak self] toast in
|
||||||
toast.dismissToast(animated: true)
|
toast.dismissToast(animated: true)
|
||||||
await self?.loadOlder()
|
await self?.loadOlder()
|
||||||
}
|
}
|
||||||
showToast(configuration: config, animated: true)
|
showToast(configuration: config, animated: true)
|
||||||
|
|
||||||
var snapshot = dataSource.snapshot()
|
|
||||||
snapshot.deleteItems([.loadingIndicator])
|
snapshot.deleteItems([.loadingIndicator])
|
||||||
await apply(snapshot: snapshot, animatingDifferences: false)
|
await apply(snapshot: snapshot, animatingDifferences: false)
|
||||||
}
|
}
|
||||||
|
@ -226,11 +219,6 @@ class LocalPredicateStatusesViewController: UIViewController, CollectionViewCont
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func managedObjectsDidChange(_ notification: Foundation.Notification) {
|
@objc private func managedObjectsDidChange(_ notification: Foundation.Notification) {
|
||||||
// only perform local updates while the vc is idle
|
|
||||||
// otherwise loading the bookmarks ends up inserting them out of order
|
|
||||||
guard case .loaded = state else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var snapshot = dataSource.snapshot()
|
var snapshot = dataSource.snapshot()
|
||||||
func prepend(item: Item) {
|
func prepend(item: Item) {
|
||||||
if let first = snapshot.itemIdentifiers.first {
|
if let first = snapshot.itemIdentifiers.first {
|
||||||
|
@ -241,7 +229,7 @@ class LocalPredicateStatusesViewController: UIViewController, CollectionViewCont
|
||||||
}
|
}
|
||||||
var hasChanges = false
|
var hasChanges = false
|
||||||
if let inserted = notification.userInfo?[NSInsertedObjectsKey] as? Set<NSManagedObject> {
|
if let inserted = notification.userInfo?[NSInsertedObjectsKey] as? Set<NSManagedObject> {
|
||||||
for case let status as StatusMO in inserted where predicate(status) {
|
for case let status as StatusMO in inserted where status.bookmarked == true {
|
||||||
prepend(item: .status(id: status.id, state: .unknown, addedLocally: true))
|
prepend(item: .status(id: status.id, state: .unknown, addedLocally: true))
|
||||||
hasChanges = true
|
hasChanges = true
|
||||||
}
|
}
|
||||||
|
@ -249,11 +237,11 @@ class LocalPredicateStatusesViewController: UIViewController, CollectionViewCont
|
||||||
if let updated = notification.userInfo?[NSUpdatedObjectsKey] as? Set<NSManagedObject> {
|
if let updated = notification.userInfo?[NSUpdatedObjectsKey] as? Set<NSManagedObject> {
|
||||||
for case let status as StatusMO in updated {
|
for case let status as StatusMO in updated {
|
||||||
let item = Item.status(id: status.id, state: .unknown, addedLocally: true)
|
let item = Item.status(id: status.id, state: .unknown, addedLocally: true)
|
||||||
let exists = snapshot.itemIdentifiers.contains(item)
|
var exists = snapshot.itemIdentifiers.contains(item)
|
||||||
if predicate(status) && !exists {
|
if status.bookmarked == true && !exists {
|
||||||
prepend(item: item)
|
prepend(item: item)
|
||||||
hasChanges = true
|
hasChanges = true
|
||||||
} else if !predicate(status) && exists {
|
} else if status.bookmarked == false && exists {
|
||||||
snapshot.deleteItems([item])
|
snapshot.deleteItems([item])
|
||||||
hasChanges = true
|
hasChanges = true
|
||||||
}
|
}
|
||||||
|
@ -279,21 +267,22 @@ class LocalPredicateStatusesViewController: UIViewController, CollectionViewCont
|
||||||
state = .loadingNewer
|
state = .loadingNewer
|
||||||
|
|
||||||
Task {
|
Task {
|
||||||
|
|
||||||
do {
|
do {
|
||||||
let req = request(newer.withCount(Self.pageSize))
|
let req = Client.getBookmarks(range: newer.withCount(BookmarksViewController.pageSize))
|
||||||
let (statuses, pagination) = try await mastodonController.run(req)
|
let (statuses, pagination) = try await mastodonController.run(req)
|
||||||
self.newer = pagination?.newer
|
self.newer = pagination?.newer
|
||||||
|
|
||||||
await mastodonController.persistentContainer.addAll(statuses: statuses)
|
await mastodonController.persistentContainer.addAll(statuses: statuses)
|
||||||
|
|
||||||
var snapshot = dataSource.snapshot()
|
var snapshot = dataSource.snapshot()
|
||||||
let localItems: [String: CollapseState] = Dictionary(uniqueKeysWithValues: snapshot.itemIdentifiers.compactMap {
|
let localItems: [String: CollapseState] = Dictionary(uniqueKeysWithValues: snapshot.itemIdentifiers.compactMap({
|
||||||
if case .status(id: let id, state: let state, addedLocally: true) = $0 {
|
if case .status(id: let id, state: let state, addedLocally: true) = $0 {
|
||||||
return (id, state)
|
return (id, state)
|
||||||
} else {
|
} else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
})
|
}))
|
||||||
var newItems: [Item] = []
|
var newItems: [Item] = []
|
||||||
for status in statuses {
|
for status in statuses {
|
||||||
let state: CollapseState
|
let state: CollapseState
|
||||||
|
@ -311,9 +300,8 @@ class LocalPredicateStatusesViewController: UIViewController, CollectionViewCont
|
||||||
snapshot.appendItems(newItems)
|
snapshot.appendItems(newItems)
|
||||||
}
|
}
|
||||||
await apply(snapshot: snapshot, animatingDifferences: true)
|
await apply(snapshot: snapshot, animatingDifferences: true)
|
||||||
|
|
||||||
} catch {
|
} catch {
|
||||||
let config = ToastConfiguration(from: error, with: "Error Refreshing \(predicateTitle)", in: self) { [weak self] toast in
|
let config = ToastConfiguration(from: error, with: "Error Refreshing Bookmarks", in: self) { [weak self] toast in
|
||||||
toast.dismissToast(animated: true)
|
toast.dismissToast(animated: true)
|
||||||
self?.refresh()
|
self?.refresh()
|
||||||
}
|
}
|
||||||
|
@ -329,15 +317,15 @@ class LocalPredicateStatusesViewController: UIViewController, CollectionViewCont
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension LocalPredicateStatusesViewController {
|
extension BookmarksViewController {
|
||||||
enum Section {
|
enum Section {
|
||||||
case statuses
|
case bookmarks
|
||||||
}
|
}
|
||||||
enum Item: Equatable, Hashable {
|
enum Item: Equatable, Hashable {
|
||||||
case status(id: String, state: CollapseState, addedLocally: Bool)
|
case status(id: String, state: CollapseState, addedLocally: Bool)
|
||||||
case loadingIndicator
|
case loadingIndicator
|
||||||
|
|
||||||
var hideSeparators: Bool {
|
var hideIndicators: Bool {
|
||||||
switch self {
|
switch self {
|
||||||
case .loadingIndicator:
|
case .loadingIndicator:
|
||||||
return true
|
return true
|
||||||
|
@ -369,7 +357,7 @@ extension LocalPredicateStatusesViewController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension LocalPredicateStatusesViewController {
|
extension BookmarksViewController {
|
||||||
enum State {
|
enum State {
|
||||||
case unloaded
|
case unloaded
|
||||||
case loadingInitial
|
case loadingInitial
|
||||||
|
@ -379,7 +367,7 @@ extension LocalPredicateStatusesViewController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension LocalPredicateStatusesViewController: UICollectionViewDelegate {
|
extension BookmarksViewController: UICollectionViewDelegate {
|
||||||
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
|
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
|
||||||
if indexPath.section == 0,
|
if indexPath.section == 0,
|
||||||
indexPath.row == collectionView.numberOfItems(inSection: indexPath.section) - 1 {
|
indexPath.row == collectionView.numberOfItems(inSection: indexPath.section) - 1 {
|
||||||
|
@ -404,7 +392,7 @@ extension LocalPredicateStatusesViewController: UICollectionViewDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
|
func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
|
||||||
return (collectionView.cellForItem(at: indexPath) as? TimelineStatusCollectionViewCell)?.contextMenuConfiguration()
|
(collectionView.cellForItem(at: indexPath) as? TimelineStatusCollectionViewCell)?.contextMenuConfiguration()
|
||||||
}
|
}
|
||||||
|
|
||||||
func collectionView(_ collectionView: UICollectionView, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) {
|
func collectionView(_ collectionView: UICollectionView, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) {
|
||||||
|
@ -412,17 +400,17 @@ extension LocalPredicateStatusesViewController: UICollectionViewDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension LocalPredicateStatusesViewController: UICollectionViewDragDelegate {
|
extension BookmarksViewController: UICollectionViewDragDelegate {
|
||||||
func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
|
func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
|
||||||
return (collectionView.cellForItem(at: indexPath) as? TimelineStatusCollectionViewCell)?.dragItemsForBeginning(session: session) ?? []
|
(collectionView.cellForItem(at: indexPath) as? TimelineStatusCollectionViewCell)?.dragItemsForBeginning(session: session) ?? []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension LocalPredicateStatusesViewController: TuskerNavigationDelegate {
|
extension BookmarksViewController: TuskerNavigationDelegate {
|
||||||
var apiController: MastodonController! { mastodonController }
|
var apiController: MastodonController! { mastodonController }
|
||||||
}
|
}
|
||||||
|
|
||||||
extension LocalPredicateStatusesViewController: StatusCollectionViewCellDelegate {
|
extension BookmarksViewController: StatusCollectionViewCellDelegate {
|
||||||
func statusCellNeedsReconfigure(_ cell: StatusCollectionViewCell, animated: Bool, completion: (() -> Void)?) {
|
func statusCellNeedsReconfigure(_ cell: StatusCollectionViewCell, animated: Bool, completion: (() -> Void)?) {
|
||||||
if let indexPath = collectionView.indexPath(for: cell) {
|
if let indexPath = collectionView.indexPath(for: cell) {
|
||||||
var snapshot = dataSource.snapshot()
|
var snapshot = dataSource.snapshot()
|
||||||
|
@ -432,17 +420,17 @@ extension LocalPredicateStatusesViewController: StatusCollectionViewCellDelegate
|
||||||
}
|
}
|
||||||
|
|
||||||
func statusCellShowFiltered(_ cell: StatusCollectionViewCell) {
|
func statusCellShowFiltered(_ cell: StatusCollectionViewCell) {
|
||||||
// filtering isn't supported here
|
// bookmarks aren't filtered
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension LocalPredicateStatusesViewController: TabBarScrollableViewController {
|
extension BookmarksViewController: TabBarScrollableViewController {
|
||||||
func tabBarScrollToTop() {
|
func tabBarScrollToTop() {
|
||||||
collectionView.scrollToTop()
|
collectionView.scrollToTop()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension LocalPredicateStatusesViewController: StatusBarTappableViewController {
|
extension BookmarksViewController: StatusBarTappableViewController {
|
||||||
func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult {
|
func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult {
|
||||||
collectionView.scrollToTop()
|
collectionView.scrollToTop()
|
||||||
return .stop
|
return .stop
|
|
@ -236,7 +236,6 @@ struct ComposeAutocompleteEmojisView: View {
|
||||||
CustomEmojiImageView(emoji: emoji)
|
CustomEmojiImageView(emoji: emoji)
|
||||||
.frame(height: emojiSize)
|
.frame(height: emojiSize)
|
||||||
}
|
}
|
||||||
.accessibilityLabel(emoji.shortcode)
|
|
||||||
}
|
}
|
||||||
} header: {
|
} header: {
|
||||||
if !section.isEmpty {
|
if !section.isEmpty {
|
||||||
|
@ -272,7 +271,6 @@ struct ComposeAutocompleteEmojisView: View {
|
||||||
.foregroundColor(Color(UIColor.label))
|
.foregroundColor(Color(UIColor.label))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.accessibilityLabel(emoji.shortcode)
|
|
||||||
.frame(height: emojiSize)
|
.frame(height: emojiSize)
|
||||||
}
|
}
|
||||||
.animation(.linear(duration: 0.2), value: emojis)
|
.animation(.linear(duration: 0.2), value: emojis)
|
||||||
|
@ -295,7 +293,6 @@ struct ComposeAutocompleteEmojisView: View {
|
||||||
.aspectRatio(contentMode: .fit)
|
.aspectRatio(contentMode: .fit)
|
||||||
.rotationEffect(expanded ? .zero : .degrees(180))
|
.rotationEffect(expanded ? .zero : .degrees(180))
|
||||||
}
|
}
|
||||||
.accessibilityLabel(expanded ? "Collapse" : "Expand")
|
|
||||||
.frame(width: 20, height: 20)
|
.frame(width: 20, height: 20)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,17 +15,15 @@ struct ComposeEmojiTextField: UIViewRepresentable {
|
||||||
|
|
||||||
@Binding var text: String
|
@Binding var text: String
|
||||||
let placeholder: String
|
let placeholder: String
|
||||||
let maxLength: Int?
|
|
||||||
let becomeFirstResponder: Binding<Bool>?
|
let becomeFirstResponder: Binding<Bool>?
|
||||||
let focusNextView: Binding<Bool>?
|
let focusNextView: Binding<Bool>?
|
||||||
private var didChange: ((String) -> Void)? = nil
|
private var didChange: ((String) -> Void)? = nil
|
||||||
private var didEndEditing: (() -> Void)? = nil
|
private var didEndEditing: (() -> Void)? = nil
|
||||||
private var backgroundColor: UIColor? = nil
|
private var backgroundColor: UIColor? = nil
|
||||||
|
|
||||||
init(text: Binding<String>, placeholder: String, maxLength: Int? = nil, becomeFirstResponder: Binding<Bool>? = nil, focusNextView: Binding<Bool>? = nil) {
|
init(text: Binding<String>, placeholder: String, becomeFirstResponder: Binding<Bool>? = nil, focusNextView: Binding<Bool>? = nil) {
|
||||||
self._text = text
|
self._text = text
|
||||||
self.placeholder = placeholder
|
self.placeholder = placeholder
|
||||||
self.maxLength = maxLength
|
|
||||||
self.becomeFirstResponder = becomeFirstResponder
|
self.becomeFirstResponder = becomeFirstResponder
|
||||||
self.focusNextView = focusNextView
|
self.focusNextView = focusNextView
|
||||||
self.didChange = nil
|
self.didChange = nil
|
||||||
|
@ -76,7 +74,6 @@ struct ComposeEmojiTextField: UIViewRepresentable {
|
||||||
} else {
|
} else {
|
||||||
uiView.text = text
|
uiView.text = text
|
||||||
}
|
}
|
||||||
context.coordinator.maxLength = maxLength
|
|
||||||
context.coordinator.didChange = didChange
|
context.coordinator.didChange = didChange
|
||||||
context.coordinator.didEndEditing = didEndEditing
|
context.coordinator.didEndEditing = didEndEditing
|
||||||
context.coordinator.focusNextView = focusNextView
|
context.coordinator.focusNextView = focusNextView
|
||||||
|
@ -98,7 +95,6 @@ struct ComposeEmojiTextField: UIViewRepresentable {
|
||||||
var text: Binding<String>!
|
var text: Binding<String>!
|
||||||
// break retained cycle through ComposeUIState.currentInput
|
// break retained cycle through ComposeUIState.currentInput
|
||||||
unowned var uiState: ComposeUIState!
|
unowned var uiState: ComposeUIState!
|
||||||
var maxLength: Int?
|
|
||||||
var didChange: ((String) -> Void)?
|
var didChange: ((String) -> Void)?
|
||||||
var didEndEditing: (() -> Void)?
|
var didEndEditing: (() -> Void)?
|
||||||
var focusNextView: Binding<Bool>?
|
var focusNextView: Binding<Bool>?
|
||||||
|
@ -118,14 +114,6 @@ struct ComposeEmojiTextField: UIViewRepresentable {
|
||||||
focusNextView?.wrappedValue = true
|
focusNextView?.wrappedValue = true
|
||||||
}
|
}
|
||||||
|
|
||||||
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
|
|
||||||
if let maxLength {
|
|
||||||
return ((textField.text ?? "") as NSString).replacingCharacters(in: range, with: string).count <= maxLength
|
|
||||||
} else {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func textFieldDidBeginEditing(_ textField: UITextField) {
|
func textFieldDidBeginEditing(_ textField: UITextField) {
|
||||||
uiState.currentInput = self
|
uiState.currentInput = self
|
||||||
updateAutocompleteState(textField: textField)
|
updateAutocompleteState(textField: textField)
|
||||||
|
|
|
@ -20,7 +20,6 @@ struct ComposePollView: View {
|
||||||
@ObservedObject var draft: Draft
|
@ObservedObject var draft: Draft
|
||||||
@ObservedObject var poll: Draft.Poll
|
@ObservedObject var poll: Draft.Poll
|
||||||
|
|
||||||
@EnvironmentObject var mastodonController: MastodonController
|
|
||||||
@Environment(\.colorScheme) var colorScheme: ColorScheme
|
@Environment(\.colorScheme) var colorScheme: ColorScheme
|
||||||
|
|
||||||
@State private var duration: Duration
|
@State private var duration: Duration
|
||||||
|
@ -32,14 +31,6 @@ struct ComposePollView: View {
|
||||||
self._duration = State(initialValue: .fromTimeInterval(poll.duration) ?? .oneDay)
|
self._duration = State(initialValue: .fromTimeInterval(poll.duration) ?? .oneDay)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var canAddOption: Bool {
|
|
||||||
if let pollConfig = mastodonController.instance?.pollsConfiguration {
|
|
||||||
return poll.options.count < pollConfig.maxOptions
|
|
||||||
} else {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
VStack {
|
||||||
HStack {
|
HStack {
|
||||||
|
@ -76,15 +67,9 @@ struct ComposePollView: View {
|
||||||
.frame(height: 44 * CGFloat(poll.options.count))
|
.frame(height: 44 * CGFloat(poll.options.count))
|
||||||
|
|
||||||
Button(action: self.addOption) {
|
Button(action: self.addOption) {
|
||||||
Label {
|
Label("Add Option", systemImage: "plus")
|
||||||
Text("Add Option")
|
|
||||||
} icon: {
|
|
||||||
Image(systemName: "plus")
|
|
||||||
.foregroundColor(.accentColor)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.buttonStyle(.borderless)
|
.buttonStyle(.borderless)
|
||||||
.disabled(!canAddOption)
|
|
||||||
|
|
||||||
HStack {
|
HStack {
|
||||||
MenuPicker(selection: $poll.multiple, options: [
|
MenuPicker(selection: $poll.multiple, options: [
|
||||||
|
@ -170,8 +155,6 @@ struct ComposePollOption: View {
|
||||||
@ObservedObject var option: Draft.Poll.Option
|
@ObservedObject var option: Draft.Poll.Option
|
||||||
let optionIndex: Int
|
let optionIndex: Int
|
||||||
|
|
||||||
@EnvironmentObject private var mastodonController: MastodonController
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack(spacing: 4) {
|
HStack(spacing: 4) {
|
||||||
Checkbox(radiusFraction: poll.multiple ? 0.1 : 0.5, borderWidth: 2)
|
Checkbox(radiusFraction: poll.multiple ? 0.1 : 0.5, borderWidth: 2)
|
||||||
|
@ -190,7 +173,7 @@ struct ComposePollOption: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private var textField: some View {
|
private var textField: some View {
|
||||||
var field = ComposeEmojiTextField(text: $option.text, placeholder: "Option \(optionIndex + 1)", maxLength: mastodonController.instance?.pollsConfiguration?.maxCharactersPerOption)
|
var field = ComposeEmojiTextField(text: $option.text, placeholder: "Option \(optionIndex + 1)")
|
||||||
return field.backgroundColor(.systemBackground)
|
return field.backgroundColor(.systemBackground)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,16 @@
|
||||||
import UIKit
|
import UIKit
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
|
|
||||||
|
class ConversationNode {
|
||||||
|
let status: StatusMO
|
||||||
|
var children: [ConversationNode]
|
||||||
|
|
||||||
|
init(status: StatusMO) {
|
||||||
|
self.status = status
|
||||||
|
self.children = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class ConversationCollectionViewController: UIViewController, CollectionViewController {
|
class ConversationCollectionViewController: UIViewController, CollectionViewController {
|
||||||
|
|
||||||
private let mastodonController: MastodonController
|
private let mastodonController: MastodonController
|
||||||
|
@ -45,15 +55,11 @@ class ConversationCollectionViewController: UIViewController, CollectionViewCont
|
||||||
(collectionView.cellForItem(at: $0) as? TimelineStatusCollectionViewCell)?.trailingSwipeActions()
|
(collectionView.cellForItem(at: $0) as? TimelineStatusCollectionViewCell)?.trailingSwipeActions()
|
||||||
}
|
}
|
||||||
config.itemSeparatorHandler = { [unowned self] indexPath, sectionConfig in
|
config.itemSeparatorHandler = { [unowned self] indexPath, sectionConfig in
|
||||||
|
let rowsInSection = self.collectionView.numberOfItems(inSection: indexPath.section)
|
||||||
|
let lastInSection = indexPath.row == rowsInSection - 1
|
||||||
var config = sectionConfig
|
var config = sectionConfig
|
||||||
config.topSeparatorVisibility = .hidden
|
config.topSeparatorVisibility = .hidden
|
||||||
if case .ancestors = self.dataSource.sectionIdentifier(for: indexPath.section) {
|
config.bottomSeparatorVisibility = lastInSection ? .visible : .hidden
|
||||||
config.bottomSeparatorVisibility = .hidden
|
|
||||||
} else if indexPath.row == self.collectionView.numberOfItems(inSection: indexPath.section) - 1 {
|
|
||||||
config.bottomSeparatorVisibility = .visible
|
|
||||||
} else {
|
|
||||||
config.bottomSeparatorVisibility = .hidden
|
|
||||||
}
|
|
||||||
config.bottomSeparatorInsets = TimelineStatusCollectionViewCell.separatorInsets
|
config.bottomSeparatorInsets = TimelineStatusCollectionViewCell.separatorInsets
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
@ -93,7 +99,7 @@ class ConversationCollectionViewController: UIViewController, CollectionViewCont
|
||||||
}
|
}
|
||||||
return UICollectionViewDiffableDataSource(collectionView: collectionView) { [unowned self] collectionView, indexPath, itemIdentifier in
|
return UICollectionViewDiffableDataSource(collectionView: collectionView) { [unowned self] collectionView, indexPath, itemIdentifier in
|
||||||
switch itemIdentifier {
|
switch itemIdentifier {
|
||||||
case let .status(id: id, node: _, state: state, prevLink: prevLink, nextLink: nextLink):
|
case let .status(id: id, state: state, prevLink: prevLink, nextLink: nextLink):
|
||||||
if id == self.mainStatusID {
|
if id == self.mainStatusID {
|
||||||
return collectionView.dequeueConfiguredReusableCell(using: mainStatusCell, for: indexPath, item: (id, state, prevLink))
|
return collectionView.dequeueConfiguredReusableCell(using: mainStatusCell, for: indexPath, item: (id, state, prevLink))
|
||||||
} else {
|
} else {
|
||||||
|
@ -117,32 +123,45 @@ class ConversationCollectionViewController: UIViewController, CollectionViewCont
|
||||||
loadViewIfNeeded()
|
loadViewIfNeeded()
|
||||||
|
|
||||||
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||||
snapshot.appendSections([.ancestors, .mainStatus])
|
snapshot.appendSections([.statuses])
|
||||||
|
|
||||||
if status.inReplyToID != nil {
|
if status.inReplyToID != nil {
|
||||||
snapshot.appendItems([.loadingIndicator], toSection: .ancestors)
|
snapshot.appendItems([.loadingIndicator], toSection: .statuses)
|
||||||
}
|
}
|
||||||
|
|
||||||
// this will be replace with the actual node in the tree once it's loaded
|
let mainStatusItem = Item.status(id: mainStatusID, state: mainStatusState, prevLink: status.inReplyToID != nil, nextLink: false)
|
||||||
let tempMainNode = ConversationNode(status: status)
|
snapshot.appendItems([mainStatusItem], toSection: .statuses)
|
||||||
let mainStatusItem = Item.status(id: mainStatusID, node: tempMainNode, state: mainStatusState, prevLink: status.inReplyToID != nil, nextLink: false)
|
|
||||||
snapshot.appendItems([mainStatusItem], toSection: .mainStatus)
|
|
||||||
|
|
||||||
dataSource.apply(snapshot, animatingDifferences: false)
|
dataSource.apply(snapshot, animatingDifferences: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func addTree(_ tree: ConversationTree, mainStatus: StatusMO) {
|
func addContext(_ context: ConversationContext, for mainStatus: StatusMO) async {
|
||||||
|
let parentIDs = getDirectParents(inReplyTo: mainStatus.inReplyToID, from: context.ancestors)
|
||||||
|
let parentStatuses = context.ancestors.filter { parentIDs.contains($0.id) }
|
||||||
|
|
||||||
|
await mastodonController.persistentContainer.addAll(statuses: parentStatuses + context.descendants)
|
||||||
|
|
||||||
var snapshot = dataSource.snapshot()
|
var snapshot = dataSource.snapshot()
|
||||||
snapshot.deleteItems([.loadingIndicator])
|
snapshot.deleteItems([.loadingIndicator])
|
||||||
let mainStatusItem = Item.status(id: mainStatusID, node: tree.mainStatus, state: mainStatusState, prevLink: mainStatus.inReplyToID != nil, nextLink: false)
|
let mainStatusItem = Item.status(id: mainStatusID, state: mainStatusState, prevLink: mainStatus.inReplyToID != nil, nextLink: false)
|
||||||
let parentItems = tree.ancestors.enumerated().map { index, node in
|
let parentItems = parentIDs.enumerated().map { index, id in
|
||||||
Item.status(id: node.status.id, node: node, state: .unknown, prevLink: index > 0, nextLink: true)
|
Item.status(id: id, state: .unknown, prevLink: index > 0, nextLink: true)
|
||||||
}
|
}
|
||||||
snapshot.appendItems(parentItems, toSection: .ancestors)
|
snapshot.insertItems(parentItems, beforeItem: mainStatusItem)
|
||||||
snapshot.reloadItems([mainStatusItem])
|
snapshot.reloadItems([mainStatusItem])
|
||||||
|
|
||||||
// convert sub-threads into items for section and add to snapshot
|
// fetch all descendant status managed objects
|
||||||
self.addFlattenedChildThreadsToSnapshot(tree.descendants, mainStatus: mainStatus, snapshot: &snapshot)
|
let descendantIDs = context.descendants.map(\.id)
|
||||||
|
let request = StatusMO.fetchRequest()
|
||||||
|
request.predicate = NSPredicate(format: "id IN %@", descendantIDs)
|
||||||
|
|
||||||
|
if let descendants = try? mastodonController.persistentContainer.viewContext.fetch(request) {
|
||||||
|
// convert array of descendant statuses into tree of sub-threads
|
||||||
|
let childThreads = self.getDescendantThreads(mainStatus: mainStatus, descendants: descendants)
|
||||||
|
|
||||||
|
// convert sub-threads into items for section and add to snapshot
|
||||||
|
self.addFlattenedChildThreadsToSnapshot(childThreads, mainStatus: mainStatus, snapshot: &snapshot)
|
||||||
|
}
|
||||||
|
|
||||||
self.dataSource.apply(snapshot, animatingDifferences: false) {
|
self.dataSource.apply(snapshot, animatingDifferences: false) {
|
||||||
let item: Item
|
let item: Item
|
||||||
|
@ -152,7 +171,7 @@ class ConversationCollectionViewController: UIViewController, CollectionViewCont
|
||||||
position = .centeredVertically
|
position = .centeredVertically
|
||||||
} else {
|
} else {
|
||||||
item = snapshot.itemIdentifiers.first {
|
item = snapshot.itemIdentifiers.first {
|
||||||
if case .status(id: self.statusIDToScrollToOnLoad, _, _, _, _) = $0 {
|
if case .status(id: self.statusIDToScrollToOnLoad, _, _, _) = $0 {
|
||||||
return true
|
return true
|
||||||
} else {
|
} else {
|
||||||
return false
|
return false
|
||||||
|
@ -168,6 +187,54 @@ class ConversationCollectionViewController: UIViewController, CollectionViewCont
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func getDirectParents(inReplyTo inReplyToID: String?, from statuses: [Status]) -> [String] {
|
||||||
|
var statuses = statuses
|
||||||
|
var parents = [String]()
|
||||||
|
|
||||||
|
var parentID: String? = inReplyToID
|
||||||
|
|
||||||
|
while parentID != nil, let parentIndex = statuses.firstIndex(where: { $0.id == parentID }) {
|
||||||
|
let parentStatus = statuses.remove(at: parentIndex)
|
||||||
|
parents.insert(parentStatus.id, at: 0)
|
||||||
|
parentID = parentStatus.inReplyToID
|
||||||
|
}
|
||||||
|
|
||||||
|
return parents
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getDescendantThreads(mainStatus: StatusMO, descendants: [StatusMO]) -> [ConversationNode] {
|
||||||
|
var descendants = descendants
|
||||||
|
|
||||||
|
func removeAllInReplyTo(id: String) -> [StatusMO] {
|
||||||
|
let statuses = descendants.filter { $0.inReplyToID == id }
|
||||||
|
descendants.removeAll { $0.inReplyToID == id }
|
||||||
|
return statuses
|
||||||
|
}
|
||||||
|
|
||||||
|
var nodes: [String: ConversationNode] = [
|
||||||
|
mainStatus.id: ConversationNode(status: mainStatus)
|
||||||
|
]
|
||||||
|
|
||||||
|
var idsToCheck = [mainStatusID]
|
||||||
|
|
||||||
|
while !idsToCheck.isEmpty {
|
||||||
|
let inReplyToID = idsToCheck.removeFirst()
|
||||||
|
let nodeForID = nodes[inReplyToID]!
|
||||||
|
|
||||||
|
let inReply = removeAllInReplyTo(id: inReplyToID)
|
||||||
|
for reply in inReply {
|
||||||
|
idsToCheck.append(reply.id)
|
||||||
|
|
||||||
|
let replyNode = ConversationNode(status: reply)
|
||||||
|
nodes[reply.id] = replyNode
|
||||||
|
|
||||||
|
nodeForID.children.append(replyNode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nodes[mainStatusID]!.children
|
||||||
|
}
|
||||||
|
|
||||||
private func addFlattenedChildThreadsToSnapshot(_ childThreads: [ConversationNode], mainStatus: StatusMO, snapshot: inout NSDiffableDataSourceSnapshot<Section, Item>) {
|
private func addFlattenedChildThreadsToSnapshot(_ childThreads: [ConversationNode], mainStatus: StatusMO, snapshot: inout NSDiffableDataSourceSnapshot<Section, Item>) {
|
||||||
var childThreads = childThreads
|
var childThreads = childThreads
|
||||||
|
|
||||||
|
@ -181,7 +248,7 @@ class ConversationCollectionViewController: UIViewController, CollectionViewCont
|
||||||
for node in childThreads {
|
for node in childThreads {
|
||||||
let section = Section.childThread(firstStatusID: node.status.id)
|
let section = Section.childThread(firstStatusID: node.status.id)
|
||||||
snapshot.appendSections([section])
|
snapshot.appendSections([section])
|
||||||
snapshot.appendItems([.status(id: node.status.id, node: node, state: .unknown, prevLink: false, nextLink: !node.children.isEmpty)], toSection: section)
|
snapshot.appendItems([.status(id: node.status.id, state: .unknown, prevLink: false, nextLink: !node.children.isEmpty)], toSection: section)
|
||||||
|
|
||||||
var currentNode = node
|
var currentNode = node
|
||||||
while true {
|
while true {
|
||||||
|
@ -204,7 +271,7 @@ class ConversationCollectionViewController: UIViewController, CollectionViewCont
|
||||||
}
|
}
|
||||||
|
|
||||||
currentNode = next
|
currentNode = next
|
||||||
snapshot.appendItems([.status(id: next.status.id, node: next, state: .unknown, prevLink: true, nextLink: !next.children.isEmpty)], toSection: section)
|
snapshot.appendItems([.status(id: next.status.id, state: .unknown, prevLink: true, nextLink: !next.children.isEmpty)], toSection: section)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -213,7 +280,7 @@ class ConversationCollectionViewController: UIViewController, CollectionViewCont
|
||||||
var snapshot = dataSource.snapshot()
|
var snapshot = dataSource.snapshot()
|
||||||
var cellsToMask: [StatusCollectionViewCell] = []
|
var cellsToMask: [StatusCollectionViewCell] = []
|
||||||
for item in snapshot.itemIdentifiers {
|
for item in snapshot.itemIdentifiers {
|
||||||
guard case .status(id: _, node: _, state: let state, prevLink: _, nextLink: _) = item,
|
guard case .status(id: _, state: let state, prevLink: _, nextLink: _) = item,
|
||||||
state.collapsible == true else {
|
state.collapsible == true else {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -244,18 +311,17 @@ class ConversationCollectionViewController: UIViewController, CollectionViewCont
|
||||||
|
|
||||||
extension ConversationCollectionViewController {
|
extension ConversationCollectionViewController {
|
||||||
enum Section: Hashable {
|
enum Section: Hashable {
|
||||||
case ancestors
|
case statuses
|
||||||
case mainStatus
|
|
||||||
case childThread(firstStatusID: String)
|
case childThread(firstStatusID: String)
|
||||||
}
|
}
|
||||||
enum Item: Hashable {
|
enum Item: Hashable {
|
||||||
case status(id: String, node: ConversationNode, state: CollapseState, prevLink: Bool, nextLink: Bool)
|
case status(id: String, state: CollapseState, prevLink: Bool, nextLink: Bool)
|
||||||
case expandThread(childThreads: [ConversationNode], inline: Bool)
|
case expandThread(childThreads: [ConversationNode], inline: Bool)
|
||||||
case loadingIndicator
|
case loadingIndicator
|
||||||
|
|
||||||
static func ==(lhs: Item, rhs: Item) -> Bool {
|
static func ==(lhs: Item, rhs: Item) -> Bool {
|
||||||
switch (lhs, rhs) {
|
switch (lhs, rhs) {
|
||||||
case let (.status(id: a, node: _, state: _, prevLink: aPrev, nextLink: aNext), .status(id: b, node: _, state: _, prevLink: bPrev, nextLink: bNext)):
|
case let (.status(id: a, state: _, prevLink: aPrev, nextLink: aNext), .status(id: b, state: _, prevLink: bPrev, nextLink: bNext)):
|
||||||
return a == b && aPrev == bPrev && aNext == bNext
|
return a == b && aPrev == bPrev && aNext == bNext
|
||||||
case let (.expandThread(childThreads: a, inline: aInline), .expandThread(childThreads: b, inline: bInline)):
|
case let (.expandThread(childThreads: a, inline: aInline), .expandThread(childThreads: b, inline: bInline)):
|
||||||
return a.count == b.count && zip(a, b).allSatisfy { $0.status.id == $1.status.id } && aInline == bInline
|
return a.count == b.count && zip(a, b).allSatisfy { $0.status.id == $1.status.id } && aInline == bInline
|
||||||
|
@ -268,7 +334,7 @@ extension ConversationCollectionViewController {
|
||||||
|
|
||||||
func hash(into hasher: inout Hasher) {
|
func hash(into hasher: inout Hasher) {
|
||||||
switch self {
|
switch self {
|
||||||
case let .status(id: id, node: _, state: _, prevLink: prevLink, nextLink: nextLink):
|
case let .status(id: id, state: _, prevLink: prevLink, nextLink: nextLink):
|
||||||
hasher.combine(0)
|
hasher.combine(0)
|
||||||
hasher.combine(id)
|
hasher.combine(id)
|
||||||
hasher.combine(prevLink)
|
hasher.combine(prevLink)
|
||||||
|
@ -289,7 +355,7 @@ extension ConversationCollectionViewController {
|
||||||
extension ConversationCollectionViewController: UICollectionViewDelegate {
|
extension ConversationCollectionViewController: UICollectionViewDelegate {
|
||||||
func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool {
|
func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool {
|
||||||
switch dataSource.itemIdentifier(for: indexPath) {
|
switch dataSource.itemIdentifier(for: indexPath) {
|
||||||
case .status(id: let id, node: _, state: _, prevLink: _, nextLink: _):
|
case .status(id: let id, state: _, prevLink: _, nextLink: _):
|
||||||
return id != mainStatusID
|
return id != mainStatusID
|
||||||
case .expandThread(childThreads: _, inline: _):
|
case .expandThread(childThreads: _, inline: _):
|
||||||
return true
|
return true
|
||||||
|
@ -304,24 +370,12 @@ extension ConversationCollectionViewController: UICollectionViewDelegate {
|
||||||
break
|
break
|
||||||
case .loadingIndicator:
|
case .loadingIndicator:
|
||||||
break
|
break
|
||||||
case .status(id: let id, node: let node, state: let state, _, _):
|
case .status(id: let id, state: let state, _, _):
|
||||||
// we can only take the fast path if the user tapped on a descendant status.
|
selected(status: id, state: state.copy())
|
||||||
// if the current main status is C, or one of its descendants, and the user taps A, then B won't be loaded:
|
|
||||||
// A
|
|
||||||
// / \
|
|
||||||
// B C
|
|
||||||
if case .childThread(_) = dataSource.sectionIdentifier(for: indexPath.section) {
|
|
||||||
let tree = ConversationTree(ancestors: buildNewAncestors(above: indexPath), mainStatus: node)
|
|
||||||
let conv = ConversationViewController(preloadedTree: tree, state: state.copy(), mastodonController: mastodonController)
|
|
||||||
conv.showStatusesAutomatically = showStatusesAutomatically
|
|
||||||
show(conv)
|
|
||||||
} else {
|
|
||||||
selected(status: id, state: state.copy())
|
|
||||||
}
|
|
||||||
case .expandThread(childThreads: let childThreads, inline: _):
|
case .expandThread(childThreads: let childThreads, inline: _):
|
||||||
if case .status(id: _, node: let node, state: let state, _, _) = dataSource.itemIdentifier(for: IndexPath(row: indexPath.row - 1, section: indexPath.section)) {
|
if case .status(id: let id, state: let state, _, _) = dataSource.itemIdentifier(for: IndexPath(row: indexPath.row - 1, section: indexPath.section)) {
|
||||||
let tree = ConversationTree(ancestors: buildNewAncestors(above: indexPath), mainStatus: node)
|
// todo: it would be nice to avoid re-fetching the context here, since we should have all the necessary information already
|
||||||
let conv = ConversationViewController(preloadedTree: tree, state: state.copy(), mastodonController: mastodonController)
|
let conv = ConversationViewController(for: id, state: state.copy(), mastodonController: mastodonController)
|
||||||
conv.statusIDToScrollToOnLoad = childThreads.first!.status.id
|
conv.statusIDToScrollToOnLoad = childThreads.first!.status.id
|
||||||
conv.showStatusesAutomatically = showStatusesAutomatically
|
conv.showStatusesAutomatically = showStatusesAutomatically
|
||||||
show(conv)
|
show(conv)
|
||||||
|
@ -329,34 +383,6 @@ extension ConversationCollectionViewController: UICollectionViewDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConversationNode doesn't know about its parent, so we reconstruct that info from the data source
|
|
||||||
private func buildNewAncestors(above indexPath: IndexPath) -> [ConversationNode] {
|
|
||||||
let snapshot = dataSource.snapshot()
|
|
||||||
let currentAncestors = snapshot.itemIdentifiers(inSection: .ancestors).compactMap {
|
|
||||||
if case .status(id: _, node: let node, _, _, _) = $0 {
|
|
||||||
return node
|
|
||||||
} else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let currentMainStatus = snapshot.itemIdentifiers(inSection: .mainStatus).compactMap {
|
|
||||||
if case .status(id: _, node: let node, _, _, _) = $0 {
|
|
||||||
return node
|
|
||||||
} else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let parentsInCurrentSection = snapshot.itemIdentifiers(inSection: dataSource.sectionIdentifier(for: indexPath.section)!)[0..<indexPath.row].compactMap {
|
|
||||||
if case .status(id: _, node: let node, _, _, _) = $0 {
|
|
||||||
return node
|
|
||||||
} else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return currentAncestors + currentMainStatus + parentsInCurrentSection
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
|
func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
|
||||||
return (collectionView.cellForItem(at: indexPath) as? TimelineStatusCollectionViewCell)?.contextMenuConfiguration()
|
return (collectionView.cellForItem(at: indexPath) as? TimelineStatusCollectionViewCell)?.contextMenuConfiguration()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,101 +0,0 @@
|
||||||
//
|
|
||||||
// ConversationTree.swift
|
|
||||||
// Tusker
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 2/4/23.
|
|
||||||
// Copyright © 2023 Shadowfacts. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import Pachyderm
|
|
||||||
|
|
||||||
class ConversationNode {
|
|
||||||
let status: StatusMO
|
|
||||||
var children: [ConversationNode]
|
|
||||||
|
|
||||||
init(status: StatusMO) {
|
|
||||||
self.status = status
|
|
||||||
self.children = []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
struct ConversationTree {
|
|
||||||
let ancestors: [ConversationNode]
|
|
||||||
let mainStatus: ConversationNode
|
|
||||||
var descendants: [ConversationNode] {
|
|
||||||
mainStatus.children
|
|
||||||
}
|
|
||||||
|
|
||||||
init(ancestors: [ConversationNode], mainStatus: ConversationNode) {
|
|
||||||
self.ancestors = ancestors
|
|
||||||
self.mainStatus = mainStatus
|
|
||||||
}
|
|
||||||
|
|
||||||
static func build(for mainStatus: StatusMO, ancestors: [StatusMO], descendants: [StatusMO]) -> ConversationTree {
|
|
||||||
let mainStatusNode = ConversationNode(status: mainStatus)
|
|
||||||
let ancestors = buildAncestorNodes(mainStatusNode: mainStatusNode, ancestors: ancestors)
|
|
||||||
buildDescendantNodes(mainStatusNode: mainStatusNode, descendants: descendants)
|
|
||||||
return ConversationTree(ancestors: ancestors, mainStatus: mainStatusNode)
|
|
||||||
}
|
|
||||||
|
|
||||||
private static func buildAncestorNodes(mainStatusNode: ConversationNode, ancestors: [StatusMO]) -> [ConversationNode] {
|
|
||||||
var statuses = ancestors
|
|
||||||
var parents = [ConversationNode]()
|
|
||||||
|
|
||||||
var parentID: String? = mainStatusNode.status.inReplyToID
|
|
||||||
|
|
||||||
while let currentParentID = parentID,
|
|
||||||
let parentIndex = statuses.firstIndex(where: { $0.id == currentParentID }) {
|
|
||||||
let parentStatus = statuses.remove(at: parentIndex)
|
|
||||||
|
|
||||||
let node = ConversationNode(status: parentStatus)
|
|
||||||
parents.insert(node, at: 0)
|
|
||||||
|
|
||||||
parentID = parentStatus.inReplyToID
|
|
||||||
}
|
|
||||||
|
|
||||||
// once the parents list is built and in-order, then we walk through and set each node's children
|
|
||||||
for (index, node) in parents.enumerated() {
|
|
||||||
if index == parents.count - 1 {
|
|
||||||
// the last parent is the direct parent of the main status
|
|
||||||
node.children = [mainStatusNode]
|
|
||||||
} else {
|
|
||||||
// otherwise, it's the parent of the status that comes immediately after it in the parents list
|
|
||||||
node.children = [parents[index + 1]]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return parents
|
|
||||||
}
|
|
||||||
|
|
||||||
// doesn't return anything, since we're modifying the main status node in-place
|
|
||||||
private static func buildDescendantNodes(mainStatusNode: ConversationNode, descendants: [StatusMO]) {
|
|
||||||
var descendants = descendants
|
|
||||||
|
|
||||||
func removeAllInReplyTo(id: String) -> [StatusMO] {
|
|
||||||
let statuses = descendants.filter { $0.inReplyToID == id }
|
|
||||||
descendants.removeAll { $0.inReplyToID == id }
|
|
||||||
return statuses
|
|
||||||
}
|
|
||||||
|
|
||||||
var nodes: [String: ConversationNode] = [
|
|
||||||
mainStatusNode.status.id: mainStatusNode
|
|
||||||
]
|
|
||||||
|
|
||||||
var idsToCheck = [mainStatusNode.status.id]
|
|
||||||
|
|
||||||
while !idsToCheck.isEmpty {
|
|
||||||
let inReplyToID = idsToCheck.removeFirst()
|
|
||||||
let nodeForID = nodes[inReplyToID]!
|
|
||||||
|
|
||||||
let inReply = removeAllInReplyTo(id: inReplyToID)
|
|
||||||
for reply in inReply {
|
|
||||||
idsToCheck.append(reply.id)
|
|
||||||
|
|
||||||
let replyNode = ConversationNode(status: reply)
|
|
||||||
nodes[reply.id] = replyNode
|
|
||||||
|
|
||||||
nodeForID.children.append(replyNode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -77,14 +77,6 @@ class ConversationViewController: UIViewController {
|
||||||
super.init(nibName: nil, bundle: nil)
|
super.init(nibName: nil, bundle: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
init(preloadedTree: ConversationTree, state mainStatusState: CollapseState, mastodonController: MastodonController) {
|
|
||||||
self.mode = .preloaded(preloadedTree)
|
|
||||||
self.mainStatusState = mainStatusState
|
|
||||||
self.mastodonController = mastodonController
|
|
||||||
|
|
||||||
super.init(nibName: nil, bundle: nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
@ -123,15 +115,9 @@ class ConversationViewController: UIViewController {
|
||||||
override func viewWillAppear(_ animated: Bool) {
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
super.viewWillAppear(animated)
|
super.viewWillAppear(animated)
|
||||||
|
|
||||||
if case .unloaded = state {
|
Task {
|
||||||
if case .preloaded(let tree) = mode {
|
if case .unloaded = state {
|
||||||
// when everything is preloaded, we're on the fast path and want to avoid any async work
|
await loadMainStatus()
|
||||||
// just kicking off a MainActor task causes a delay before the content appears, even if the task doesn't suspend
|
|
||||||
mainStatusLoaded(tree.mainStatus.status)
|
|
||||||
} else {
|
|
||||||
Task { @MainActor in
|
|
||||||
await loadMainStatus()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -156,20 +142,9 @@ class ConversationViewController: UIViewController {
|
||||||
|
|
||||||
// MARK: Loading
|
// MARK: Loading
|
||||||
|
|
||||||
@MainActor
|
|
||||||
private func loadMainStatus() async {
|
private func loadMainStatus() async {
|
||||||
let mainStatusID: String
|
guard let mainStatusID = await resolveStatusIfNecessary() else {
|
||||||
switch mode {
|
return
|
||||||
case .localID(let id):
|
|
||||||
mainStatusID = id
|
|
||||||
case .resolve(let url):
|
|
||||||
if let id = await resolveStatus(url: url) {
|
|
||||||
mainStatusID = id
|
|
||||||
} else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
case .preloaded(_):
|
|
||||||
fatalError("unreachable")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
|
@ -191,7 +166,7 @@ class ConversationViewController: UIViewController {
|
||||||
Task {
|
Task {
|
||||||
await doLoadMainStatus()
|
await doLoadMainStatus()
|
||||||
}
|
}
|
||||||
mainStatusLoaded(cached)
|
await mainStatusLoaded(cached)
|
||||||
} else {
|
} else {
|
||||||
// otherwise, show a loading indicator while loading the main status
|
// otherwise, show a loading indicator while loading the main status
|
||||||
let indicator = UIActivityIndicatorView(style: .medium)
|
let indicator = UIActivityIndicatorView(style: .medium)
|
||||||
|
@ -199,94 +174,74 @@ class ConversationViewController: UIViewController {
|
||||||
state = .loading(indicator)
|
state = .loading(indicator)
|
||||||
|
|
||||||
if let status = await doLoadMainStatus() {
|
if let status = await doLoadMainStatus() {
|
||||||
mainStatusLoaded(status)
|
await mainStatusLoaded(status)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
private func resolveStatus(url: URL) async -> String? {
|
private func resolveStatusIfNecessary() async -> String? {
|
||||||
let indicator = UIActivityIndicatorView(style: .medium)
|
switch mode {
|
||||||
indicator.startAnimating()
|
case .localID(let id):
|
||||||
state = .loading(indicator)
|
return id
|
||||||
|
case .resolve(let url):
|
||||||
|
let indicator = UIActivityIndicatorView(style: .medium)
|
||||||
|
indicator.startAnimating()
|
||||||
|
state = .loading(indicator)
|
||||||
|
|
||||||
let url = WebURL(url)!.serialized(excludingFragment: true)
|
let url = WebURL(url)!.serialized(excludingFragment: true)
|
||||||
let request = Client.search(query: url, types: [.statuses], resolve: true)
|
let request = Client.search(query: url, types: [.statuses], resolve: true)
|
||||||
do {
|
do {
|
||||||
let (results, _) = try await mastodonController.run(request)
|
let (results, _) = try await mastodonController.run(request)
|
||||||
guard let status = results.statuses.first(where: { $0.url?.serialized() == url }) else {
|
guard let status = results.statuses.first(where: { $0.url?.serialized() == url }) else {
|
||||||
throw UnableToResolveError()
|
throw UnableToResolveError()
|
||||||
|
}
|
||||||
|
_ = mastodonController.persistentContainer.addOrUpdateOnViewContext(status: status)
|
||||||
|
mode = .localID(status.id)
|
||||||
|
return status.id
|
||||||
|
} catch {
|
||||||
|
state = .unableToResolve(error)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
_ = mastodonController.persistentContainer.addOrUpdateOnViewContext(status: status)
|
|
||||||
mode = .localID(status.id)
|
|
||||||
return status.id
|
|
||||||
} catch {
|
|
||||||
state = .unableToResolve(error)
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func mainStatusLoaded(_ mainStatus: StatusMO) {
|
@MainActor
|
||||||
|
private func mainStatusLoaded(_ mainStatus: StatusMO) async {
|
||||||
let vc = ConversationCollectionViewController(for: mainStatus.id, state: mainStatusState, mastodonController: mastodonController)
|
let vc = ConversationCollectionViewController(for: mainStatus.id, state: mainStatusState, mastodonController: mastodonController)
|
||||||
vc.statusIDToScrollToOnLoad = statusIDToScrollToOnLoad ?? mainStatus.id
|
vc.statusIDToScrollToOnLoad = statusIDToScrollToOnLoad ?? mainStatus.id
|
||||||
vc.showStatusesAutomatically = showStatusesAutomatically
|
vc.showStatusesAutomatically = showStatusesAutomatically
|
||||||
vc.addMainStatus(mainStatus)
|
vc.addMainStatus(mainStatus)
|
||||||
state = .displaying(vc)
|
state = .displaying(vc)
|
||||||
|
|
||||||
if case .preloaded(let tree) = mode {
|
await loadContext(for: mainStatus)
|
||||||
vc.addTree(tree, mainStatus: mainStatus)
|
|
||||||
} else {
|
|
||||||
Task { @MainActor in
|
|
||||||
await loadTree(for: mainStatus)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
private func loadTree(for mainStatus: StatusMO) async {
|
private func loadContext(for mainStatus: StatusMO) async {
|
||||||
guard case .displaying(_) = state,
|
guard case .displaying(_) = state else {
|
||||||
let context = await loadContext(for: mainStatus) else {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await mastodonController.persistentContainer.addAll(statuses: context.ancestors + context.descendants)
|
|
||||||
|
|
||||||
let ancestorIDs = context.ancestors.map(\.id)
|
|
||||||
let ancestorsReq = StatusMO.fetchRequest()
|
|
||||||
ancestorsReq.predicate = NSPredicate(format: "id in %@", ancestorIDs)
|
|
||||||
let ancestors = try? mastodonController.persistentContainer.viewContext.fetch(ancestorsReq)
|
|
||||||
|
|
||||||
let descendantIDs = context.descendants.map(\.id)
|
|
||||||
let descendantsReq = StatusMO.fetchRequest()
|
|
||||||
descendantsReq.predicate = NSPredicate(format: "id IN %@", descendantIDs)
|
|
||||||
let descendants = try? mastodonController.persistentContainer.viewContext.fetch(descendantsReq)
|
|
||||||
|
|
||||||
let tree = ConversationTree.build(for: mainStatus, ancestors: ancestors ?? [], descendants: descendants ?? [])
|
|
||||||
|
|
||||||
guard case .displaying(let vc) = state else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
vc.addTree(tree, mainStatus: mainStatus)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func loadContext(for mainStatus: StatusMO) async -> ConversationContext? {
|
|
||||||
let request = Status.getContext(mainStatus.id)
|
let request = Status.getContext(mainStatus.id)
|
||||||
do {
|
do {
|
||||||
let (context, _) = try await mastodonController.run(request)
|
let (context, _) = try await mastodonController.run(request)
|
||||||
return context
|
guard case .displaying(let vc) = state else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await vc.addContext(context, for: mainStatus)
|
||||||
} catch {
|
} catch {
|
||||||
guard case .displaying(_) = state else {
|
guard case .displaying(_) = state else {
|
||||||
return nil
|
return
|
||||||
}
|
}
|
||||||
let error = error as! Client.Error
|
let error = error as! Client.Error
|
||||||
|
|
||||||
let config = ToastConfiguration(from: error, with: "Error Loading Context", in: self) { [weak self] toast in
|
let config = ToastConfiguration(from: error, with: "Error Loading Context", in: self) { [weak self] toast in
|
||||||
toast.dismissToast(animated: true)
|
toast.dismissToast(animated: true)
|
||||||
await self?.loadTree(for: mainStatus)
|
await self?.loadContext(for: mainStatus)
|
||||||
}
|
}
|
||||||
self.showToast(configuration: config, animated: true)
|
self.showToast(configuration: config, animated: true)
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -386,7 +341,6 @@ extension ConversationViewController {
|
||||||
enum Mode {
|
enum Mode {
|
||||||
case localID(String)
|
case localID(String)
|
||||||
case resolve(URL)
|
case resolve(URL)
|
||||||
case preloaded(ConversationTree)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -154,9 +154,9 @@ class ExploreViewController: UIViewController, UICollectionViewDelegate, Collect
|
||||||
private func applyInitialSnapshot() {
|
private func applyInitialSnapshot() {
|
||||||
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||||
snapshot.appendSections(Section.allCases.filter { $0 != .discover })
|
snapshot.appendSections(Section.allCases.filter { $0 != .discover })
|
||||||
snapshot.appendItems([.bookmarks, .favorites], toSection: .bookmarks)
|
snapshot.appendItems([.bookmarks], toSection: .bookmarks)
|
||||||
if mastodonController.instanceFeatures.trends,
|
if mastodonController.instanceFeatures.trends,
|
||||||
!Preferences.shared.hideTrends {
|
!Preferences.shared.hideDiscover {
|
||||||
addDiscoverSection(to: &snapshot)
|
addDiscoverSection(to: &snapshot)
|
||||||
}
|
}
|
||||||
snapshot.appendItems([.addList], toSection: .lists)
|
snapshot.appendItems([.addList], toSection: .lists)
|
||||||
|
@ -175,17 +175,19 @@ class ExploreViewController: UIViewController, UICollectionViewDelegate, Collect
|
||||||
|
|
||||||
private func addDiscoverSection(to snapshot: inout NSDiffableDataSourceSnapshot<Section, Item>) {
|
private func addDiscoverSection(to snapshot: inout NSDiffableDataSourceSnapshot<Section, Item>) {
|
||||||
snapshot.insertSections([.discover], afterSection: .bookmarks)
|
snapshot.insertSections([.discover], afterSection: .bookmarks)
|
||||||
snapshot.appendItems([.trends], toSection: .discover)
|
snapshot.appendItems([.trendingTags, .profileDirectory], toSection: .discover)
|
||||||
|
if mastodonController.instanceFeatures.trendingStatusesAndLinks {
|
||||||
|
snapshot.insertItems([.trendingStatuses], beforeItem: .trendingTags)
|
||||||
|
snapshot.insertItems([.trendingLinks], afterItem: .trendingTags)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func ownInstanceLoaded(_ instance: Instance) {
|
private func ownInstanceLoaded(_ instance: Instance) {
|
||||||
var snapshot = self.dataSource.snapshot()
|
var snapshot = self.dataSource.snapshot()
|
||||||
if mastodonController.instanceFeatures.trends,
|
if mastodonController.instanceFeatures.trends,
|
||||||
!snapshot.sectionIdentifiers.contains(.discover) {
|
!snapshot.sectionIdentifiers.contains(.discover) {
|
||||||
addDiscoverSection(to: &snapshot)
|
snapshot.insertSections([.discover], afterSection: .bookmarks)
|
||||||
} else if !mastodonController.instanceFeatures.trends,
|
snapshot.appendItems([.trendingTags, .profileDirectory], toSection: .discover)
|
||||||
snapshot.sectionIdentifiers.contains(.discover) {
|
|
||||||
snapshot.deleteSections([.discover])
|
|
||||||
}
|
}
|
||||||
self.dataSource.apply(snapshot)
|
self.dataSource.apply(snapshot)
|
||||||
}
|
}
|
||||||
|
@ -248,7 +250,7 @@ class ExploreViewController: UIViewController, UICollectionViewDelegate, Collect
|
||||||
@objc private func preferencesChanged() {
|
@objc private func preferencesChanged() {
|
||||||
var snapshot = dataSource.snapshot()
|
var snapshot = dataSource.snapshot()
|
||||||
let hasSection = snapshot.sectionIdentifiers.contains(.discover)
|
let hasSection = snapshot.sectionIdentifiers.contains(.discover)
|
||||||
let hide = Preferences.shared.hideTrends
|
let hide = Preferences.shared.hideDiscover
|
||||||
if hasSection && hide {
|
if hasSection && hide {
|
||||||
snapshot.deleteSections([.discover])
|
snapshot.deleteSections([.discover])
|
||||||
} else if !hasSection && !hide {
|
} else if !hasSection && !hide {
|
||||||
|
@ -334,11 +336,17 @@ class ExploreViewController: UIViewController, UICollectionViewDelegate, Collect
|
||||||
case .bookmarks:
|
case .bookmarks:
|
||||||
show(BookmarksViewController(mastodonController: mastodonController), sender: nil)
|
show(BookmarksViewController(mastodonController: mastodonController), sender: nil)
|
||||||
|
|
||||||
case .favorites:
|
case .trendingStatuses:
|
||||||
show(FavoritesViewController(mastodonController: mastodonController), sender: nil)
|
show(TrendingStatusesViewController(mastodonController: mastodonController), sender: nil)
|
||||||
|
|
||||||
case .trends:
|
case .trendingTags:
|
||||||
show(TrendsViewController(mastodonController: mastodonController), sender: nil)
|
show(TrendingHashtagsViewController(mastodonController: mastodonController), sender: nil)
|
||||||
|
|
||||||
|
case .trendingLinks:
|
||||||
|
show(TrendingLinksViewController(mastodonController: mastodonController), sender: nil)
|
||||||
|
|
||||||
|
case .profileDirectory:
|
||||||
|
show(ProfileDirectoryViewController(mastodonController: mastodonController), sender: nil)
|
||||||
|
|
||||||
case let .list(list):
|
case let .list(list):
|
||||||
show(ListTimelineViewController(for: list, mastodonController: mastodonController), sender: nil)
|
show(ListTimelineViewController(for: list, mastodonController: mastodonController), sender: nil)
|
||||||
|
@ -387,7 +395,7 @@ extension ExploreViewController {
|
||||||
case .bookmarks:
|
case .bookmarks:
|
||||||
return nil
|
return nil
|
||||||
case .discover:
|
case .discover:
|
||||||
return nil
|
return NSLocalizedString("Discover", comment: "discover section title")
|
||||||
case .lists:
|
case .lists:
|
||||||
return NSLocalizedString("Lists", comment: "explore lists section title")
|
return NSLocalizedString("Lists", comment: "explore lists section title")
|
||||||
case .savedHashtags:
|
case .savedHashtags:
|
||||||
|
@ -400,8 +408,10 @@ extension ExploreViewController {
|
||||||
|
|
||||||
enum Item: Hashable {
|
enum Item: Hashable {
|
||||||
case bookmarks
|
case bookmarks
|
||||||
case favorites
|
case trendingStatuses
|
||||||
case trends
|
case trendingTags
|
||||||
|
case trendingLinks
|
||||||
|
case profileDirectory
|
||||||
case list(List)
|
case list(List)
|
||||||
case addList
|
case addList
|
||||||
case savedHashtag(Hashtag)
|
case savedHashtag(Hashtag)
|
||||||
|
@ -413,10 +423,14 @@ extension ExploreViewController {
|
||||||
switch self {
|
switch self {
|
||||||
case .bookmarks:
|
case .bookmarks:
|
||||||
return NSLocalizedString("Bookmarks", comment: "bookmarks nav item title")
|
return NSLocalizedString("Bookmarks", comment: "bookmarks nav item title")
|
||||||
case .favorites:
|
case .trendingStatuses:
|
||||||
return NSLocalizedString("Favorites", comment: "favorites nav item title")
|
return NSLocalizedString("Trending Posts", comment: "trending statuses nav item title")
|
||||||
case .trends:
|
case .trendingTags:
|
||||||
return NSLocalizedString("Trends", comment: "trends nav item title")
|
return NSLocalizedString("Trending Hashtags", comment: "trending hashtags nav item title")
|
||||||
|
case .trendingLinks:
|
||||||
|
return NSLocalizedString("Trending Links", comment: "trending links nav item title")
|
||||||
|
case .profileDirectory:
|
||||||
|
return NSLocalizedString("Profile Directory", comment: "profile directory nav item title")
|
||||||
case let .list(list):
|
case let .list(list):
|
||||||
return list.title
|
return list.title
|
||||||
case .addList:
|
case .addList:
|
||||||
|
@ -437,10 +451,14 @@ extension ExploreViewController {
|
||||||
switch self {
|
switch self {
|
||||||
case .bookmarks:
|
case .bookmarks:
|
||||||
name = "bookmark.fill"
|
name = "bookmark.fill"
|
||||||
case .favorites:
|
case .trendingStatuses:
|
||||||
name = "star.fill"
|
name = "doc.text.image"
|
||||||
case .trends:
|
case .trendingTags:
|
||||||
name = "chart.line.uptrend.xyaxis"
|
name = "number"
|
||||||
|
case .trendingLinks:
|
||||||
|
name = "link"
|
||||||
|
case .profileDirectory:
|
||||||
|
name = "person.2.fill"
|
||||||
case .list(_):
|
case .list(_):
|
||||||
name = "list.bullet"
|
name = "list.bullet"
|
||||||
case .addList, .addSavedHashtag:
|
case .addList, .addSavedHashtag:
|
||||||
|
@ -459,9 +477,13 @@ extension ExploreViewController {
|
||||||
switch (lhs, rhs) {
|
switch (lhs, rhs) {
|
||||||
case (.bookmarks, .bookmarks):
|
case (.bookmarks, .bookmarks):
|
||||||
return true
|
return true
|
||||||
case (.favorites, .favorites):
|
case (.trendingStatuses, .trendingStatuses):
|
||||||
return true
|
return true
|
||||||
case (.trends, .trends):
|
case (.trendingTags, .trendingTags):
|
||||||
|
return true
|
||||||
|
case (.trendingLinks, .trendingLinks):
|
||||||
|
return true
|
||||||
|
case (.profileDirectory, .profileDirectory):
|
||||||
return true
|
return true
|
||||||
case let (.list(a), .list(b)):
|
case let (.list(a), .list(b)):
|
||||||
return a.id == b.id && a.title == b.title
|
return a.id == b.id && a.title == b.title
|
||||||
|
@ -484,10 +506,14 @@ extension ExploreViewController {
|
||||||
switch self {
|
switch self {
|
||||||
case .bookmarks:
|
case .bookmarks:
|
||||||
hasher.combine("bookmarks")
|
hasher.combine("bookmarks")
|
||||||
case .favorites:
|
case .trendingStatuses:
|
||||||
hasher.combine("favorites")
|
hasher.combine("trendingStatuses")
|
||||||
case .trends:
|
case .trendingTags:
|
||||||
hasher.combine("trends")
|
hasher.combine("trendingTags")
|
||||||
|
case .trendingLinks:
|
||||||
|
hasher.combine("trendingLinks")
|
||||||
|
case .profileDirectory:
|
||||||
|
hasher.combine("profileDirectory")
|
||||||
case let .list(list):
|
case let .list(list):
|
||||||
hasher.combine("list")
|
hasher.combine("list")
|
||||||
hasher.combine(list.id)
|
hasher.combine(list.id)
|
||||||
|
|
|
@ -1,79 +0,0 @@
|
||||||
//
|
|
||||||
// InlineTrendsViewController.swift
|
|
||||||
// Tusker
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 6/24/20.
|
|
||||||
// Copyright © 2020 Shadowfacts. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
|
|
||||||
class InlineTrendsViewController: UIViewController {
|
|
||||||
|
|
||||||
weak var mastodonController: MastodonController!
|
|
||||||
|
|
||||||
var resultsController: SearchResultsViewController!
|
|
||||||
var searchController: UISearchController!
|
|
||||||
|
|
||||||
var searchControllerStatusOnAppearance: Bool? = nil
|
|
||||||
|
|
||||||
init(mastodonController: MastodonController) {
|
|
||||||
self.mastodonController = mastodonController
|
|
||||||
|
|
||||||
super.init(nibName: nil, bundle: nil)
|
|
||||||
|
|
||||||
title = NSLocalizedString("Explore", comment: "explore tab title")
|
|
||||||
}
|
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
|
||||||
fatalError("init(coder:) has not been implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override func viewDidLoad() {
|
|
||||||
super.viewDidLoad()
|
|
||||||
|
|
||||||
resultsController = SearchResultsViewController(mastodonController: mastodonController)
|
|
||||||
resultsController.exploreNavigationController = self.navigationController
|
|
||||||
searchController = UISearchController(searchResultsController: resultsController)
|
|
||||||
searchController.obscuresBackgroundDuringPresentation = true
|
|
||||||
if #available(iOS 16.0, *) {
|
|
||||||
searchController.scopeBarActivation = .onSearchActivation
|
|
||||||
}
|
|
||||||
searchController.searchBar.autocapitalizationType = .none
|
|
||||||
searchController.searchBar.delegate = resultsController
|
|
||||||
searchController.searchBar.scopeButtonTitles = SearchResultsViewController.Scope.allCases.map(\.title)
|
|
||||||
searchController.hidesNavigationBarDuringPresentation = false
|
|
||||||
definesPresentationContext = true
|
|
||||||
|
|
||||||
navigationItem.searchController = searchController
|
|
||||||
navigationItem.hidesSearchBarWhenScrolling = false
|
|
||||||
if #available(iOS 16.0, *) {
|
|
||||||
navigationItem.preferredSearchBarPlacement = .stacked
|
|
||||||
}
|
|
||||||
|
|
||||||
let trends = TrendsViewController(mastodonController: mastodonController)
|
|
||||||
trends.view.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
addChild(trends)
|
|
||||||
view.addSubview(trends.view)
|
|
||||||
NSLayoutConstraint.activate([
|
|
||||||
trends.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
|
||||||
trends.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
|
||||||
trends.view.topAnchor.constraint(equalTo: view.topAnchor),
|
|
||||||
trends.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
|
||||||
])
|
|
||||||
trends.didMove(toParent: self)
|
|
||||||
}
|
|
||||||
|
|
||||||
override func viewDidAppear(_ animated: Bool) {
|
|
||||||
super.viewDidAppear(animated)
|
|
||||||
|
|
||||||
// this is a workaround for the issue that setting isActive on a search controller that is not visible
|
|
||||||
// does not cause it to automatically become active once it becomes visible
|
|
||||||
// see FB7814561
|
|
||||||
if let active = searchControllerStatusOnAppearance {
|
|
||||||
searchController.isActive = active
|
|
||||||
searchControllerStatusOnAppearance = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -9,13 +9,13 @@
|
||||||
import UIKit
|
import UIKit
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
|
|
||||||
class TrendingStatusesViewController: UIViewController, CollectionViewController {
|
class TrendingStatusesViewController: UIViewController {
|
||||||
|
|
||||||
weak var mastodonController: MastodonController!
|
weak var mastodonController: MastodonController!
|
||||||
let filterer: Filterer
|
let filterer: Filterer
|
||||||
|
|
||||||
var collectionView: UICollectionView! {
|
private var collectionView: UICollectionView {
|
||||||
view as? UICollectionView
|
view as! UICollectionView
|
||||||
}
|
}
|
||||||
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
||||||
|
|
||||||
|
@ -110,8 +110,6 @@ class TrendingStatusesViewController: UIViewController, CollectionViewController
|
||||||
override func viewWillAppear(_ animated: Bool) {
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
super.viewWillAppear(animated)
|
super.viewWillAppear(animated)
|
||||||
|
|
||||||
clearSelectionOnAppear(animated: animated)
|
|
||||||
|
|
||||||
if !loaded {
|
if !loaded {
|
||||||
loaded = true
|
loaded = true
|
||||||
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||||
|
|
|
@ -42,8 +42,6 @@ class FastAccountSwitcherViewController: UIViewController {
|
||||||
|
|
||||||
view.isHidden = true
|
view.isHidden = true
|
||||||
|
|
||||||
view.accessibilityViewIsModal = true
|
|
||||||
|
|
||||||
view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleTap(_:))))
|
view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleTap(_:))))
|
||||||
accountsStack.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:))))
|
accountsStack.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:))))
|
||||||
}
|
}
|
||||||
|
@ -69,16 +67,10 @@ class FastAccountSwitcherViewController: UIViewController {
|
||||||
|
|
||||||
view.isHidden = false
|
view.isHidden = false
|
||||||
|
|
||||||
func completion() {
|
|
||||||
UIAccessibility.post(notification: .screenChanged, argument: accountViews.first)
|
|
||||||
}
|
|
||||||
|
|
||||||
if UIAccessibility.prefersCrossFadeTransitions {
|
if UIAccessibility.prefersCrossFadeTransitions {
|
||||||
view.alpha = 0
|
view.alpha = 0
|
||||||
UIView.animate(withDuration: 0.2, delay: 0, options: [.curveEaseInOut, .allowUserInteraction]) {
|
UIView.animate(withDuration: 0.2, delay: 0, options: [.curveEaseInOut, .allowUserInteraction]) {
|
||||||
self.view.alpha = 1
|
self.view.alpha = 1
|
||||||
} completion: { _ in
|
|
||||||
completion()
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let totalDuration: TimeInterval = 0.5
|
let totalDuration: TimeInterval = 0.5
|
||||||
|
@ -103,8 +95,6 @@ class FastAccountSwitcherViewController: UIViewController {
|
||||||
accountView.transform = .identity
|
accountView.transform = .identity
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} completion: { _ in
|
|
||||||
completion()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -124,8 +114,6 @@ class FastAccountSwitcherViewController: UIViewController {
|
||||||
self.view.isHidden = true
|
self.view.isHidden = true
|
||||||
completion?()
|
completion?()
|
||||||
self.view.removeFromSuperview()
|
self.view.removeFromSuperview()
|
||||||
|
|
||||||
UIAccessibility.post(notification: .screenChanged, argument: nil)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -284,14 +272,6 @@ class FastAccountSwitcherViewController: UIViewController {
|
||||||
super.touchesBegan(touches, with: event)
|
super.touchesBegan(touches, with: event)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func accessibilityPerformEscape() -> Bool {
|
|
||||||
guard !view.isHidden else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
hide()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension FastAccountSwitcherViewController {
|
extension FastAccountSwitcherViewController {
|
||||||
|
|
|
@ -117,8 +117,6 @@ class FastSwitchingAccountView: UIView {
|
||||||
}
|
}
|
||||||
|
|
||||||
updateLabelColors()
|
updateLabelColors()
|
||||||
|
|
||||||
isAccessibilityElement = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setupAccount(account: LocalData.UserAccountInfo) {
|
private func setupAccount(account: LocalData.UserAccountInfo) {
|
||||||
|
@ -136,16 +134,12 @@ class FastSwitchingAccountView: UIView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
accessibilityLabel = "\(account.username!)@\(account.instanceURL.host!)"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setupPlaceholder() {
|
private func setupPlaceholder() {
|
||||||
usernameLabel.text = "Add Account"
|
usernameLabel.text = "Add Account"
|
||||||
instanceLabel.isHidden = true
|
instanceLabel.isHidden = true
|
||||||
avatarImageView.image = UIImage(systemName: "plus")
|
avatarImageView.image = UIImage(systemName: "plus")
|
||||||
|
|
||||||
accessibilityLabel = "Add Account"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateLabelColors() {
|
private func updateLabelColors() {
|
||||||
|
|
|
@ -28,7 +28,7 @@
|
||||||
<view contentMode="scaleToFill" ambiguous="YES" translatesAutoresizingMaskIntoConstraints="NO" id="kHo-B9-R7a">
|
<view contentMode="scaleToFill" ambiguous="YES" translatesAutoresizingMaskIntoConstraints="NO" id="kHo-B9-R7a">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="36"/>
|
<rect key="frame" x="0.0" y="0.0" width="375" height="36"/>
|
||||||
</view>
|
</view>
|
||||||
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" editable="NO" textAlignment="natural" adjustsFontForContentSizeCategory="YES" translatesAutoresizingMaskIntoConstraints="NO" id="JZk-BO-2Vh">
|
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" editable="NO" textAlignment="natural" adjustsFontForContentSizeCategory="YES" selectable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="JZk-BO-2Vh">
|
||||||
<rect key="frame" x="0.0" y="517" width="375" height="150"/>
|
<rect key="frame" x="0.0" y="517" width="375" height="150"/>
|
||||||
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.5" colorSpace="custom" customColorSpace="displayP3"/>
|
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.5" colorSpace="custom" customColorSpace="displayP3"/>
|
||||||
<constraints>
|
<constraints>
|
||||||
|
|
|
@ -1,27 +0,0 @@
|
||||||
//
|
|
||||||
// BookmarksViewController.swift
|
|
||||||
// Tusker
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 12/15/19.
|
|
||||||
// Copyright © 2019 Shadowfacts. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
import Pachyderm
|
|
||||||
|
|
||||||
class BookmarksViewController: LocalPredicateStatusesViewController {
|
|
||||||
|
|
||||||
init(mastodonController: MastodonController) {
|
|
||||||
super.init(
|
|
||||||
predicate: { $0.bookmarked ?? false },
|
|
||||||
predicateTitle: "Bookmarks",
|
|
||||||
request: { Client.getBookmarks(range: $0) },
|
|
||||||
mastodonController: mastodonController
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
|
||||||
fatalError("init(coder:) has not been implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,27 +0,0 @@
|
||||||
//
|
|
||||||
// FavoritesViewController.swift
|
|
||||||
// Tusker
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 2/4/23.
|
|
||||||
// Copyright © 2023 Shadowfacts. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
import Pachyderm
|
|
||||||
|
|
||||||
class FavoritesViewController: LocalPredicateStatusesViewController {
|
|
||||||
|
|
||||||
init(mastodonController: MastodonController) {
|
|
||||||
super.init(
|
|
||||||
predicate: \.favourited,
|
|
||||||
predicateTitle: "Favorites",
|
|
||||||
request: { Client.getFavourites(range: $0) },
|
|
||||||
mastodonController: mastodonController
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
|
||||||
fatalError("init(coder:) has not been implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -41,7 +41,7 @@ class MainSidebarViewController: UIViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
var exploreTabItems: [Item] {
|
var exploreTabItems: [Item] {
|
||||||
var items: [Item] = [.explore, .bookmarks, .favorites]
|
var items: [Item] = [.explore, .bookmarks, .profileDirectory]
|
||||||
let snapshot = dataSource.snapshot()
|
let snapshot = dataSource.snapshot()
|
||||||
for case let .list(list) in snapshot.itemIdentifiers(inSection: .lists) {
|
for case let .list(list) in snapshot.itemIdentifiers(inSection: .lists) {
|
||||||
items.append(.list(list))
|
items.append(.list(list))
|
||||||
|
@ -104,6 +104,7 @@ class MainSidebarViewController: UIViewController {
|
||||||
select(item: .tab(.timelines), animated: false)
|
select(item: .tab(.timelines), animated: false)
|
||||||
|
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(reloadSavedInstances), name: .savedInstancesChanged, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(reloadSavedInstances), name: .savedInstancesChanged, object: nil)
|
||||||
|
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
|
||||||
|
|
||||||
mastodonController.$lists
|
mastodonController.$lists
|
||||||
.sink { [unowned self] in self.reloadLists($0) }
|
.sink { [unowned self] in self.reloadLists($0) }
|
||||||
|
@ -165,26 +166,47 @@ class MainSidebarViewController: UIViewController {
|
||||||
|
|
||||||
private func applyInitialSnapshot() {
|
private func applyInitialSnapshot() {
|
||||||
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||||
snapshot.appendSections(Section.allCases)
|
snapshot.appendSections(Section.allCases.filter { $0 != .discover })
|
||||||
snapshot.appendItems([
|
snapshot.appendItems([
|
||||||
.tab(.timelines),
|
.tab(.timelines),
|
||||||
.tab(.notifications),
|
.tab(.notifications),
|
||||||
.explore,
|
.explore,
|
||||||
.bookmarks,
|
.bookmarks,
|
||||||
.favorites,
|
|
||||||
.tab(.myProfile)
|
.tab(.myProfile)
|
||||||
], toSection: .tabs)
|
], toSection: .tabs)
|
||||||
snapshot.appendItems([
|
snapshot.appendItems([
|
||||||
.tab(.compose)
|
.tab(.compose)
|
||||||
], toSection: .compose)
|
], toSection: .compose)
|
||||||
|
if mastodonController.instanceFeatures.trends,
|
||||||
|
!Preferences.shared.hideDiscover {
|
||||||
|
snapshot.insertSections([.discover], afterSection: .compose)
|
||||||
|
}
|
||||||
dataSource.apply(snapshot, animatingDifferences: false)
|
dataSource.apply(snapshot, animatingDifferences: false)
|
||||||
|
|
||||||
|
applyDiscoverSectionSnapshot()
|
||||||
reloadLists(mastodonController.lists)
|
reloadLists(mastodonController.lists)
|
||||||
updateHashtagsSection(followed: mastodonController.followedHashtags)
|
updateHashtagsSection(followed: mastodonController.followedHashtags)
|
||||||
reloadSavedInstances()
|
reloadSavedInstances()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func applyDiscoverSectionSnapshot() {
|
||||||
|
var discoverSnapshot = NSDiffableDataSourceSectionSnapshot<Item>()
|
||||||
|
discoverSnapshot.append([.discoverHeader])
|
||||||
|
discoverSnapshot.append([
|
||||||
|
.profileDirectory,
|
||||||
|
], to: .discoverHeader)
|
||||||
|
dataSource.apply(discoverSnapshot, to: .discover)
|
||||||
|
}
|
||||||
|
|
||||||
private func ownInstanceLoaded(_ instance: Instance) {
|
private func ownInstanceLoaded(_ instance: Instance) {
|
||||||
|
if mastodonController.instanceFeatures.trends {
|
||||||
|
var snapshot = self.dataSource.snapshot()
|
||||||
|
if !snapshot.sectionIdentifiers.contains(.discover) {
|
||||||
|
snapshot.appendSections([.discover])
|
||||||
|
dataSource.apply(snapshot, animatingDifferences: false)
|
||||||
|
}
|
||||||
|
applyDiscoverSectionSnapshot()
|
||||||
|
}
|
||||||
let prevSelected = collectionView.indexPathsForSelectedItems
|
let prevSelected = collectionView.indexPathsForSelectedItems
|
||||||
|
|
||||||
if let prevSelected = prevSelected?.first {
|
if let prevSelected = prevSelected?.first {
|
||||||
|
@ -267,6 +289,22 @@ class MainSidebarViewController: UIViewController {
|
||||||
self.dataSource.apply(instancesSnapshot, to: .savedInstances)
|
self.dataSource.apply(instancesSnapshot, to: .savedInstances)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc private func preferencesChanged() {
|
||||||
|
var snapshot = dataSource.snapshot()
|
||||||
|
let hasSection = snapshot.sectionIdentifiers.contains(.discover)
|
||||||
|
let hide = Preferences.shared.hideDiscover
|
||||||
|
if hasSection && hide {
|
||||||
|
snapshot.deleteSections([.discover])
|
||||||
|
dataSource.apply(snapshot)
|
||||||
|
} else if !hasSection && !hide {
|
||||||
|
snapshot.insertSections([.discover], afterSection: .compose)
|
||||||
|
dataSource.apply(snapshot)
|
||||||
|
applyDiscoverSectionSnapshot()
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func returnToPreviousItem() {
|
private func returnToPreviousItem() {
|
||||||
let item = previouslySelectedItem ?? .tab(.timelines)
|
let item = previouslySelectedItem ?? .tab(.timelines)
|
||||||
previouslySelectedItem = nil
|
previouslySelectedItem = nil
|
||||||
|
@ -339,13 +377,15 @@ extension MainSidebarViewController {
|
||||||
enum Section: Int, Hashable, CaseIterable {
|
enum Section: Int, Hashable, CaseIterable {
|
||||||
case tabs
|
case tabs
|
||||||
case compose
|
case compose
|
||||||
|
case discover
|
||||||
case lists
|
case lists
|
||||||
case savedHashtags
|
case savedHashtags
|
||||||
case savedInstances
|
case savedInstances
|
||||||
}
|
}
|
||||||
enum Item: Hashable {
|
enum Item: Hashable {
|
||||||
case tab(MainTabBarViewController.Tab)
|
case tab(MainTabBarViewController.Tab)
|
||||||
case explore, bookmarks, favorites
|
case explore, bookmarks
|
||||||
|
case discoverHeader, profileDirectory
|
||||||
case listsHeader, list(List), addList
|
case listsHeader, list(List), addList
|
||||||
case savedHashtagsHeader, savedHashtag(Hashtag), addSavedHashtag
|
case savedHashtagsHeader, savedHashtag(Hashtag), addSavedHashtag
|
||||||
case savedInstancesHeader, savedInstance(URL), addSavedInstance
|
case savedInstancesHeader, savedInstance(URL), addSavedInstance
|
||||||
|
@ -358,8 +398,10 @@ extension MainSidebarViewController {
|
||||||
return "Explore"
|
return "Explore"
|
||||||
case .bookmarks:
|
case .bookmarks:
|
||||||
return "Bookmarks"
|
return "Bookmarks"
|
||||||
case .favorites:
|
case .discoverHeader:
|
||||||
return "Favorites"
|
return "Discover"
|
||||||
|
case .profileDirectory:
|
||||||
|
return "Profile Directory"
|
||||||
case .listsHeader:
|
case .listsHeader:
|
||||||
return "Lists"
|
return "Lists"
|
||||||
case let .list(list):
|
case let .list(list):
|
||||||
|
@ -389,15 +431,15 @@ extension MainSidebarViewController {
|
||||||
return "magnifyingglass"
|
return "magnifyingglass"
|
||||||
case .bookmarks:
|
case .bookmarks:
|
||||||
return "bookmark"
|
return "bookmark"
|
||||||
case .favorites:
|
case .profileDirectory:
|
||||||
return "star"
|
return "person.2.fill"
|
||||||
case .list(_):
|
case .list(_):
|
||||||
return "list.bullet"
|
return "list.bullet"
|
||||||
case .savedHashtag(_):
|
case .savedHashtag(_):
|
||||||
return "number"
|
return "number"
|
||||||
case .savedInstance(_):
|
case .savedInstance(_):
|
||||||
return "globe"
|
return "globe"
|
||||||
case .listsHeader, .savedHashtagsHeader, .savedInstancesHeader:
|
case .discoverHeader, .listsHeader, .savedHashtagsHeader, .savedInstancesHeader:
|
||||||
return nil
|
return nil
|
||||||
case .addList, .addSavedHashtag, .addSavedInstance:
|
case .addList, .addSavedHashtag, .addSavedInstance:
|
||||||
return "plus"
|
return "plus"
|
||||||
|
@ -406,7 +448,7 @@ extension MainSidebarViewController {
|
||||||
|
|
||||||
var hasChildren: Bool {
|
var hasChildren: Bool {
|
||||||
switch self {
|
switch self {
|
||||||
case .listsHeader, .savedHashtagsHeader, .savedInstancesHeader:
|
case .discoverHeader, .listsHeader, .savedHashtagsHeader, .savedInstancesHeader:
|
||||||
return true
|
return true
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
|
|
|
@ -217,7 +217,7 @@ extension MainSplitViewController: UISplitViewControllerDelegate {
|
||||||
// Make sure viewDidLoad is called so that the searchController/resultsController have been initialized
|
// Make sure viewDidLoad is called so that the searchController/resultsController have been initialized
|
||||||
explore.loadViewIfNeeded()
|
explore.loadViewIfNeeded()
|
||||||
|
|
||||||
let search = secondaryNavController.viewControllers.first as! InlineTrendsViewController
|
let search = secondaryNavController.viewControllers.first as! SearchViewController
|
||||||
// Copy the search query from the search VC to the Explore VC's search controller.
|
// Copy the search query from the search VC to the Explore VC's search controller.
|
||||||
let query = search.searchController.searchBar.text ?? ""
|
let query = search.searchController.searchBar.text ?? ""
|
||||||
explore.searchController.searchBar.text = query
|
explore.searchController.searchBar.text = query
|
||||||
|
@ -232,15 +232,15 @@ extension MainSplitViewController: UISplitViewControllerDelegate {
|
||||||
|
|
||||||
tabBarViewController.select(tab: .explore)
|
tabBarViewController.select(tab: .explore)
|
||||||
|
|
||||||
case .bookmarks, .favorites, .list(_), .savedHashtag(_), .savedInstance(_):
|
case .bookmarks, .profileDirectory, .list(_), .savedHashtag(_), .savedInstance(_):
|
||||||
tabBarViewController.select(tab: .explore)
|
tabBarViewController.select(tab: .explore)
|
||||||
// Make sure the Explore VC doesn't show its search bar when it appears, in case the user was previously
|
// Make sure the Explore VC doesn't show it's search bar when it appears, in case the user was previously
|
||||||
// in compact mode and performing a search.
|
// in compact mode and performing a search.
|
||||||
let exploreNav = tabBarViewController.viewController(for: .explore) as! UINavigationController
|
let exploreNav = tabBarViewController.viewController(for: .explore) as! UINavigationController
|
||||||
let explore = exploreNav.viewControllers.first as! ExploreViewController
|
let explore = exploreNav.viewControllers.first as! ExploreViewController
|
||||||
explore.searchControllerStatusOnAppearance = false
|
explore.searchControllerStatusOnAppearance = false
|
||||||
|
|
||||||
case .listsHeader, .addList, .savedHashtagsHeader, .addSavedHashtag, .savedInstancesHeader, .addSavedInstance:
|
case .discoverHeader, .listsHeader, .addList, .savedHashtagsHeader, .addSavedHashtag, .savedInstancesHeader, .addSavedInstance:
|
||||||
// These items are not selectable in the sidebar collection view, so this code is unreachable.
|
// These items are not selectable in the sidebar collection view, so this code is unreachable.
|
||||||
fatalError("unexpected selected sidebar item: \(sidebar.selectedItem!)")
|
fatalError("unexpected selected sidebar item: \(sidebar.selectedItem!)")
|
||||||
}
|
}
|
||||||
|
@ -285,9 +285,7 @@ extension MainSplitViewController: UISplitViewControllerDelegate {
|
||||||
// Search screen has special considerations, all others can be transferred directly.
|
// Search screen has special considerations, all others can be transferred directly.
|
||||||
if tabNavigationStack.count == 1 || ((tabNavigationStack.first as? ExploreViewController)?.searchController?.isActive ?? false) {
|
if tabNavigationStack.count == 1 || ((tabNavigationStack.first as? ExploreViewController)?.searchController?.isActive ?? false) {
|
||||||
exploreItem = .explore
|
exploreItem = .explore
|
||||||
// reuse the existing VC, if there is one
|
let searchVC = SearchViewController(mastodonController: mastodonController)
|
||||||
let searchVC = getOrCreateNavigationStack(item: .explore).first! as! InlineTrendsViewController
|
|
||||||
// load the view so that the search controller is accessible
|
|
||||||
searchVC.loadViewIfNeeded()
|
searchVC.loadViewIfNeeded()
|
||||||
let explore = tabNavigationStack.first as! ExploreViewController
|
let explore = tabNavigationStack.first as! ExploreViewController
|
||||||
if let exploreSearchControler = explore.searchController,
|
if let exploreSearchControler = explore.searchController,
|
||||||
|
@ -305,8 +303,6 @@ extension MainSplitViewController: UISplitViewControllerDelegate {
|
||||||
switch tabNavigationStack[1] {
|
switch tabNavigationStack[1] {
|
||||||
case is BookmarksViewController:
|
case is BookmarksViewController:
|
||||||
exploreItem = .bookmarks
|
exploreItem = .bookmarks
|
||||||
case is FavoritesViewController:
|
|
||||||
exploreItem = .favorites
|
|
||||||
case let listVC as ListTimelineViewController:
|
case let listVC as ListTimelineViewController:
|
||||||
exploreItem = .list(listVC.list)
|
exploreItem = .list(listVC.list)
|
||||||
case let hashtagVC as HashtagTimelineViewController:
|
case let hashtagVC as HashtagTimelineViewController:
|
||||||
|
@ -317,11 +313,10 @@ extension MainSplitViewController: UISplitViewControllerDelegate {
|
||||||
exploreItem = .explore
|
exploreItem = .explore
|
||||||
// these three VCs are part of the root SearchViewController, so we don't need to transfer them
|
// these three VCs are part of the root SearchViewController, so we don't need to transfer them
|
||||||
skipFirst = 2
|
skipFirst = 2
|
||||||
|
case is ProfileDirectoryViewController:
|
||||||
|
exploreItem = .profileDirectory
|
||||||
default:
|
default:
|
||||||
// transfer the navigation stack prepending, the existing explore VC
|
fatalError("unhandled second-level explore screen: \(tabNavigationStack[1])")
|
||||||
// if there was other stuff on the explore stack, it will get discarded
|
|
||||||
toPrepend = getOrCreateNavigationStack(item: .explore).first!
|
|
||||||
exploreItem = .explore
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
transferNavigationStack(from: tabNavController, to: exploreItem!, skipFirst: skipFirst, prepend: toPrepend)
|
transferNavigationStack(from: tabNavController, to: exploreItem!, skipFirst: skipFirst, prepend: toPrepend)
|
||||||
|
@ -377,18 +372,18 @@ fileprivate extension MainSidebarViewController.Item {
|
||||||
case let .tab(tab):
|
case let .tab(tab):
|
||||||
return tab.createViewController(mastodonController)
|
return tab.createViewController(mastodonController)
|
||||||
case .explore:
|
case .explore:
|
||||||
return InlineTrendsViewController(mastodonController: mastodonController)
|
return SearchViewController(mastodonController: mastodonController)
|
||||||
case .bookmarks:
|
case .bookmarks:
|
||||||
return BookmarksViewController(mastodonController: mastodonController)
|
return BookmarksViewController(mastodonController: mastodonController)
|
||||||
case .favorites:
|
case .profileDirectory:
|
||||||
return FavoritesViewController(mastodonController: mastodonController)
|
return ProfileDirectoryViewController(mastodonController: mastodonController)
|
||||||
case let .list(list):
|
case let .list(list):
|
||||||
return ListTimelineViewController(for: list, mastodonController: mastodonController)
|
return ListTimelineViewController(for: list, mastodonController: mastodonController)
|
||||||
case let .savedHashtag(hashtag):
|
case let .savedHashtag(hashtag):
|
||||||
return HashtagTimelineViewController(for: hashtag, mastodonController: mastodonController)
|
return HashtagTimelineViewController(for: hashtag, mastodonController: mastodonController)
|
||||||
case let .savedInstance(url):
|
case let .savedInstance(url):
|
||||||
return InstanceTimelineViewController(for: url, parentMastodonController: mastodonController)
|
return InstanceTimelineViewController(for: url, parentMastodonController: mastodonController)
|
||||||
case .listsHeader, .addList, .savedHashtagsHeader, .addSavedHashtag, .savedInstancesHeader, .addSavedInstance:
|
case .discoverHeader, .listsHeader, .addList, .savedHashtagsHeader, .addSavedHashtag, .savedInstancesHeader, .addSavedInstance:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -478,7 +473,7 @@ extension MainSplitViewController: TuskerRootViewController {
|
||||||
select(item: .explore)
|
select(item: .explore)
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let searchViewController = secondaryNavController.viewControllers.first as? InlineTrendsViewController else {
|
guard let searchViewController = secondaryNavController.viewControllers.first as? SearchViewController else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -296,7 +296,6 @@ class NotificationsTableViewController: DiffableTimelineLikeTableViewController<
|
||||||
completion(true)
|
completion(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
dismissAction.accessibilityLabel = "Dismiss Notification"
|
|
||||||
dismissAction.image = UIImage(systemName: "clear.fill")
|
dismissAction.image = UIImage(systemName: "clear.fill")
|
||||||
|
|
||||||
let cellConfiguration = (tableView.cellForRow(at: indexPath) as? TableViewSwipeActionProvider)?.trailingSwipeActionsConfiguration()
|
let cellConfiguration = (tableView.cellForRow(at: indexPath) as? TableViewSwipeActionProvider)?.trailingSwipeActionsConfiguration()
|
||||||
|
|
|
@ -29,21 +29,6 @@ struct AppearancePrefsView : View {
|
||||||
Preferences.shared.avatarStyle = $0 ? .circle : .roundRect
|
Preferences.shared.avatarStyle = $0 ? .circle : .roundRect
|
||||||
}
|
}
|
||||||
|
|
||||||
private let accentColorsAndImages: [(Preferences.AccentColor, UIImage?)] = Preferences.AccentColor.allCases.map { color in
|
|
||||||
var image: UIImage?
|
|
||||||
if let color = color.color {
|
|
||||||
if #available(iOS 16.0, *) {
|
|
||||||
image = UIImage(systemName: "circle.fill")!.withTintColor(color, renderingMode: .alwaysTemplate).withRenderingMode(.alwaysOriginal)
|
|
||||||
} else {
|
|
||||||
image = UIGraphicsImageRenderer(size: CGSize(width: 20, height: 20)).image { context in
|
|
||||||
color.setFill()
|
|
||||||
context.cgContext.fillEllipse(in: CGRect(x: 0, y: 0, width: 20, height: 20))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return (color, image)
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
List {
|
List {
|
||||||
themeSection
|
themeSection
|
||||||
|
@ -63,12 +48,12 @@ struct AppearancePrefsView : View {
|
||||||
}
|
}
|
||||||
|
|
||||||
Picker(selection: accentColor, label: Text("Accent Color")) {
|
Picker(selection: accentColor, label: Text("Accent Color")) {
|
||||||
ForEach(accentColorsAndImages, id: \.0.rawValue) { (color, image) in
|
ForEach(Preferences.AccentColor.allCases, id: \.rawValue) { color in
|
||||||
HStack {
|
HStack {
|
||||||
Text(color.name)
|
Text(color.name)
|
||||||
if let image {
|
if let color = color.color {
|
||||||
Spacer()
|
Spacer()
|
||||||
Image(uiImage: image)
|
Image(uiImage: UIImage(systemName: "circle.fill")!.withTintColor(color, renderingMode: .alwaysTemplate).withRenderingMode(.alwaysOriginal))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.tag(color)
|
.tag(color)
|
||||||
|
|
|
@ -6,11 +6,11 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import TTTKit
|
||||||
|
|
||||||
struct PreferencesView: View {
|
struct PreferencesView: View {
|
||||||
let mastodonController: MastodonController
|
let mastodonController: MastodonController
|
||||||
|
@ObservedObject var localData = LocalData.shared
|
||||||
@ObservedObject private var localData = LocalData.shared
|
|
||||||
@State private var showingLogoutConfirmation = false
|
@State private var showingLogoutConfirmation = false
|
||||||
|
|
||||||
init(mastodonController: MastodonController) {
|
init(mastodonController: MastodonController) {
|
||||||
|
@ -18,109 +18,101 @@ struct PreferencesView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
List {
|
// workaround: the navigation view is provided by MyProfileTableViewController so that it can inject the Done button
|
||||||
accountsSection
|
// NavigationView {
|
||||||
preferencesSection
|
List {
|
||||||
aboutSection
|
Section(header: Text("Accounts")) {
|
||||||
}
|
ForEach(localData.accounts, id: \.accessToken) { (account) in
|
||||||
.listStyle(.insetGrouped)
|
Button(action: {
|
||||||
.navigationBarTitle("Preferences")
|
NotificationCenter.default.post(name: .activateAccount, object: nil, userInfo: ["account": account])
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
}) {
|
||||||
}
|
HStack {
|
||||||
|
LocalAccountAvatarView(localAccountInfo: account)
|
||||||
private var accountsSection: some View {
|
VStack(alignment: .leading) {
|
||||||
Section {
|
Text(verbatim: account.username)
|
||||||
ForEach(localData.accounts, id: \.accessToken) { (account) in
|
.foregroundColor(.primary)
|
||||||
Button(action: {
|
Text(verbatim: account.instanceURL.host!)
|
||||||
NotificationCenter.default.post(name: .activateAccount, object: nil, userInfo: ["account": account])
|
.font(.caption)
|
||||||
}) {
|
.foregroundColor(.primary)
|
||||||
HStack {
|
}
|
||||||
LocalAccountAvatarView(localAccountInfo: account)
|
Spacer()
|
||||||
VStack(alignment: .leading) {
|
if account == mastodonController.accountInfo! {
|
||||||
Text(verbatim: account.username)
|
Image(systemName: "checkmark")
|
||||||
.foregroundColor(.primary)
|
.renderingMode(.template)
|
||||||
Text(verbatim: account.instanceURL.host!)
|
.foregroundColor(.secondary)
|
||||||
.font(.caption)
|
}
|
||||||
.foregroundColor(.primary)
|
}
|
||||||
|
}.onDrag {
|
||||||
|
let activity = UserActivityManager.mainSceneActivity(accountID: account.id)
|
||||||
|
return NSItemProvider(object: activity)
|
||||||
}
|
}
|
||||||
Spacer()
|
}.onDelete { (indices: IndexSet) in
|
||||||
if account == mastodonController.accountInfo! {
|
var indices = indices
|
||||||
Image(systemName: "checkmark")
|
var logoutFromCurrent = false
|
||||||
.renderingMode(.template)
|
if let index = indices.first(where: { localData.accounts[$0] == mastodonController.accountInfo! }) {
|
||||||
.foregroundColor(.secondary)
|
logoutFromCurrent = true
|
||||||
|
indices.remove(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
indices.forEach { LogoutService(accountInfo: localData.accounts[$0]).run() }
|
||||||
|
|
||||||
|
if logoutFromCurrent {
|
||||||
|
self.logoutPressed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(action: {
|
||||||
|
NotificationCenter.default.post(name: .addAccount, object: nil)
|
||||||
|
}) {
|
||||||
|
Text("Add Account...")
|
||||||
|
}
|
||||||
|
if localData.getMostRecentAccount() != nil {
|
||||||
|
Button(action: {
|
||||||
|
self.showingLogoutConfirmation = true
|
||||||
|
}) {
|
||||||
|
Text("Logout from current")
|
||||||
|
}.alert(isPresented: $showingLogoutConfirmation) {
|
||||||
|
Alert(title: Text("Are you sure you want to logout?"), message: nil, primaryButton: .destructive(Text("Logout"), action: self.logoutPressed), secondaryButton: .cancel())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.onDrag {
|
|
||||||
let activity = UserActivityManager.mainSceneActivity(accountID: account.id)
|
|
||||||
return NSItemProvider(object: activity)
|
|
||||||
}
|
|
||||||
}.onDelete { (indices: IndexSet) in
|
|
||||||
var indices = indices
|
|
||||||
var logoutFromCurrent = false
|
|
||||||
if let index = indices.first(where: { localData.accounts[$0] == mastodonController.accountInfo! }) {
|
|
||||||
logoutFromCurrent = true
|
|
||||||
indices.remove(index)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
indices.forEach { LogoutService(accountInfo: localData.accounts[$0]).run() }
|
Section {
|
||||||
|
NavigationLink(destination: AppearancePrefsView()) {
|
||||||
|
Text("Appearance")
|
||||||
|
}
|
||||||
|
NavigationLink(destination: ComposingPrefsView()) {
|
||||||
|
Text("Composing")
|
||||||
|
}
|
||||||
|
NavigationLink(destination: MediaPrefsView()) {
|
||||||
|
Text("Media")
|
||||||
|
}
|
||||||
|
NavigationLink(destination: BehaviorPrefsView()) {
|
||||||
|
Text("Behavior")
|
||||||
|
}
|
||||||
|
NavigationLink(destination: WellnessPrefsView()) {
|
||||||
|
Text("Digital Wellness")
|
||||||
|
}
|
||||||
|
NavigationLink(destination: AdvancedPrefsView()) {
|
||||||
|
Text("Advanced")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if logoutFromCurrent {
|
Section {
|
||||||
self.logoutPressed()
|
NavigationLink("About") {
|
||||||
|
AboutView()
|
||||||
|
}
|
||||||
|
NavigationLink("Tip Jar") {
|
||||||
|
TipJarView()
|
||||||
|
}
|
||||||
|
NavigationLink("Acknowledgements") {
|
||||||
|
AcknowledgementsView()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.listStyle(InsetGroupedListStyle())
|
||||||
Button(action: {
|
.navigationBarTitle(Text("Preferences"), displayMode: .inline)
|
||||||
NotificationCenter.default.post(name: .addAccount, object: nil)
|
// }
|
||||||
}) {
|
|
||||||
Text("Add Account...")
|
|
||||||
}
|
|
||||||
Button(action: {
|
|
||||||
self.showingLogoutConfirmation = true
|
|
||||||
}) {
|
|
||||||
Text("Logout from current")
|
|
||||||
}.alert(isPresented: $showingLogoutConfirmation) {
|
|
||||||
Alert(title: Text("Are you sure you want to logout?"), message: nil, primaryButton: .destructive(Text("Logout"), action: self.logoutPressed), secondaryButton: .cancel())
|
|
||||||
}
|
|
||||||
} header: {
|
|
||||||
Text("Accounts")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var preferencesSection: some View {
|
|
||||||
Section {
|
|
||||||
NavigationLink(destination: AppearancePrefsView()) {
|
|
||||||
Text("Appearance")
|
|
||||||
}
|
|
||||||
NavigationLink(destination: ComposingPrefsView()) {
|
|
||||||
Text("Composing")
|
|
||||||
}
|
|
||||||
NavigationLink(destination: MediaPrefsView()) {
|
|
||||||
Text("Media")
|
|
||||||
}
|
|
||||||
NavigationLink(destination: BehaviorPrefsView()) {
|
|
||||||
Text("Behavior")
|
|
||||||
}
|
|
||||||
NavigationLink(destination: WellnessPrefsView()) {
|
|
||||||
Text("Digital Wellness")
|
|
||||||
}
|
|
||||||
NavigationLink(destination: AdvancedPrefsView()) {
|
|
||||||
Text("Advanced")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var aboutSection: some View {
|
|
||||||
Section {
|
|
||||||
NavigationLink("About") {
|
|
||||||
AboutView()
|
|
||||||
}
|
|
||||||
NavigationLink("Tip Jar") {
|
|
||||||
TipJarView()
|
|
||||||
}
|
|
||||||
NavigationLink("Acknowledgements") {
|
|
||||||
AcknowledgementsView()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func logoutPressed() {
|
func logoutPressed() {
|
||||||
|
|
|
@ -17,7 +17,7 @@ struct WellnessPrefsView: View {
|
||||||
notificationsMode
|
notificationsMode
|
||||||
grayscaleImages
|
grayscaleImages
|
||||||
disableInfiniteScrolling
|
disableInfiniteScrolling
|
||||||
hideTrends
|
hideDiscover
|
||||||
}
|
}
|
||||||
.listStyle(InsetGroupedListStyle())
|
.listStyle(InsetGroupedListStyle())
|
||||||
.navigationBarTitle(Text("Digital Wellness"))
|
.navigationBarTitle(Text("Digital Wellness"))
|
||||||
|
@ -57,10 +57,10 @@ struct WellnessPrefsView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var hideTrends: some View {
|
private var hideDiscover: some View {
|
||||||
Section(footer: Text("Do not show Trends (hashtags, links, posts, suggested accounts) on the Explore screen.")) {
|
Section(footer: Text("Do not show the Discover section (Trends, Profile Directory) of the Explore screen or sidebar.")) {
|
||||||
Toggle(isOn: $preferences.hideTrends) {
|
Toggle(isOn: $preferences.hideDiscover) {
|
||||||
Text("Hide Trends")
|
Text("Hide Discover Section")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,6 @@
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
class MyProfileViewController: ProfileViewController {
|
class MyProfileViewController: ProfileViewController {
|
||||||
|
|
||||||
|
|
|
@ -335,14 +335,12 @@ extension ProfileViewController: TabbedPageViewController {
|
||||||
|
|
||||||
extension ProfileViewController: TabBarScrollableViewController {
|
extension ProfileViewController: TabBarScrollableViewController {
|
||||||
func tabBarScrollToTop() {
|
func tabBarScrollToTop() {
|
||||||
guard isViewLoaded else { return }
|
|
||||||
currentViewController.tabBarScrollToTop()
|
currentViewController.tabBarScrollToTop()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ProfileViewController: StatusBarTappableViewController {
|
extension ProfileViewController: StatusBarTappableViewController {
|
||||||
func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult {
|
func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult {
|
||||||
guard isViewLoaded else { return .stop }
|
|
||||||
return currentViewController.handleStatusBarTapped(xPosition: xPosition)
|
return currentViewController.handleStatusBarTapped(xPosition: xPosition)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,35 +1,36 @@
|
||||||
//
|
//
|
||||||
// TrendsViewController.swift
|
// SearchViewController.swift
|
||||||
// Tusker
|
// Tusker
|
||||||
//
|
//
|
||||||
// Created by Shadowfacts on 2/5/23.
|
// Created by Shadowfacts on 6/24/20.
|
||||||
// Copyright © 2023 Shadowfacts. All rights reserved.
|
// Copyright © 2020 Shadowfacts. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
import SafariServices
|
import SafariServices
|
||||||
|
import WebURLFoundationExtras
|
||||||
|
|
||||||
class TrendsViewController: UIViewController, CollectionViewController {
|
class SearchViewController: UIViewController, CollectionViewController {
|
||||||
|
|
||||||
let mastodonController: MastodonController
|
weak var mastodonController: MastodonController!
|
||||||
|
|
||||||
var collectionView: UICollectionView!
|
var collectionView: UICollectionView!
|
||||||
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
||||||
|
|
||||||
private var loadTask: Task<Void, Never>?
|
var resultsController: SearchResultsViewController!
|
||||||
|
var searchController: UISearchController!
|
||||||
|
|
||||||
private var isShowingTrends = false
|
var searchControllerStatusOnAppearance: Bool? = nil
|
||||||
private var shouldShowTrends: Bool {
|
|
||||||
mastodonController.instanceFeatures.trends && !Preferences.shared.hideTrends
|
private var loadTask: Task<Void, Never>?
|
||||||
}
|
|
||||||
|
|
||||||
init(mastodonController: MastodonController) {
|
init(mastodonController: MastodonController) {
|
||||||
self.mastodonController = mastodonController
|
self.mastodonController = mastodonController
|
||||||
|
|
||||||
super.init(nibName: nil, bundle: nil)
|
super.init(nibName: nil, bundle: nil)
|
||||||
|
|
||||||
title = "Trends"
|
title = NSLocalizedString("Explore", comment: "explore tab title")
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
|
@ -52,9 +53,9 @@ class TrendsViewController: UIViewController, CollectionViewController {
|
||||||
let item = NSCollectionLayoutItem(layoutSize: itemSize)
|
let item = NSCollectionLayoutItem(layoutSize: itemSize)
|
||||||
let groupSize = NSCollectionLayoutSize(widthDimension: .absolute(250), heightDimension: .estimated(280))
|
let groupSize = NSCollectionLayoutSize(widthDimension: .absolute(250), heightDimension: .estimated(280))
|
||||||
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
|
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
|
||||||
group.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 8, bottom: 0, trailing: 8)
|
group.edgeSpacing = NSCollectionLayoutEdgeSpacing(leading: .fixed(8), top: nil, trailing: .fixed(8), bottom: nil)
|
||||||
let section = NSCollectionLayoutSection(group: group)
|
let section = NSCollectionLayoutSection(group: group)
|
||||||
section.orthogonalScrollingBehavior = .groupPaging
|
section.orthogonalScrollingBehavior = .continuousGroupLeadingBoundary
|
||||||
section.boundarySupplementaryItems = [
|
section.boundarySupplementaryItems = [
|
||||||
NSCollectionLayoutBoundarySupplementaryItem(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(12)), elementKind: UICollectionView.elementKindSectionHeader, alignment: .topLeading)
|
NSCollectionLayoutBoundarySupplementaryItem(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(12)), elementKind: UICollectionView.elementKindSectionHeader, alignment: .topLeading)
|
||||||
]
|
]
|
||||||
|
@ -66,9 +67,9 @@ class TrendsViewController: UIViewController, CollectionViewController {
|
||||||
let item = NSCollectionLayoutItem(layoutSize: itemSize)
|
let item = NSCollectionLayoutItem(layoutSize: itemSize)
|
||||||
let groupSize = NSCollectionLayoutSize(widthDimension: .absolute(350), heightDimension: .absolute(250))
|
let groupSize = NSCollectionLayoutSize(widthDimension: .absolute(350), heightDimension: .absolute(250))
|
||||||
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
|
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
|
||||||
group.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 8, bottom: 0, trailing: 8)
|
group.edgeSpacing = NSCollectionLayoutEdgeSpacing(leading: .fixed(8), top: nil, trailing: .fixed(8), bottom: nil)
|
||||||
let section = NSCollectionLayoutSection(group: group)
|
let section = NSCollectionLayoutSection(group: group)
|
||||||
section.orthogonalScrollingBehavior = .groupPaging
|
section.orthogonalScrollingBehavior = .continuousGroupLeadingBoundary
|
||||||
section.boundarySupplementaryItems = [
|
section.boundarySupplementaryItems = [
|
||||||
NSCollectionLayoutBoundarySupplementaryItem(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(12)), elementKind: UICollectionView.elementKindSectionHeader, alignment: .topLeading)
|
NSCollectionLayoutBoundarySupplementaryItem(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(12)), elementKind: UICollectionView.elementKindSectionHeader, alignment: .topLeading)
|
||||||
]
|
]
|
||||||
|
@ -91,9 +92,53 @@ class TrendsViewController: UIViewController, CollectionViewController {
|
||||||
|
|
||||||
dataSource = createDataSource()
|
dataSource = createDataSource()
|
||||||
|
|
||||||
|
resultsController = SearchResultsViewController(mastodonController: mastodonController)
|
||||||
|
resultsController.exploreNavigationController = self.navigationController
|
||||||
|
searchController = UISearchController(searchResultsController: resultsController)
|
||||||
|
searchController.obscuresBackgroundDuringPresentation = true
|
||||||
|
if #available(iOS 16.0, *) {
|
||||||
|
searchController.scopeBarActivation = .onSearchActivation
|
||||||
|
}
|
||||||
|
searchController.searchBar.autocapitalizationType = .none
|
||||||
|
searchController.searchBar.delegate = resultsController
|
||||||
|
searchController.searchBar.scopeButtonTitles = SearchResultsViewController.Scope.allCases.map(\.title)
|
||||||
|
searchController.hidesNavigationBarDuringPresentation = false
|
||||||
|
definesPresentationContext = true
|
||||||
|
|
||||||
|
navigationItem.searchController = searchController
|
||||||
|
navigationItem.hidesSearchBarWhenScrolling = false
|
||||||
|
if #available(iOS 16.0, *) {
|
||||||
|
navigationItem.preferredSearchBarPlacement = .stacked
|
||||||
|
}
|
||||||
|
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
|
super.viewWillAppear(animated)
|
||||||
|
|
||||||
|
clearSelectionOnAppear(animated: animated)
|
||||||
|
|
||||||
|
loadTask?.cancel()
|
||||||
|
loadTask = Task(priority: .userInitiated) {
|
||||||
|
if (try? await mastodonController.getOwnInstance()) != nil {
|
||||||
|
await applySnapshot()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidAppear(_ animated: Bool) {
|
||||||
|
super.viewDidAppear(animated)
|
||||||
|
|
||||||
|
// this is a workaround for the issue that setting isActive on a search controller that is not visible
|
||||||
|
// does not cause it to automatically become active once it becomes visible
|
||||||
|
// see FB7814561
|
||||||
|
if let active = searchControllerStatusOnAppearance {
|
||||||
|
searchController.isActive = active
|
||||||
|
searchControllerStatusOnAppearance = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> {
|
private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> {
|
||||||
let sectionHeaderCell = UICollectionView.SupplementaryRegistration<UICollectionViewListCell>(elementKind: UICollectionView.elementKindSectionHeader) { [unowned self] (headerView, collectionView, indexPath) in
|
let sectionHeaderCell = UICollectionView.SupplementaryRegistration<UICollectionViewListCell>(elementKind: UICollectionView.elementKindSectionHeader) { [unowned self] (headerView, collectionView, indexPath) in
|
||||||
let section = self.dataSource.snapshot().sectionIdentifiers[indexPath.section]
|
let section = self.dataSource.snapshot().sectionIdentifiers[indexPath.section]
|
||||||
|
@ -143,27 +188,10 @@ class TrendsViewController: UIViewController, CollectionViewController {
|
||||||
return dataSource
|
return dataSource
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewWillAppear(_ animated: Bool) {
|
|
||||||
super.viewWillAppear(animated)
|
|
||||||
|
|
||||||
clearSelectionOnAppear(animated: animated)
|
|
||||||
|
|
||||||
if loadTask == nil {
|
|
||||||
loadTask = Task(priority: .userInitiated) {
|
|
||||||
if (try? await mastodonController.getOwnInstance()) != nil {
|
|
||||||
await loadTrends()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
private func loadTrends() async {
|
private func applySnapshot() async {
|
||||||
guard isShowingTrends != shouldShowTrends else {
|
guard mastodonController.instanceFeatures.trends,
|
||||||
return
|
!Preferences.shared.hideDiscover else {
|
||||||
}
|
|
||||||
isShowingTrends = shouldShowTrends
|
|
||||||
guard shouldShowTrends else {
|
|
||||||
await dataSource.apply(NSDiffableDataSourceSnapshot())
|
await dataSource.apply(NSDiffableDataSourceSnapshot())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -216,11 +244,9 @@ class TrendsViewController: UIViewController, CollectionViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func preferencesChanged() {
|
@objc private func preferencesChanged() {
|
||||||
if isShowingTrends != shouldShowTrends {
|
loadTask?.cancel()
|
||||||
loadTask?.cancel()
|
loadTask = Task {
|
||||||
loadTask = Task {
|
await applySnapshot()
|
||||||
await loadTrends()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -247,9 +273,10 @@ class TrendsViewController: UIViewController, CollectionViewController {
|
||||||
self.showToast(configuration: config, animated: true)
|
self.showToast(configuration: config, animated: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension TrendsViewController {
|
extension SearchViewController {
|
||||||
enum Section {
|
enum Section {
|
||||||
case trendingHashtags
|
case trendingHashtags
|
||||||
case trendingLinks
|
case trendingLinks
|
||||||
|
@ -275,7 +302,7 @@ extension TrendsViewController {
|
||||||
case link(Card)
|
case link(Card)
|
||||||
case account(String, Suggestion.Source)
|
case account(String, Suggestion.Source)
|
||||||
|
|
||||||
static func == (lhs: Item, rhs: Item) -> Bool {
|
static func == (lhs: SearchViewController.Item, rhs: SearchViewController.Item) -> Bool {
|
||||||
switch (lhs, rhs) {
|
switch (lhs, rhs) {
|
||||||
case let (.status(a, _), .status(b, _)):
|
case let (.status(a, _), .status(b, _)):
|
||||||
return a == b
|
return a == b
|
||||||
|
@ -309,7 +336,7 @@ extension TrendsViewController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension TrendsViewController: UICollectionViewDelegate {
|
extension SearchViewController: UICollectionViewDelegate {
|
||||||
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||||
guard let item = dataSource.itemIdentifier(for: indexPath) else {
|
guard let item = dataSource.itemIdentifier(for: indexPath) else {
|
||||||
return
|
return
|
||||||
|
@ -416,7 +443,7 @@ extension TrendsViewController: UICollectionViewDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension TrendsViewController: UICollectionViewDragDelegate {
|
extension SearchViewController: UICollectionViewDragDelegate {
|
||||||
func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
|
func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
|
||||||
guard let item = dataSource.itemIdentifier(for: indexPath) else {
|
guard let item = dataSource.itemIdentifier(for: indexPath) else {
|
||||||
return []
|
return []
|
||||||
|
@ -463,17 +490,17 @@ extension TrendsViewController: UICollectionViewDragDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension TrendsViewController: TuskerNavigationDelegate {
|
extension SearchViewController: TuskerNavigationDelegate {
|
||||||
var apiController: MastodonController! { mastodonController }
|
var apiController: MastodonController! { mastodonController }
|
||||||
}
|
}
|
||||||
|
|
||||||
extension TrendsViewController: ToastableViewController {
|
extension SearchViewController: ToastableViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension TrendsViewController: MenuActionProvider {
|
extension SearchViewController: MenuActionProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension TrendsViewController: StatusCollectionViewCellDelegate {
|
extension SearchViewController: StatusCollectionViewCellDelegate {
|
||||||
func statusCellNeedsReconfigure(_ cell: StatusCollectionViewCell, animated: Bool, completion: (() -> Void)?) {
|
func statusCellNeedsReconfigure(_ cell: StatusCollectionViewCell, animated: Bool, completion: (() -> Void)?) {
|
||||||
if let indexPath = collectionView.indexPath(for: cell) {
|
if let indexPath = collectionView.indexPath(for: cell) {
|
||||||
var snapshot = dataSource.snapshot()
|
var snapshot = dataSource.snapshot()
|
|
@ -48,7 +48,6 @@ class StatusActionAccountListViewController: UIViewController {
|
||||||
indicator.centerYAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerYAnchor),
|
indicator.centerYAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerYAnchor),
|
||||||
])
|
])
|
||||||
case .displaying(let vc):
|
case .displaying(let vc):
|
||||||
vc.view.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
embedChild(vc)
|
embedChild(vc)
|
||||||
case .notFound:
|
case .notFound:
|
||||||
showStatusNotFound()
|
showStatusNotFound()
|
||||||
|
|
|
@ -84,58 +84,47 @@ class InstanceTimelineViewController: TimelineViewController {
|
||||||
// MARK: Timeline
|
// MARK: Timeline
|
||||||
|
|
||||||
override func handleLoadAllError(_ error: Swift.Error) async {
|
override func handleLoadAllError(_ error: Swift.Error) async {
|
||||||
guard let error = error as? Client.Error else {
|
switch (error as? Client.Error)?.type {
|
||||||
await super.handleLoadAllError(error)
|
case .mastodonError(422, _), .unexpectedStatus(422):
|
||||||
return
|
collectionView.isHidden = true
|
||||||
}
|
view.backgroundColor = .systemBackground
|
||||||
let code: Int
|
|
||||||
switch error.type {
|
let image = UIImageView(image: UIImage(systemName: "lock.fill"))
|
||||||
case .mastodonError(let c, _), .unexpectedStatus(let c):
|
image.tintColor = .secondaryLabel
|
||||||
code = c
|
image.contentMode = .scaleAspectFit
|
||||||
|
|
||||||
|
let title = UILabel()
|
||||||
|
title.textColor = .secondaryLabel
|
||||||
|
title.font = .preferredFont(forTextStyle: .title1).withTraits(.traitBold)!
|
||||||
|
title.adjustsFontForContentSizeCategory = true
|
||||||
|
title.numberOfLines = 0
|
||||||
|
title.textAlignment = .center
|
||||||
|
title.text = "This instance requires an account to view."
|
||||||
|
|
||||||
|
let stack = UIStackView(arrangedSubviews: [
|
||||||
|
image,
|
||||||
|
title,
|
||||||
|
])
|
||||||
|
stack.axis = .vertical
|
||||||
|
stack.alignment = .center
|
||||||
|
stack.spacing = 8
|
||||||
|
stack.isAccessibilityElement = true
|
||||||
|
stack.accessibilityLabel = title.text!
|
||||||
|
|
||||||
|
stack.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
view.addSubview(stack)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
image.widthAnchor.constraint(equalToConstant: 64),
|
||||||
|
image.heightAnchor.constraint(equalToConstant: 64),
|
||||||
|
|
||||||
|
stack.leadingAnchor.constraint(equalToSystemSpacingAfter: view.safeAreaLayoutGuide.leadingAnchor, multiplier: 1),
|
||||||
|
view.safeAreaLayoutGuide.trailingAnchor.constraint(equalToSystemSpacingAfter: stack.trailingAnchor, multiplier: 1),
|
||||||
|
stack.centerYAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerYAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
default:
|
default:
|
||||||
await super.handleLoadAllError(error)
|
await super.handleLoadAllError(error)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
guard code == 422 || code == 401 else {
|
|
||||||
await super.handleLoadAllError(error)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
collectionView.isHidden = true
|
|
||||||
view.backgroundColor = .systemBackground
|
|
||||||
|
|
||||||
let image = UIImageView(image: UIImage(systemName: "lock.fill"))
|
|
||||||
image.tintColor = .secondaryLabel
|
|
||||||
image.contentMode = .scaleAspectFit
|
|
||||||
|
|
||||||
let title = UILabel()
|
|
||||||
title.textColor = .secondaryLabel
|
|
||||||
title.font = .preferredFont(forTextStyle: .title1).withTraits(.traitBold)!
|
|
||||||
title.adjustsFontForContentSizeCategory = true
|
|
||||||
title.numberOfLines = 0
|
|
||||||
title.textAlignment = .center
|
|
||||||
title.text = "This instance requires an account to view."
|
|
||||||
|
|
||||||
let stack = UIStackView(arrangedSubviews: [
|
|
||||||
image,
|
|
||||||
title,
|
|
||||||
])
|
|
||||||
stack.axis = .vertical
|
|
||||||
stack.alignment = .center
|
|
||||||
stack.spacing = 8
|
|
||||||
stack.isAccessibilityElement = true
|
|
||||||
stack.accessibilityLabel = title.text!
|
|
||||||
|
|
||||||
stack.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
view.addSubview(stack)
|
|
||||||
NSLayoutConstraint.activate([
|
|
||||||
image.widthAnchor.constraint(equalToConstant: 64),
|
|
||||||
image.heightAnchor.constraint(equalToConstant: 64),
|
|
||||||
|
|
||||||
stack.leadingAnchor.constraint(equalToSystemSpacingAfter: view.safeAreaLayoutGuide.leadingAnchor, multiplier: 1),
|
|
||||||
view.safeAreaLayoutGuide.trailingAnchor.constraint(equalToSystemSpacingAfter: stack.trailingAnchor, multiplier: 1),
|
|
||||||
stack.centerYAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerYAnchor),
|
|
||||||
])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Interaction
|
// MARK: Interaction
|
||||||
|
|
|
@ -1,50 +0,0 @@
|
||||||
//
|
|
||||||
// CopyableLable.swift
|
|
||||||
// Tusker
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 2/4/23.
|
|
||||||
// Copyright © 2023 Shadowfacts. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
|
|
||||||
class CopyableLable: UILabel {
|
|
||||||
|
|
||||||
private var _editMenuInteraction: Any!
|
|
||||||
@available(iOS 16.0, *)
|
|
||||||
private var editMenuInteraction: UIEditMenuInteraction {
|
|
||||||
get { _editMenuInteraction as! UIEditMenuInteraction }
|
|
||||||
set { _editMenuInteraction = newValue }
|
|
||||||
}
|
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
|
||||||
super.init(frame: frame)
|
|
||||||
commonInit()
|
|
||||||
}
|
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
|
||||||
super.init(coder: coder)
|
|
||||||
commonInit()
|
|
||||||
}
|
|
||||||
|
|
||||||
private func commonInit() {
|
|
||||||
if #available(iOS 16.0, *) {
|
|
||||||
editMenuInteraction = UIEditMenuInteraction(delegate: nil)
|
|
||||||
addInteraction(editMenuInteraction)
|
|
||||||
isUserInteractionEnabled = true
|
|
||||||
addGestureRecognizer(UILongPressGestureRecognizer(target: self, action: #selector(longPressed)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override func copy(_ sender: Any?) {
|
|
||||||
UIPasteboard.general.string = text
|
|
||||||
}
|
|
||||||
|
|
||||||
@available(iOS 16.0, *)
|
|
||||||
@objc private func longPressed(_ recognizer: UILongPressGestureRecognizer) {
|
|
||||||
if recognizer.state == .began {
|
|
||||||
editMenuInteraction.presentEditMenu(with: UIEditMenuConfiguration(identifier: nil, sourcePoint: CGPoint(x: bounds.midX, y: bounds.midY)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -222,31 +222,6 @@ class ActionNotificationGroupTableViewCell: UITableViewCell {
|
||||||
updateTimestampWorkItem = nil
|
updateTimestampWorkItem = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Accessibility
|
|
||||||
|
|
||||||
override var accessibilityLabel: String? {
|
|
||||||
get {
|
|
||||||
let first = group.notifications.first!
|
|
||||||
var str = ""
|
|
||||||
switch group.kind {
|
|
||||||
case .favourite:
|
|
||||||
str += "Favorited by "
|
|
||||||
case .reblog:
|
|
||||||
str += "Reblogged by "
|
|
||||||
default:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
str += first.account.displayNameWithoutCustomEmoji
|
|
||||||
if group.notifications.count > 1 {
|
|
||||||
str += " and \(group.notifications.count - 1) more"
|
|
||||||
}
|
|
||||||
str += ", \(first.createdAt.formatted(.relative(presentation: .numeric))), "
|
|
||||||
str += statusContentLabel.text ?? ""
|
|
||||||
return str
|
|
||||||
}
|
|
||||||
set {}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ActionNotificationGroupTableViewCell: SelectableTableViewCell {
|
extension ActionNotificationGroupTableViewCell: SelectableTableViewCell {
|
||||||
|
|
|
@ -186,22 +186,6 @@ class FollowNotificationGroupTableViewCell: UITableViewCell {
|
||||||
updateTimestampWorkItem = nil
|
updateTimestampWorkItem = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Accessibility
|
|
||||||
|
|
||||||
override var accessibilityLabel: String? {
|
|
||||||
get {
|
|
||||||
let first = group.notifications.first!
|
|
||||||
var str = "Followed by "
|
|
||||||
str += first.account.displayNameWithoutCustomEmoji
|
|
||||||
if group.notifications.count > 1 {
|
|
||||||
str += " and \(group.notifications.count - 1) more"
|
|
||||||
}
|
|
||||||
str += ", \(first.createdAt.formatted(.relative(presentation: .numeric)))"
|
|
||||||
return str
|
|
||||||
}
|
|
||||||
set {}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension FollowNotificationGroupTableViewCell: SelectableTableViewCell {
|
extension FollowNotificationGroupTableViewCell: SelectableTableViewCell {
|
||||||
|
|
|
@ -135,29 +135,6 @@ class FollowRequestNotificationTableViewCell: UITableViewCell {
|
||||||
self.stackView.addArrangedSubview(label)
|
self.stackView.addArrangedSubview(label)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Accessibility
|
|
||||||
|
|
||||||
override var accessibilityLabel: String? {
|
|
||||||
get {
|
|
||||||
guard let notification else { return nil }
|
|
||||||
var str = "Follow requested by "
|
|
||||||
str += notification.account.displayNameWithoutCustomEmoji
|
|
||||||
str += ", \(notification.createdAt.formatted(.relative(presentation: .numeric)))"
|
|
||||||
return str
|
|
||||||
}
|
|
||||||
set {}
|
|
||||||
}
|
|
||||||
|
|
||||||
override var accessibilityCustomActions: [UIAccessibilityCustomAction]? {
|
|
||||||
get {
|
|
||||||
return [
|
|
||||||
UIAccessibilityCustomAction(name: "Accept Request", target: self, selector: #selector(acceptButtonPressed)),
|
|
||||||
UIAccessibilityCustomAction(name: "Reject Request", target: self, selector: #selector(acceptButtonPressed)),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
set {}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Interaction
|
// MARK: - Interaction
|
||||||
|
|
||||||
@IBAction func rejectButtonPressed() {
|
@IBAction func rejectButtonPressed() {
|
||||||
|
|
|
@ -55,7 +55,7 @@
|
||||||
<rect key="frame" x="0.0" y="0.0" width="115" height="26.5"/>
|
<rect key="frame" x="0.0" y="0.0" width="115" height="26.5"/>
|
||||||
<accessibility key="accessibilityConfiguration" label="Accept Request"/>
|
<accessibility key="accessibilityConfiguration" label="Accept Request"/>
|
||||||
<state key="normal" title=" Accept" image="checkmark.circle.fill" catalog="system">
|
<state key="normal" title=" Accept" image="checkmark.circle.fill" catalog="system">
|
||||||
<color key="titleColor" systemColor="tintColor"/>
|
<color key="titleColor" systemColor="systemBlueColor"/>
|
||||||
</state>
|
</state>
|
||||||
<connections>
|
<connections>
|
||||||
<action selector="acceptButtonPressed" destination="Pcu-ap-Xqf" eventType="touchUpInside" id="hGw-3d-RNi"/>
|
<action selector="acceptButtonPressed" destination="Pcu-ap-Xqf" eventType="touchUpInside" id="hGw-3d-RNi"/>
|
||||||
|
@ -64,7 +64,7 @@
|
||||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" pointerInteraction="YES" translatesAutoresizingMaskIntoConstraints="NO" id="7MW-rY-m5l">
|
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" pointerInteraction="YES" translatesAutoresizingMaskIntoConstraints="NO" id="7MW-rY-m5l">
|
||||||
<rect key="frame" x="115" y="0.0" width="115" height="26.5"/>
|
<rect key="frame" x="115" y="0.0" width="115" height="26.5"/>
|
||||||
<state key="normal" title=" Reject" image="xmark.circle.fill" catalog="system">
|
<state key="normal" title=" Reject" image="xmark.circle.fill" catalog="system">
|
||||||
<color key="titleColor" systemColor="tintColor"/>
|
<color key="titleColor" systemColor="systemBlueColor"/>
|
||||||
</state>
|
</state>
|
||||||
<connections>
|
<connections>
|
||||||
<action selector="rejectButtonPressed" destination="Pcu-ap-Xqf" eventType="touchUpInside" id="EP6-Bg-3nC"/>
|
<action selector="rejectButtonPressed" destination="Pcu-ap-Xqf" eventType="touchUpInside" id="EP6-Bg-3nC"/>
|
||||||
|
@ -111,7 +111,7 @@
|
||||||
<systemColor name="secondaryLabelColor">
|
<systemColor name="secondaryLabelColor">
|
||||||
<color red="0.23529411764705882" green="0.23529411764705882" blue="0.2627450980392157" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
<color red="0.23529411764705882" green="0.23529411764705882" blue="0.2627450980392157" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
</systemColor>
|
</systemColor>
|
||||||
<systemColor name="tintColor">
|
<systemColor name="systemBlueColor">
|
||||||
<color red="0.0" green="0.47843137254901963" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
<color red="0.0" green="0.47843137254901963" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
</systemColor>
|
</systemColor>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -93,25 +93,6 @@ class PollFinishedTableViewCell: UITableViewCell {
|
||||||
updateTimestampWorkItem = nil
|
updateTimestampWorkItem = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Accessibility
|
|
||||||
|
|
||||||
override var accessibilityLabel: String? {
|
|
||||||
get {
|
|
||||||
guard let notification else { return nil }
|
|
||||||
var str = "Poll from "
|
|
||||||
str += notification.account.displayNameWithoutCustomEmoji
|
|
||||||
str += " finished "
|
|
||||||
str += notification.createdAt.formatted(.relative(presentation: .numeric))
|
|
||||||
if let poll = notification.status?.poll,
|
|
||||||
poll.options.contains(where: { ($0.votesCount ?? 0) > 0 }) {
|
|
||||||
let winner = poll.options.max(by: { ($0.votesCount ?? 0) < ($1.votesCount ?? 0) })!
|
|
||||||
str += ", winning option: \(winner.title)"
|
|
||||||
}
|
|
||||||
return str
|
|
||||||
}
|
|
||||||
set {}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension PollFinishedTableViewCell: SelectableTableViewCell {
|
extension PollFinishedTableViewCell: SelectableTableViewCell {
|
||||||
|
|
|
@ -85,22 +85,6 @@ class StatusUpdatedNotificationTableViewCell: UITableViewCell {
|
||||||
updateTimestampWorkItem = nil
|
updateTimestampWorkItem = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Accessibility
|
|
||||||
|
|
||||||
override var accessibilityLabel: String? {
|
|
||||||
get {
|
|
||||||
guard let notification else { return nil }
|
|
||||||
var str = "Post from "
|
|
||||||
str += notification.account.displayNameWithoutCustomEmoji
|
|
||||||
str += " edited "
|
|
||||||
str += notification.createdAt.formatted(.relative(presentation: .numeric))
|
|
||||||
str += ", "
|
|
||||||
str += contentLabel.text ?? ""
|
|
||||||
return str
|
|
||||||
}
|
|
||||||
set {}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension StatusUpdatedNotificationTableViewCell: SelectableTableViewCell {
|
extension StatusUpdatedNotificationTableViewCell: SelectableTableViewCell {
|
||||||
|
|
|
@ -111,7 +111,7 @@
|
||||||
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="jwU-EH-hmC">
|
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="jwU-EH-hmC">
|
||||||
<rect key="frame" x="144" y="235" width="103.5" height="23"/>
|
<rect key="frame" x="144" y="235" width="103.5" height="23"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="751" text="@username" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="1C3-Pd-QiL" customClass="CopyableLable" customModule="Tusker" customModuleProvider="target">
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="751" text="@username" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="1C3-Pd-QiL">
|
||||||
<rect key="frame" x="0.0" y="2.5" width="81" height="18"/>
|
<rect key="frame" x="0.0" y="2.5" width="81" height="18"/>
|
||||||
<fontDescription key="fontDescription" type="system" weight="light" pointSize="15"/>
|
<fontDescription key="fontDescription" type="system" weight="light" pointSize="15"/>
|
||||||
<color key="textColor" systemColor="secondaryLabelColor"/>
|
<color key="textColor" systemColor="secondaryLabelColor"/>
|
||||||
|
|
|
@ -762,10 +762,7 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti
|
||||||
return UIContextMenuConfiguration {
|
return UIContextMenuConfiguration {
|
||||||
ConversationViewController(for: self.statusID, state: self.statusState.copy(), mastodonController: self.mastodonController)
|
ConversationViewController(for: self.statusID, state: self.statusState.copy(), mastodonController: self.mastodonController)
|
||||||
} actionProvider: { _ in
|
} actionProvider: { _ in
|
||||||
guard let delegate = self.delegate else {
|
UIMenu(children: self.delegate!.actionsForStatus(status, source: .view(self)))
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return UIMenu(children: delegate.actionsForStatus(status, source: .view(self)))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -56,7 +56,37 @@ extension ToastConfiguration {
|
||||||
viewController.present(reporter, animated: true)
|
viewController.present(reporter, animated: true)
|
||||||
}
|
}
|
||||||
// TODO: this is a bizarre place to do this, but code path covers basically all errors
|
// TODO: this is a bizarre place to do this, but code path covers basically all errors
|
||||||
captureError(error, title: title)
|
switch error.type {
|
||||||
|
case .invalidRequest, .invalidResponse, .invalidModel(_), .mastodonError(_, _):
|
||||||
|
let event = Event(error: error)
|
||||||
|
event.message = SentryMessage(formatted: "\(title): \(error)")
|
||||||
|
event.tags = [
|
||||||
|
"request_method": error.requestMethod.name,
|
||||||
|
"request_endpoint": error.requestEndpoint.description,
|
||||||
|
]
|
||||||
|
switch error.type {
|
||||||
|
case .invalidRequest:
|
||||||
|
event.tags!["error_type"] = "invalid_request"
|
||||||
|
case .invalidResponse:
|
||||||
|
event.tags!["error_type"] = "invalid_response"
|
||||||
|
case .invalidModel(let error):
|
||||||
|
event.tags!["error_type"] = "invalid_model"
|
||||||
|
event.extra = [
|
||||||
|
"underlying_error": String(describing: error)
|
||||||
|
]
|
||||||
|
case .mastodonError(let code, let error):
|
||||||
|
event.tags!["error_type"] = "mastodon_error"
|
||||||
|
event.tags!["response_code"] = "\(code)"
|
||||||
|
event.extra = [
|
||||||
|
"underlying_error": String(describing: error)
|
||||||
|
]
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
SentrySDK.capture(event: event)
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
self.subtitle = error.localizedDescription
|
self.subtitle = error.localizedDescription
|
||||||
self.systemImageName = "exclamationmark.triangle"
|
self.systemImageName = "exclamationmark.triangle"
|
||||||
|
@ -86,39 +116,3 @@ fileprivate extension Pachyderm.Client.Error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func captureError(_ error: Client.Error, title: String) {
|
|
||||||
let event = Event(error: error)
|
|
||||||
event.message = SentryMessage(formatted: "\(title): \(error)")
|
|
||||||
event.tags = [
|
|
||||||
"request_method": error.requestMethod.name,
|
|
||||||
"request_endpoint": error.requestEndpoint.description,
|
|
||||||
]
|
|
||||||
switch error.type {
|
|
||||||
case .invalidRequest:
|
|
||||||
event.tags!["error_type"] = "invalid_request"
|
|
||||||
case .invalidResponse:
|
|
||||||
event.tags!["error_type"] = "invalid_response"
|
|
||||||
case .invalidModel(let error):
|
|
||||||
event.tags!["error_type"] = "invalid_model"
|
|
||||||
event.extra = [
|
|
||||||
"underlying_error": String(describing: error)
|
|
||||||
]
|
|
||||||
case .mastodonError(let code, let error):
|
|
||||||
event.tags!["error_type"] = "mastodon_error"
|
|
||||||
event.tags!["response_code"] = "\(code)"
|
|
||||||
event.extra = [
|
|
||||||
"underlying_error": String(describing: error)
|
|
||||||
]
|
|
||||||
case .unexpectedStatus(let code):
|
|
||||||
event.tags!["error_type"] = "unexpected_status"
|
|
||||||
event.tags!["response_code"] = "\(code)"
|
|
||||||
default:
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if let code = event.tags!["response_code"],
|
|
||||||
code == "401" || code == "403" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
SentrySDK.capture(event: event)
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in New Issue