Compare commits

...

24 Commits

Author SHA1 Message Date
Shadowfacts 4731801893 Bump build number and update changelog 2023-02-12 10:22:33 -05:00
Shadowfacts 4293b51c31 Add extended suggested profiles screen
Closes #355
2023-02-11 19:05:12 -05:00
Shadowfacts ecadb83c6d Add infinite scrolling to trending statuses
See #355
2023-02-11 18:47:39 -05:00
Shadowfacts 205bdffebd Add loading indicator to Trends screen 2023-02-11 18:32:37 -05:00
Shadowfacts ae7ca9c91c Fix wrong cells on trending links screen being selectable 2023-02-11 18:29:33 -05:00
Shadowfacts 841119949b Add infinite scrolling to trending hashtags screen
See #355
2023-02-11 18:29:33 -05:00
Shadowfacts b63f663947 Handle errors when loading trending links 2023-02-11 18:13:37 -05:00
Shadowfacts 00a23b525f Add share to trending link actions 2023-02-11 10:21:09 -05:00
Shadowfacts ea85b11945 Use cards for trending links screen, and add pagination
See #355
2023-02-11 10:09:56 -05:00
Shadowfacts d8c7eb5cf5 Add buttons to Explore screen 2023-02-10 18:19:00 -05:00
Shadowfacts 8bc185ecf9 Add jump to present button to timelines 2023-02-07 23:52:23 -05:00
Shadowfacts 1832e64ad7 Remove now-unused hashtag table view cell 2023-02-06 21:47:47 -05:00
Shadowfacts 87bc1f5f75 Rewrite search results VC using UICollectionView 2023-02-06 21:47:47 -05:00
Shadowfacts 6e2f6bb8e9 Apply non-pure black dark mode to Drafts screen 2023-02-06 19:53:15 -05:00
Shadowfacts 74d8adfffe Fix Compose background color not going under nav bar 2023-02-06 19:51:01 -05:00
Shadowfacts 99127b617b Tweak non-pure-black dark mode colors 2023-02-06 18:47:50 -05:00
Shadowfacts 65ea72c07f Don't show pure-black dark mode preference on Mac 2023-02-06 18:45:34 -05:00
Shadowfacts 04ca932a01 Mode non-pure-black dark mode stuff to dedicated modifiers 2023-02-06 18:43:00 -05:00
Shadowfacts 4ea2dff8f1 Merge branch 'develop' into non-pure-black-mode 2023-02-06 18:15:23 -05:00
Shadowfacts 9f0176350c Cleanup TuskerNavigationDelegate 2023-02-06 18:10:38 -05:00
Shadowfacts dac1e1fe3f Fix icon in suggested profile reason popover not adjusting to dark mode 2023-02-05 19:56:37 -05:00
Shadowfacts 471d3459a6 Apply non-pure-black dark mode to preferences screen 2023-02-02 23:29:44 -05:00
Shadowfacts 512eec09a8 Merge branch 'develop' into non-pure-black-mode 2023-02-02 23:14:27 -05:00
Shadowfacts 20c4c4bb2f Start adding non-pure-black dark mode 2023-02-02 23:02:11 -05:00
100 changed files with 2347 additions and 514 deletions

View File

@ -1,5 +1,17 @@
# Changelog # Changelog
## 2023.4 (73)
Features/Improvements:
- Add preference for non-pure-black dark mode
- Add Jump to Present button to timelines
- Improve status collapse animation in search results screen
- Add more trending links/hashtags/profiles buttons to Trends screen
- Add infinite scrolling to trending links/hashtags screens
- Add Share action to trending link context menu
Bugfixes:
- Fix icon in suggested profile popover not adjusting to dark mode
## 2023.4 (72) ## 2023.4 (72)
Features/Improvements: Features/Improvements:
- Consolidate Trends into a single screen - Consolidate Trends into a single screen

View File

@ -417,32 +417,46 @@ public class Client {
} }
// MARK: - Instance // MARK: - Instance
public static func getTrendingHashtags(limit: Int? = nil) -> Request<[Hashtag]> { public static func getTrendingHashtagsDeprecated(limit: Int? = nil, offset: Int? = nil) -> Request<[Hashtag]> {
let parameters: [Parameter] var parameters: [Parameter] = []
if let limit = limit { if let limit {
parameters = ["limit" => limit] parameters.append("limit" => limit)
} else { }
parameters = [] if let offset {
parameters.append("offset" => offset)
} }
return Request<[Hashtag]>(method: .get, path: "/api/v1/trends", queryParameters: parameters) return Request<[Hashtag]>(method: .get, path: "/api/v1/trends", queryParameters: parameters)
} }
public static func getTrendingStatuses(limit: Int? = nil) -> Request<[Status]> { public static func getTrendingHashtags(limit: Int? = nil, offset: Int? = nil) -> Request<[Hashtag]> {
let parameters: [Parameter] var parameters: [Parameter] = []
if let limit = limit { if let limit {
parameters = ["limit" => limit] parameters.append("limit" => limit)
} else { }
parameters = [] if let offset {
parameters.append("offset" => offset)
}
return Request<[Hashtag]>(method: .get, path: "/api/v1/trends/tags", queryParameters: parameters)
}
public static func getTrendingStatuses(limit: Int? = nil, offset: Int? = nil) -> Request<[Status]> {
var parameters: [Parameter] = []
if let limit {
parameters.append("limit" => limit)
}
if let offset {
parameters.append("offset" => offset)
} }
return Request(method: .get, path: "/api/v1/trends/statuses", queryParameters: parameters) return Request(method: .get, path: "/api/v1/trends/statuses", queryParameters: parameters)
} }
public static func getTrendingLinks(limit: Int? = nil) -> Request<[Card]> { public static func getTrendingLinks(limit: Int? = nil, offset: Int? = nil) -> Request<[Card]> {
let parameters: [Parameter] var parameters: [Parameter] = []
if let limit = limit { if let limit {
parameters = ["limit" => limit] parameters.append("limit" => limit)
} else { }
parameters = [] if let offset {
parameters.append("offset" => offset)
} }
return Request(method: .get, path: "/api/v1/trends/links", queryParameters: parameters) return Request(method: .get, path: "/api/v1/trends/links", queryParameters: parameters)
} }

View File

@ -171,7 +171,7 @@ extension Account: CustomDebugStringConvertible {
} }
extension Account { extension Account {
public struct Field: Codable { public struct Field: Codable, Equatable {
public let name: String public let name: String
public let value: String public let value: String
public let verifiedAt: Date? public let verifiedAt: Date?

View File

@ -41,8 +41,6 @@
D6114E1127F899B30080E273 /* TrendingLinksViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6114E1027F899B30080E273 /* TrendingLinksViewController.swift */; }; D6114E1127F899B30080E273 /* TrendingLinksViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6114E1027F899B30080E273 /* TrendingLinksViewController.swift */; };
D6114E1327F89B440080E273 /* TrendingLinkTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6114E1227F89B440080E273 /* TrendingLinkTableViewCell.swift */; }; D6114E1327F89B440080E273 /* TrendingLinkTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6114E1227F89B440080E273 /* TrendingLinkTableViewCell.swift */; };
D6114E1727F8BB210080E273 /* VersionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6114E1627F8BB210080E273 /* VersionTests.swift */; }; D6114E1727F8BB210080E273 /* VersionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6114E1627F8BB210080E273 /* VersionTests.swift */; };
D611C2CF232DC61100C86A49 /* HashtagTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D611C2CD232DC61100C86A49 /* HashtagTableViewCell.swift */; };
D611C2D0232DC61100C86A49 /* HashtagTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D611C2CE232DC61100C86A49 /* HashtagTableViewCell.xib */; };
D61A45E628DC0F2F002BE511 /* ConfirmLoadMoreCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61A45E528DC0F2F002BE511 /* ConfirmLoadMoreCollectionViewCell.swift */; }; D61A45E628DC0F2F002BE511 /* ConfirmLoadMoreCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61A45E528DC0F2F002BE511 /* ConfirmLoadMoreCollectionViewCell.swift */; };
D61A45E828DF477D002BE511 /* LoadingCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61A45E728DF477D002BE511 /* LoadingCollectionViewCell.swift */; }; D61A45E828DF477D002BE511 /* LoadingCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61A45E728DF477D002BE511 /* LoadingCollectionViewCell.swift */; };
D61A45EA28DF51EE002BE511 /* TimelineLikeCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61A45E928DF51EE002BE511 /* TimelineLikeCollectionViewController.swift */; }; D61A45EA28DF51EE002BE511 /* TimelineLikeCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61A45E928DF51EE002BE511 /* TimelineLikeCollectionViewController.swift */; };
@ -202,6 +200,7 @@
D681E4D7246E32290053414F /* StatusActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D681E4D6246E32290053414F /* StatusActivityItemSource.swift */; }; D681E4D7246E32290053414F /* StatusActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D681E4D6246E32290053414F /* StatusActivityItemSource.swift */; };
D681E4D9246E346E0053414F /* AccountActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D681E4D8246E346E0053414F /* AccountActivityItemSource.swift */; }; D681E4D9246E346E0053414F /* AccountActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D681E4D8246E346E0053414F /* AccountActivityItemSource.swift */; };
D68232F72464F4FD00325FB8 /* ComposeDrawingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68232F62464F4FD00325FB8 /* ComposeDrawingViewController.swift */; }; D68232F72464F4FD00325FB8 /* ComposeDrawingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68232F62464F4FD00325FB8 /* ComposeDrawingViewController.swift */; };
D68329EF299540050026EB24 /* MoreTrendsFooterCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68329EE299540050026EB24 /* MoreTrendsFooterCollectionViewCell.swift */; };
D686BBE324FBF8110068E6AA /* WrappedProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D686BBE224FBF8110068E6AA /* WrappedProgressView.swift */; }; D686BBE324FBF8110068E6AA /* WrappedProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D686BBE224FBF8110068E6AA /* WrappedProgressView.swift */; };
D6895DC228D65274006341DA /* CustomAlertController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6895DC128D65274006341DA /* CustomAlertController.swift */; }; D6895DC228D65274006341DA /* CustomAlertController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6895DC128D65274006341DA /* CustomAlertController.swift */; };
D6895DC428D65342006341DA /* ConfirmReblogStatusPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6895DC328D65342006341DA /* ConfirmReblogStatusPreviewView.swift */; }; D6895DC428D65342006341DA /* ConfirmReblogStatusPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6895DC328D65342006341DA /* ConfirmReblogStatusPreviewView.swift */; };
@ -299,6 +298,8 @@
D6C3F4F7298ED7F70009FCFF /* FavoritesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C3F4F6298ED7F70009FCFF /* FavoritesViewController.swift */; }; D6C3F4F7298ED7F70009FCFF /* FavoritesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C3F4F6298ED7F70009FCFF /* FavoritesViewController.swift */; };
D6C3F4F9298EDBF20009FCFF /* ConversationTree.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C3F4F8298EDBF20009FCFF /* ConversationTree.swift */; }; D6C3F4F9298EDBF20009FCFF /* ConversationTree.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C3F4F8298EDBF20009FCFF /* ConversationTree.swift */; };
D6C3F4FB299035650009FCFF /* TrendsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C3F4FA299035650009FCFF /* TrendsViewController.swift */; }; D6C3F4FB299035650009FCFF /* TrendsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C3F4FA299035650009FCFF /* TrendsViewController.swift */; };
D6C3F5172991C1A00009FCFF /* View+AppListStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C3F5162991C1A00009FCFF /* View+AppListStyle.swift */; };
D6C3F5192991F5D60009FCFF /* TimelineJumpButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C3F5182991F5D60009FCFF /* TimelineJumpButton.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,6 +326,8 @@
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 */; };
D6D94955298963A900C59229 /* Colors.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D94954298963A900C59229 /* Colors.swift */; };
D6D9498D298CBB4000C59229 /* TrendingStatusCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D9498C298CBB4000C59229 /* TrendingStatusCollectionViewCell.swift */; };
D6D9498F298EB79400C59229 /* CopyableLable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D9498E298EB79400C59229 /* CopyableLable.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 */; };
@ -332,6 +335,7 @@
D6DD353F22F502EC00A9563A /* Preferences+Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD353E22F502EC00A9563A /* Preferences+Notification.swift */; }; D6DD353F22F502EC00A9563A /* Preferences+Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD353E22F502EC00A9563A /* Preferences+Notification.swift */; };
D6DD8FFD298495A8002AD3FD /* LogoutService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD8FFC298495A8002AD3FD /* LogoutService.swift */; }; D6DD8FFD298495A8002AD3FD /* LogoutService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD8FFC298495A8002AD3FD /* LogoutService.swift */; };
D6DD8FFF2984D327002AD3FD /* BookmarksViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD8FFE2984D327002AD3FD /* BookmarksViewController.swift */; }; D6DD8FFF2984D327002AD3FD /* BookmarksViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD8FFE2984D327002AD3FD /* BookmarksViewController.swift */; };
D6DD996B2998611A0015C962 /* SuggestedProfilesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD996A2998611A0015C962 /* SuggestedProfilesViewController.swift */; };
D6DF95C12533F5DE0027A9B6 /* RelationshipMO.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DF95C02533F5DE0027A9B6 /* RelationshipMO.swift */; }; D6DF95C12533F5DE0027A9B6 /* RelationshipMO.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DF95C02533F5DE0027A9B6 /* RelationshipMO.swift */; };
D6DFC69E242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DFC69D242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift */; }; D6DFC69E242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DFC69D242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift */; };
D6DFC6A0242C4CCC00ACC392 /* Weak.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DFC69F242C4CCC00ACC392 /* Weak.swift */; }; D6DFC6A0242C4CCC00ACC392 /* Weak.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DFC69F242C4CCC00ACC392 /* Weak.swift */; };
@ -451,8 +455,6 @@
D6114E1027F899B30080E273 /* TrendingLinksViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingLinksViewController.swift; sourceTree = "<group>"; }; D6114E1027F899B30080E273 /* TrendingLinksViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingLinksViewController.swift; sourceTree = "<group>"; };
D6114E1227F89B440080E273 /* TrendingLinkTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingLinkTableViewCell.swift; sourceTree = "<group>"; }; D6114E1227F89B440080E273 /* TrendingLinkTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingLinkTableViewCell.swift; sourceTree = "<group>"; };
D6114E1627F8BB210080E273 /* VersionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionTests.swift; sourceTree = "<group>"; }; D6114E1627F8BB210080E273 /* VersionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionTests.swift; sourceTree = "<group>"; };
D611C2CD232DC61100C86A49 /* HashtagTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagTableViewCell.swift; sourceTree = "<group>"; };
D611C2CE232DC61100C86A49 /* HashtagTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = HashtagTableViewCell.xib; sourceTree = "<group>"; };
D61A45E528DC0F2F002BE511 /* ConfirmLoadMoreCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmLoadMoreCollectionViewCell.swift; sourceTree = "<group>"; }; D61A45E528DC0F2F002BE511 /* ConfirmLoadMoreCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmLoadMoreCollectionViewCell.swift; sourceTree = "<group>"; };
D61A45E728DF477D002BE511 /* LoadingCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingCollectionViewCell.swift; sourceTree = "<group>"; }; D61A45E728DF477D002BE511 /* LoadingCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingCollectionViewCell.swift; sourceTree = "<group>"; };
D61A45E928DF51EE002BE511 /* TimelineLikeCollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineLikeCollectionViewController.swift; sourceTree = "<group>"; }; D61A45E928DF51EE002BE511 /* TimelineLikeCollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineLikeCollectionViewController.swift; sourceTree = "<group>"; };
@ -612,6 +614,7 @@
D681E4D6246E32290053414F /* StatusActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusActivityItemSource.swift; sourceTree = "<group>"; }; D681E4D6246E32290053414F /* StatusActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusActivityItemSource.swift; sourceTree = "<group>"; };
D681E4D8246E346E0053414F /* AccountActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountActivityItemSource.swift; sourceTree = "<group>"; }; D681E4D8246E346E0053414F /* AccountActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountActivityItemSource.swift; sourceTree = "<group>"; };
D68232F62464F4FD00325FB8 /* ComposeDrawingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeDrawingViewController.swift; sourceTree = "<group>"; }; D68232F62464F4FD00325FB8 /* ComposeDrawingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeDrawingViewController.swift; sourceTree = "<group>"; };
D68329EE299540050026EB24 /* MoreTrendsFooterCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoreTrendsFooterCollectionViewCell.swift; sourceTree = "<group>"; };
D686BBE224FBF8110068E6AA /* WrappedProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WrappedProgressView.swift; sourceTree = "<group>"; }; D686BBE224FBF8110068E6AA /* WrappedProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WrappedProgressView.swift; sourceTree = "<group>"; };
D6895DC128D65274006341DA /* CustomAlertController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomAlertController.swift; sourceTree = "<group>"; }; D6895DC128D65274006341DA /* CustomAlertController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomAlertController.swift; sourceTree = "<group>"; };
D6895DC328D65342006341DA /* ConfirmReblogStatusPreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmReblogStatusPreviewView.swift; sourceTree = "<group>"; }; D6895DC328D65342006341DA /* ConfirmReblogStatusPreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmReblogStatusPreviewView.swift; sourceTree = "<group>"; };
@ -710,6 +713,8 @@
D6C3F4F6298ED7F70009FCFF /* FavoritesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesViewController.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>"; }; 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>"; }; D6C3F4FA299035650009FCFF /* TrendsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendsViewController.swift; sourceTree = "<group>"; };
D6C3F5162991C1A00009FCFF /* View+AppListStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+AppListStyle.swift"; sourceTree = "<group>"; };
D6C3F5182991F5D60009FCFF /* TimelineJumpButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineJumpButton.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,6 +748,8 @@
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>"; };
D6D94954298963A900C59229 /* Colors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Colors.swift; sourceTree = "<group>"; };
D6D9498C298CBB4000C59229 /* TrendingStatusCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingStatusCollectionViewCell.swift; sourceTree = "<group>"; };
D6D9498E298EB79400C59229 /* CopyableLable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyableLable.swift; 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>"; };
@ -750,6 +757,7 @@
D6DD353E22F502EC00A9563A /* Preferences+Notification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Preferences+Notification.swift"; sourceTree = "<group>"; }; D6DD353E22F502EC00A9563A /* Preferences+Notification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Preferences+Notification.swift"; sourceTree = "<group>"; };
D6DD8FFC298495A8002AD3FD /* LogoutService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogoutService.swift; sourceTree = "<group>"; }; D6DD8FFC298495A8002AD3FD /* LogoutService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogoutService.swift; sourceTree = "<group>"; };
D6DD8FFE2984D327002AD3FD /* BookmarksViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksViewController.swift; sourceTree = "<group>"; }; D6DD8FFE2984D327002AD3FD /* BookmarksViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksViewController.swift; sourceTree = "<group>"; };
D6DD996A2998611A0015C962 /* SuggestedProfilesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestedProfilesViewController.swift; sourceTree = "<group>"; };
D6DF95C02533F5DE0027A9B6 /* RelationshipMO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelationshipMO.swift; sourceTree = "<group>"; }; D6DF95C02533F5DE0027A9B6 /* RelationshipMO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelationshipMO.swift; sourceTree = "<group>"; };
D6DFC69D242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackpadScrollGestureRecognizer.swift; sourceTree = "<group>"; }; D6DFC69D242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackpadScrollGestureRecognizer.swift; sourceTree = "<group>"; };
D6DFC69F242C4CCC00ACC392 /* Weak.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Weak.swift; sourceTree = "<group>"; }; D6DFC69F242C4CCC00ACC392 /* Weak.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Weak.swift; sourceTree = "<group>"; };
@ -853,8 +861,6 @@
D611C2CC232DC5FC00C86A49 /* Hashtag Cell */ = { D611C2CC232DC5FC00C86A49 /* Hashtag Cell */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
D611C2CD232DC61100C86A49 /* HashtagTableViewCell.swift */,
D611C2CE232DC61100C86A49 /* HashtagTableViewCell.xib */,
D6E77D08286D25FA00D8B732 /* TrendingHashtagCollectionViewCell.swift */, D6E77D08286D25FA00D8B732 /* TrendingHashtagCollectionViewCell.swift */,
); );
path = "Hashtag Cell"; path = "Hashtag Cell";
@ -943,6 +949,8 @@
D693A72D25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift */, D693A72D25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift */,
D693A72E25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.xib */, D693A72E25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.xib */,
D6C3F4FA299035650009FCFF /* TrendsViewController.swift */, D6C3F4FA299035650009FCFF /* TrendsViewController.swift */,
D68329EE299540050026EB24 /* MoreTrendsFooterCollectionViewCell.swift */,
D6DD996A2998611A0015C962 /* SuggestedProfilesViewController.swift */,
); );
path = Explore; path = Explore;
sourceTree = "<group>"; sourceTree = "<group>";
@ -1050,6 +1058,7 @@
D66C900A28DAB7FD00217BF2 /* TimelineViewController.swift */, D66C900A28DAB7FD00217BF2 /* TimelineViewController.swift */,
D6ADB6E728E8C878009924AB /* PublicTimelineDescriptionCollectionViewCell.swift */, D6ADB6E728E8C878009924AB /* PublicTimelineDescriptionCollectionViewCell.swift */,
D6D12B2E2925D66500D528E1 /* TimelineGapCollectionViewCell.swift */, D6D12B2E2925D66500D528E1 /* TimelineGapCollectionViewCell.swift */,
D6C3F5182991F5D60009FCFF /* TimelineJumpButton.swift */,
); );
path = Timeline; path = Timeline;
sourceTree = "<group>"; sourceTree = "<group>";
@ -1181,6 +1190,7 @@
D6ADB6E928E91C30009924AB /* TimelineStatusCollectionViewCell.swift */, D6ADB6E928E91C30009924AB /* TimelineStatusCollectionViewCell.swift */,
D601FA60297B539E00A8E8B5 /* ConversationMainStatusCollectionViewCell.swift */, D601FA60297B539E00A8E8B5 /* ConversationMainStatusCollectionViewCell.swift */,
D6ADB6EB28EA73CB009924AB /* StatusContentContainer.swift */, D6ADB6EB28EA73CB009924AB /* StatusContentContainer.swift */,
D6D9498C298CBB4000C59229 /* TrendingStatusCollectionViewCell.swift */,
); );
path = Status; path = Status;
sourceTree = "<group>"; sourceTree = "<group>";
@ -1274,6 +1284,7 @@
D6DD353E22F502EC00A9563A /* Preferences+Notification.swift */, D6DD353E22F502EC00A9563A /* Preferences+Notification.swift */,
D6BC9DB4232D4CE3002CA326 /* NotificationsMode.swift */, D6BC9DB4232D4CE3002CA326 /* NotificationsMode.swift */,
D61F75872932DB6000C0B37F /* StatusSwipeAction.swift */, D61F75872932DB6000C0B37F /* StatusSwipeAction.swift */,
D6D94954298963A900C59229 /* Colors.swift */,
); );
path = Preferences; path = Preferences;
sourceTree = "<group>"; sourceTree = "<group>";
@ -1303,6 +1314,7 @@
D63CC70F2911F1E4000E19DE /* UIScrollView+Top.swift */, D63CC70F2911F1E4000E19DE /* UIScrollView+Top.swift */,
D61F758F29353B4300C0B37F /* FileManager+Size.swift */, D61F758F29353B4300C0B37F /* FileManager+Size.swift */,
D61F75AC293AF39000C0B37F /* Filter+Helpers.swift */, D61F75AC293AF39000C0B37F /* Filter+Helpers.swift */,
D6C3F5162991C1A00009FCFF /* View+AppListStyle.swift */,
); );
path = Extensions; path = Extensions;
sourceTree = "<group>"; sourceTree = "<group>";
@ -1843,7 +1855,6 @@
isa = PBXResourcesBuildPhase; isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
D611C2D0232DC61100C86A49 /* HashtagTableViewCell.xib in Resources */,
D6E77D0D286E6B7300D8B732 /* TrendingLinkCardCollectionViewCell.xib in Resources */, D6E77D0D286E6B7300D8B732 /* TrendingLinkCardCollectionViewCell.xib in Resources */,
D693A73025CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.xib in Resources */, D693A73025CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.xib in Resources */,
D61AC1D9232EA42D00C54D2D /* InstanceTableViewCell.xib in Resources */, D61AC1D9232EA42D00C54D2D /* InstanceTableViewCell.xib in Resources */,
@ -1960,6 +1971,7 @@
D6093FB725BE0CF3004811E6 /* TrendHistoryView.swift in Sources */, D6093FB725BE0CF3004811E6 /* TrendHistoryView.swift in Sources */,
D6EBF01723C55E0D00AE061B /* UISceneSession+MastodonController.swift in Sources */, D6EBF01723C55E0D00AE061B /* UISceneSession+MastodonController.swift in Sources */,
D61F759229365C6C00C0B37F /* CollectionViewController.swift in Sources */, D61F759229365C6C00C0B37F /* CollectionViewController.swift in Sources */,
D6DD996B2998611A0015C962 /* SuggestedProfilesViewController.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 /* InlineTrendsViewController.swift in Sources */,
@ -1993,7 +2005,6 @@
0411610022B442870030A9B7 /* LoadingLargeImageViewController.swift in Sources */, 0411610022B442870030A9B7 /* LoadingLargeImageViewController.swift in Sources */,
D6A4DCE525537C7A00D9DE31 /* FastSwitchingAccountView.swift in Sources */, D6A4DCE525537C7A00D9DE31 /* FastSwitchingAccountView.swift in Sources */,
D6A4DCCC2553667800D9DE31 /* FastAccountSwitcherViewController.swift in Sources */, D6A4DCCC2553667800D9DE31 /* FastAccountSwitcherViewController.swift in Sources */,
D611C2CF232DC61100C86A49 /* HashtagTableViewCell.swift in Sources */,
D627944F23A9C99800D38C68 /* EditListAccountsViewController.swift in Sources */, D627944F23A9C99800D38C68 /* EditListAccountsViewController.swift in Sources */,
D6945C3423AC6431005C403C /* AddSavedHashtagViewController.swift in Sources */, D6945C3423AC6431005C403C /* AddSavedHashtagViewController.swift in Sources */,
D6F6A552291F098700F496A8 /* RenameListService.swift in Sources */, D6F6A552291F098700F496A8 /* RenameListService.swift in Sources */,
@ -2002,6 +2013,7 @@
D6AC956723C4347E008C9946 /* MainSceneDelegate.swift in Sources */, D6AC956723C4347E008C9946 /* MainSceneDelegate.swift in Sources */,
D6311C5025B3765B00B27539 /* ImageDataCache.swift in Sources */, D6311C5025B3765B00B27539 /* ImageDataCache.swift in Sources */,
D6BC9DD7232D7811002CA326 /* TimelinesPageViewController.swift in Sources */, D6BC9DD7232D7811002CA326 /* TimelinesPageViewController.swift in Sources */,
D6D9498D298CBB4000C59229 /* TrendingStatusCollectionViewCell.swift in Sources */,
D60E2F2E244248BF005F8713 /* MastodonCachePersistentStore.swift in Sources */, D60E2F2E244248BF005F8713 /* MastodonCachePersistentStore.swift in Sources */,
D620483623D38075008A63EF /* ContentTextView.swift in Sources */, D620483623D38075008A63EF /* ContentTextView.swift in Sources */,
D651C5B42915B00400236EF6 /* ProfileFieldsView.swift in Sources */, D651C5B42915B00400236EF6 /* ProfileFieldsView.swift in Sources */,
@ -2011,6 +2023,7 @@
D6A3BC802321B7E600FD64D5 /* FollowNotificationGroupTableViewCell.swift in Sources */, D6A3BC802321B7E600FD64D5 /* FollowNotificationGroupTableViewCell.swift in Sources */,
D627944D23A9A03D00D38C68 /* ListTimelineViewController.swift in Sources */, D627944D23A9A03D00D38C68 /* ListTimelineViewController.swift in Sources */,
D6D12B2F2925D66500D528E1 /* TimelineGapCollectionViewCell.swift in Sources */, D6D12B2F2925D66500D528E1 /* TimelineGapCollectionViewCell.swift in Sources */,
D6D94955298963A900C59229 /* Colors.swift in Sources */,
D6945C3823AC739F005C403C /* InstanceTimelineViewController.swift in Sources */, D6945C3823AC739F005C403C /* InstanceTimelineViewController.swift in Sources */,
D625E4822588262A0074BB2B /* DraggableTableViewCell.swift in Sources */, D625E4822588262A0074BB2B /* DraggableTableViewCell.swift in Sources */,
D68E525B24A3D77E0054355A /* TuskerRootViewController.swift in Sources */, D68E525B24A3D77E0054355A /* TuskerRootViewController.swift in Sources */,
@ -2066,6 +2079,7 @@
D6A00B1D26379FC900316AD4 /* PollOptionsView.swift in Sources */, D6A00B1D26379FC900316AD4 /* PollOptionsView.swift in Sources */,
D6DF95C12533F5DE0027A9B6 /* RelationshipMO.swift in Sources */, D6DF95C12533F5DE0027A9B6 /* RelationshipMO.swift in Sources */,
D6ADB6EE28EA74E8009924AB /* UIView+Configure.swift in Sources */, D6ADB6EE28EA74E8009924AB /* UIView+Configure.swift in Sources */,
D68329EF299540050026EB24 /* MoreTrendsFooterCollectionViewCell.swift in Sources */,
D623A5412635FB3C0095BD04 /* PollOptionView.swift in Sources */, D623A5412635FB3C0095BD04 /* PollOptionView.swift in Sources */,
D61F75B1293BD85300C0B37F /* CreateFilterService.swift in Sources */, D61F75B1293BD85300C0B37F /* CreateFilterService.swift in Sources */,
D65C6BF525478A9C00A6E89C /* BackgroundableViewController.swift in Sources */, D65C6BF525478A9C00A6E89C /* BackgroundableViewController.swift in Sources */,
@ -2087,6 +2101,7 @@
D6CA8CDE296387310050C433 /* SaveToPhotosActivity.swift in Sources */, D6CA8CDE296387310050C433 /* SaveToPhotosActivity.swift in Sources */,
D6B936712829F72900237D0E /* NSManagedObjectContext+Helpers.swift in Sources */, D6B936712829F72900237D0E /* NSManagedObjectContext+Helpers.swift in Sources */,
D68A76EE295369C7001DA1B3 /* AcknowledgementsView.swift in Sources */, D68A76EE295369C7001DA1B3 /* AcknowledgementsView.swift in Sources */,
D6C3F5192991F5D60009FCFF /* TimelineJumpButton.swift in Sources */,
D63CC7102911F1E4000E19DE /* UIScrollView+Top.swift in Sources */, D63CC7102911F1E4000E19DE /* UIScrollView+Top.swift in Sources */,
D6F6A557291F4F1600F496A8 /* MuteAccountView.swift in Sources */, D6F6A557291F4F1600F496A8 /* MuteAccountView.swift in Sources */,
D65B4B8B297879E900DABDFB /* AccountFollowsViewController.swift in Sources */, D65B4B8B297879E900DABDFB /* AccountFollowsViewController.swift in Sources */,
@ -2238,6 +2253,7 @@
D6E426B325337C7000C02E1C /* CustomEmojiImageView.swift in Sources */, D6E426B325337C7000C02E1C /* CustomEmojiImageView.swift in Sources */,
D6D4DDD0212518A000E1C4BB /* AppDelegate.swift in Sources */, D6D4DDD0212518A000E1C4BB /* AppDelegate.swift in Sources */,
D61F75B3293BD89C00C0B37F /* UpdateFilterService.swift in Sources */, D61F75B3293BD89C00C0B37F /* UpdateFilterService.swift in Sources */,
D6C3F5172991C1A00009FCFF /* View+AppListStyle.swift in Sources */,
D65B4B6A297777D900DABDFB /* StatusNotFoundView.swift in Sources */, D65B4B6A297777D900DABDFB /* StatusNotFoundView.swift in Sources */,
D6412B0924B0291E00F5412E /* MyProfileViewController.swift in Sources */, D6412B0924B0291E00F5412E /* MyProfileViewController.swift in Sources */,
D601FA5D297B2E6F00A8E8B5 /* ConversationCollectionViewController.swift in Sources */, D601FA5D297B2E6F00A8E8B5 /* ConversationCollectionViewController.swift in Sources */,
@ -2408,7 +2424,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 = 73;
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 +2489,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 = 73;
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 +2640,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 = 73;
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 +2668,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 = 73;
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 +2773,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 = 73;
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 +2799,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 = 73;
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;

View File

@ -20,6 +20,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
configureSentry() configureSentry()
swizzleStatusBar() swizzleStatusBar()
swizzlePresentationController()
AppShortcutItem.createItems(for: application) AppShortcutItem.createItems(for: application)
@ -154,4 +155,24 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
Logging.general.error("Unable to swizzle status bar manager") Logging.general.error("Unable to swizzle status bar manager")
} }
} }
private func swizzlePresentationController() {
var originalIMP: IMP?
let imp = imp_implementationWithBlock({ (self: UIPresentationController) in
let new = UITraitCollection(pureBlackDarkMode: self.presentingViewController.traitCollection.pureBlackDarkMode)
if let existing = self.overrideTraitCollection {
self.overrideTraitCollection = UITraitCollection(traitsFrom: [existing, new])
} else {
self.overrideTraitCollection = new
}
let original = unsafeBitCast(originalIMP!, to: (@convention(c) (UIPresentationController) -> Void).self)
original(self)
} as (@convention(block) (UIPresentationController) -> Void))
let sel = Selector(["Necessary", "If", "Traits", "update", "_"].reversed().joined())
originalIMP = class_replaceMethod(UIPresentationController.self, sel, imp, "v@:")
if originalIMP == nil {
Logging.general.error("Unable to swizzle presentation controller")
}
}
} }

View File

@ -0,0 +1,48 @@
//
// View+AppListStyle.swift
// Tusker
//
// Created by Shadowfacts on 2/6/23.
// Copyright © 2023 Shadowfacts. All rights reserved.
//
import SwiftUI
import Combine
extension View {
@ViewBuilder
func appGroupedListBackground(container: UIAppearanceContainer.Type, applyBackground: Bool = true) -> some View {
if #available(iOS 16.0, *) {
if applyBackground {
self
.scrollContentBackground(.hidden)
.background(Color.appGroupedBackground.edgesIgnoringSafeArea(.all))
} else {
self
.scrollContentBackground(.hidden)
}
} else {
self
.onAppear {
UITableView.appearance(whenContainedInInstancesOf: [container]).backgroundColor = .appGroupedBackground
}
}
}
func appGroupedListRowBackground() -> some View {
self.modifier(AppGroupedListRowBackground())
}
}
private struct AppGroupedListRowBackground: ViewModifier {
@Environment(\.colorScheme) private var colorScheme
func body(content: Content) -> some View {
if colorScheme == .dark, !Preferences.shared.pureBlackDarkMode {
content
.listRowBackground(Color.appGroupedCellBackground)
} else {
content
}
}
}

View File

@ -0,0 +1,104 @@
//
// Colors.swift
// Tusker
//
// Created by Shadowfacts on 1/31/23.
// Copyright © 2023 Shadowfacts. All rights reserved.
//
import UIKit
import SwiftUI
extension UIColor {
static let appBackground = UIColor { traitCollection in
if case .dark = traitCollection.userInterfaceStyle,
!traitCollection.pureBlackDarkMode {
return UIColor(hue: 230/360, saturation: 23/100, brightness: 10/100, alpha: 1)
} else {
return .systemBackground
}
}
static let appSecondaryBackground = UIColor { traitCollection in
if case .dark = traitCollection.userInterfaceStyle,
!traitCollection.pureBlackDarkMode {
if traitCollection.userInterfaceLevel == .elevated {
return UIColor(hue: 230/360, saturation: 23/100, brightness: 10/100, alpha: 1)
} else {
return UIColor(hue: 230/360, saturation: 23/100, brightness: 5/100, alpha: 1)
}
} else {
return .secondarySystemBackground
}
}
static let appGroupedBackground = UIColor { traitCollection in
if case .dark = traitCollection.userInterfaceStyle,
!traitCollection.pureBlackDarkMode {
return .appSecondaryBackground
} else {
return .systemGroupedBackground
}
}
static let appSelectedCellBackground = UIColor { traitCollection in
if case .dark = traitCollection.userInterfaceStyle,
!traitCollection.pureBlackDarkMode {
return UIColor(hue: 230/360, saturation: 20/100, brightness: 27/100, alpha: 1)
} else {
return .systemFill
}
}
static let appGroupedCellBackground = UIColor { traitCollection in
if case .dark = traitCollection.userInterfaceStyle {
if traitCollection.pureBlackDarkMode {
return .secondarySystemBackground
} else {
return .appFill
}
} else {
return .systemBackground
}
}
static let appFill = UIColor { traitCollection in
if case .dark = traitCollection.userInterfaceStyle,
!traitCollection.pureBlackDarkMode {
return UIColor(hue: 230/360, saturation: 20/100, brightness: 17/100, alpha: 1)
} else {
return .systemFill
}
}
}
extension Color {
static let appBackground = Color(uiColor: .appBackground)
static let appGroupedBackground = Color(uiColor: .appGroupedBackground)
static let appSecondaryBackground = Color(uiColor: .appSecondaryBackground)
static let appSelectedCellBackground = Color(uiColor: .appGroupedCellBackground)
static let appGroupedCellBackground = Color(uiColor: .appGroupedCellBackground)
static let appFill = Color(uiColor: .appFill)
}
private let traitsKey: String = ["Traits", "Defined", "client", "_"].reversed().joined()
private let key = "tusker_usePureBlackDarkMode"
extension UITraitCollection {
var pureBlackDarkMode: Bool {
get {
// default to true to mach OS behavior
(value(forKey: traitsKey) as? [String: Any])?[key] as? Bool ?? true
}
set {
var dict = value(forKey: traitsKey) as? [String: Any] ?? [:]
dict[key] = newValue
setValue(dict, forKey: traitsKey)
}
}
convenience init(pureBlackDarkMode: Bool) {
self.init()
self.pureBlackDarkMode = pureBlackDarkMode
}
}

View File

@ -38,6 +38,7 @@ class Preferences: Codable, ObservableObject {
let container = try decoder.container(keyedBy: CodingKeys.self) let container = try decoder.container(keyedBy: CodingKeys.self)
self.theme = try container.decode(UIUserInterfaceStyle.self, forKey: .theme) self.theme = try container.decode(UIUserInterfaceStyle.self, forKey: .theme)
self.pureBlackDarkMode = try container.decodeIfPresent(Bool.self, forKey: .pureBlackDarkMode) ?? true
self.accentColor = try container.decodeIfPresent(AccentColor.self, forKey: .accentColor) ?? .default self.accentColor = try container.decodeIfPresent(AccentColor.self, forKey: .accentColor) ?? .default
self.avatarStyle = try container.decode(AvatarStyle.self, forKey: .avatarStyle) self.avatarStyle = try container.decode(AvatarStyle.self, forKey: .avatarStyle)
self.hideCustomEmojiInUsernames = try container.decode(Bool.self, forKey: .hideCustomEmojiInUsernames) self.hideCustomEmojiInUsernames = try container.decode(Bool.self, forKey: .hideCustomEmojiInUsernames)
@ -92,6 +93,7 @@ class Preferences: Codable, ObservableObject {
var container = encoder.container(keyedBy: CodingKeys.self) var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(theme, forKey: .theme) try container.encode(theme, forKey: .theme)
try container.encode(pureBlackDarkMode, forKey: .pureBlackDarkMode)
try container.encode(accentColor, forKey: .accentColor) try container.encode(accentColor, forKey: .accentColor)
try container.encode(avatarStyle, forKey: .avatarStyle) try container.encode(avatarStyle, forKey: .avatarStyle)
try container.encode(hideCustomEmojiInUsernames, forKey: .hideCustomEmojiInUsernames) try container.encode(hideCustomEmojiInUsernames, forKey: .hideCustomEmojiInUsernames)
@ -140,6 +142,7 @@ class Preferences: Codable, ObservableObject {
// MARK: Appearance // MARK: Appearance
@Published var theme = UIUserInterfaceStyle.unspecified @Published var theme = UIUserInterfaceStyle.unspecified
@Published var pureBlackDarkMode = true
@Published var accentColor = AccentColor.default @Published var accentColor = AccentColor.default
@Published var avatarStyle = AvatarStyle.roundRect @Published var avatarStyle = AvatarStyle.roundRect
@Published var hideCustomEmojiInUsernames = false @Published var hideCustomEmojiInUsernames = false
@ -202,6 +205,7 @@ class Preferences: Codable, ObservableObject {
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case theme case theme
case pureBlackDarkMode
case accentColor case accentColor
case avatarStyle case avatarStyle
case hideCustomEmojiInUsernames case hideCustomEmojiInUsernames

View File

@ -243,8 +243,13 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDelegate
} }
@objc func themePrefChanged() { @objc func themePrefChanged() {
window?.overrideUserInterfaceStyle = Preferences.shared.theme guard let window else { return }
window?.tintColor = Preferences.shared.accentColor.color window.overrideUserInterfaceStyle = Preferences.shared.theme
window.tintColor = Preferences.shared.accentColor.color
let key = ["Controller", "Presentation", "root", "_"].reversed().joined()
if let rootPresentationController = window.value(forKey: key) as? UIPresentationController {
rootPresentationController.overrideTraitCollection = UITraitCollection(pureBlackDarkMode: Preferences.shared.pureBlackDarkMode)
}
} }
func showAddAccount() { func showAddAccount() {

View File

@ -41,7 +41,8 @@ class AccountFollowsListViewController: UIViewController, CollectionViewControll
} }
override func loadView() { override func loadView() {
var config = UICollectionLayoutListConfiguration(appearance: .grouped) var config = UICollectionLayoutListConfiguration(appearance: .plain)
config.backgroundColor = .appBackground
config.itemSeparatorHandler = { [unowned self] indexPath, sectionConfig in config.itemSeparatorHandler = { [unowned self] indexPath, sectionConfig in
guard let item = self.dataSource.itemIdentifier(for: indexPath) else { guard let item = self.dataSource.itemIdentifier(for: indexPath) else {
return sectionConfig return sectionConfig
@ -65,6 +66,16 @@ class AccountFollowsListViewController: UIViewController, CollectionViewControll
let accountCell = UICollectionView.CellRegistration<AccountCollectionViewCell, String> { [unowned self] cell, indexPath, item in let accountCell = UICollectionView.CellRegistration<AccountCollectionViewCell, String> { [unowned self] cell, indexPath, item in
cell.delegate = self cell.delegate = self
cell.updateUI(accountID: item) cell.updateUI(accountID: item)
cell.configurationUpdateHandler = { cell, state in
var config = UIBackgroundConfiguration.listPlainCell().updated(for: state)
if state.isHighlighted || state.isSelected {
config.backgroundColor = .appSelectedCellBackground
} else {
config.backgroundColor = .appBackground
}
cell.backgroundConfiguration = config
}
} }
let loadingCell = UICollectionView.CellRegistration<LoadingCollectionViewCell, Void> { cell, indexPath, item in let loadingCell = UICollectionView.CellRegistration<LoadingCollectionViewCell, Void> { cell, indexPath, item in
cell.indicator.startAnimating() cell.indicator.startAnimating()

View File

@ -31,7 +31,8 @@ class AccountListViewController: UIViewController, CollectionViewController {
} }
override func loadView() { override func loadView() {
let config = UICollectionLayoutListConfiguration(appearance: .grouped) var config = UICollectionLayoutListConfiguration(appearance: .grouped)
config.backgroundColor = .appGroupedBackground
let layout = UICollectionViewCompositionalLayout.list(using: config) let layout = UICollectionViewCompositionalLayout.list(using: config)
view = UICollectionView(frame: .zero, collectionViewLayout: layout) view = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.delegate = self collectionView.delegate = self

View File

@ -72,7 +72,8 @@ class AssetCollectionViewController: UIViewController, UICollectionViewDelegate
// bottom ignores safe area because we want cells to underflow bottom of the screen on notched iPhones // bottom ignores safe area because we want cells to underflow bottom of the screen on notched iPhones
view.bottomAnchor.constraint(equalTo: collectionView.bottomAnchor), view.bottomAnchor.constraint(equalTo: collectionView.bottomAnchor),
]) ])
view.backgroundColor = .systemBackground view.backgroundColor = .appBackground
collectionView.backgroundColor = .appBackground
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(donePressed)) navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(donePressed))

View File

@ -34,6 +34,7 @@ class AssetCollectionsListViewController: UITableViewController {
tableView.register(UINib(nibName: "AlbumTableViewCell", bundle: .main), forCellReuseIdentifier: "albumCell") tableView.register(UINib(nibName: "AlbumTableViewCell", bundle: .main), forCellReuseIdentifier: "albumCell")
tableView.allowsFocus = true tableView.allowsFocus = true
tableView.backgroundColor = .appGroupedBackground
dataSource = DataSource(tableView: tableView, cellProvider: { (tableView, indexPath, item) -> UITableViewCell? in dataSource = DataSource(tableView: tableView, cellProvider: { (tableView, indexPath, item) -> UITableViewCell? in
switch item { switch item {

View File

@ -112,7 +112,7 @@ struct ComposeAttachmentsList: View {
self.isShowingAssetPickerPopover = false self.isShowingAssetPickerPopover = false
} }
// on iPadOS 16, this is necessary to show the dark color in the popover arrow // on iPadOS 16, this is necessary to show the dark color in the popover arrow
.background(Color(.systemBackground)) .background(Color(.appBackground))
.environment(\.colorScheme, .dark) .environment(\.colorScheme, .dark)
.edgesIgnoringSafeArea(.bottom) .edgesIgnoringSafeArea(.bottom)
.withSheetDetentsIfAvailable() .withSheetDetentsIfAvailable()

View File

@ -111,7 +111,7 @@ struct ComposePollView: View {
private var backgroundColor: Color { private var backgroundColor: Color {
// in light mode, .secondarySystemBackground has a blue-ish hue, which we don't want // in light mode, .secondarySystemBackground has a blue-ish hue, which we don't want
colorScheme == .dark ? Color(UIColor.secondarySystemBackground) : Color(white: 0.95) colorScheme == .dark ? Color.appFill : Color(white: 0.95)
} }
private var buttonBackgroundColor: Color { private var buttonBackgroundColor: Color {
@ -191,7 +191,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)", maxLength: mastodonController.instance?.pollsConfiguration?.maxCharactersPerOption)
return field.backgroundColor(.systemBackground) return field.backgroundColor(.appBackground)
} }
private func removeOption() { private func removeOption() {
@ -216,7 +216,7 @@ struct ComposePollOption: View {
.cornerRadius(radiusFraction * size) .cornerRadius(radiusFraction * size)
Rectangle() Rectangle()
.foregroundColor(Color(UIColor.systemBackground)) .foregroundColor(Color(UIColor.appBackground))
.frame(width: innerSize, height: innerSize) .frame(width: innerSize, height: innerSize)
.cornerRadius(radiusFraction * innerSize) .cornerRadius(radiusFraction * innerSize)
} }

View File

@ -94,6 +94,10 @@ struct ComposeView: View {
var body: some View { var body: some View {
ZStack(alignment: .top) { ZStack(alignment: .top) {
// just using .background doesn't work; for some reason it gets inset immediately after the software keyboard is dismissed
Color.appBackground
.edgesIgnoringSafeArea(.all)
mainList mainList
.scrollDismissesKeyboardInteractivelyIfAvailable() .scrollDismissesKeyboardInteractivelyIfAvailable()
@ -124,7 +128,7 @@ struct ComposeView: View {
} }
}) })
.sheet(isPresented: $uiState.isShowingDraftsList) { .sheet(isPresented: $uiState.isShowingDraftsList) {
DraftsView(currentDraft: draft, mastodonController: mastodonController) DraftsRepresentable(currentDraft: draft, mastodonController: mastodonController)
} }
.actionSheet(isPresented: $uiState.isShowingSaveDraftSheet, content: self.saveAndCloseSheet) .actionSheet(isPresented: $uiState.isShowingSaveDraftSheet, content: self.saveAndCloseSheet)
.alert(isPresented: $isShowingPostErrorAlert) { .alert(isPresented: $isShowingPostErrorAlert) {
@ -169,11 +173,13 @@ struct ComposeView: View {
) )
.listRowInsets(EdgeInsets(top: 8, leading: 8, bottom: 4, trailing: 8)) .listRowInsets(EdgeInsets(top: 8, leading: 8, bottom: 4, trailing: 8))
.listRowSeparator(.hidden) .listRowSeparator(.hidden)
.listRowBackground(Color.appBackground)
} }
header header
.listRowInsets(EdgeInsets(top: draft.inReplyToID == nil ? 8 : 4, leading: 8, bottom: 4, trailing: 8)) .listRowInsets(EdgeInsets(top: draft.inReplyToID == nil ? 8 : 4, leading: 8, bottom: 4, trailing: 8))
.listRowSeparator(.hidden) .listRowSeparator(.hidden)
.listRowBackground(Color.appBackground)
if uiState.draft.contentWarningEnabled { if uiState.draft.contentWarningEnabled {
ComposeEmojiTextField( ComposeEmojiTextField(
@ -184,6 +190,7 @@ struct ComposeView: View {
) )
.listRowInsets(EdgeInsets(top: 4, leading: 8, bottom: 4, trailing: 8)) .listRowInsets(EdgeInsets(top: 4, leading: 8, bottom: 4, trailing: 8))
.listRowSeparator(.hidden) .listRowSeparator(.hidden)
.listRowBackground(Color.appBackground)
} }
MainComposeTextView( MainComposeTextView(
@ -192,17 +199,20 @@ struct ComposeView: View {
) )
.listRowInsets(EdgeInsets(top: 4, leading: 8, bottom: 4, trailing: 8)) .listRowInsets(EdgeInsets(top: 4, leading: 8, bottom: 4, trailing: 8))
.listRowSeparator(.hidden) .listRowSeparator(.hidden)
.listRowBackground(Color.appBackground)
if let poll = draft.poll { if let poll = draft.poll {
ComposePollView(draft: draft, poll: poll) ComposePollView(draft: draft, poll: poll)
.listRowInsets(EdgeInsets(top: 4, leading: 8, bottom: 4, trailing: 8)) .listRowInsets(EdgeInsets(top: 4, leading: 8, bottom: 4, trailing: 8))
.listRowSeparator(.hidden) .listRowSeparator(.hidden)
.listRowBackground(Color.appBackground)
} }
ComposeAttachmentsList( ComposeAttachmentsList(
draft: draft draft: draft
) )
.listRowInsets(EdgeInsets(top: 4, leading: 8, bottom: 8, trailing: 8)) .listRowInsets(EdgeInsets(top: 4, leading: 8, bottom: 8, trailing: 8))
.listRowBackground(Color.appBackground)
} }
.animation(.default, value: draft.poll?.options.count) .animation(.default, value: draft.poll?.options.count)
.scrollDismissesKeyboardInteractivelyIfAvailable() .scrollDismissesKeyboardInteractivelyIfAvailable()
@ -319,7 +329,7 @@ struct ComposeView: View {
} }
} }
private extension View { extension View {
@available(iOS, obsoleted: 16.0) @available(iOS, obsoleted: 16.0)
@ViewBuilder @ViewBuilder
func scrollDismissesKeyboardInteractivelyIfAvailable() -> some View { func scrollDismissesKeyboardInteractivelyIfAvailable() -> some View {

View File

@ -8,6 +8,21 @@
import SwiftUI import SwiftUI
@available(iOS, obsoleted: 16.0)
struct DraftsRepresentable: UIViewControllerRepresentable {
typealias UIViewControllerType = UIHostingController<DraftsView>
let currentDraft: Draft
let mastodonController: MastodonController
func makeUIViewController(context: Context) -> UIHostingController<DraftsView> {
return UIHostingController(rootView: DraftsView(currentDraft: currentDraft, mastodonController: mastodonController))
}
func updateUIViewController(_ uiViewController: UIHostingController<DraftsView>, context: Context) {
}
}
struct DraftsView: View { struct DraftsView: View {
let currentDraft: Draft let currentDraft: Draft
// don't pass this in via the environment b/c it crashes on macOS (at least, in Designed for iPad mode) since the environment doesn't get propagated through the modal popup window or something // don't pass this in via the environment b/c it crashes on macOS (at least, in Designed for iPad mode) since the environment doesn't get propagated through the modal popup window or something
@ -49,8 +64,10 @@ struct DraftsView: View {
.map { visibleDrafts[$0] } .map { visibleDrafts[$0] }
.forEach { draftsManager.remove($0) } .forEach { draftsManager.remove($0) }
} }
.appGroupedListRowBackground()
} }
.listStyle(.plain) .listStyle(.plain)
.appGroupedListBackground(container: DraftsRepresentable.UIViewControllerType.self)
.navigationTitle(Text("Drafts")) .navigationTitle(Text("Drafts"))
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.toolbar { .toolbar {

View File

@ -38,10 +38,11 @@ struct MainComposeTextView: View {
@Binding var becomeFirstResponder: Bool @Binding var becomeFirstResponder: Bool
@State private var hasFirstAppeared = false @State private var hasFirstAppeared = false
@ScaledMetric private var fontSize = 20 @ScaledMetric private var fontSize = 20
@Environment(\.colorScheme) private var colorScheme
var body: some View { var body: some View {
ZStack(alignment: .topLeading) { ZStack(alignment: .topLeading) {
Color(UIColor.secondarySystemBackground) colorScheme == .dark ? Color.appFill : Color(uiColor: .secondarySystemBackground)
if draft.text.isEmpty { if draft.text.isEmpty {
placeholder placeholder

View File

@ -37,7 +37,7 @@ class ConversationCollectionViewController: UIViewController, CollectionViewCont
override func loadView() { override func loadView() {
var config = UICollectionLayoutListConfiguration(appearance: .plain) var config = UICollectionLayoutListConfiguration(appearance: .plain)
config.backgroundColor = .secondarySystemBackground config.backgroundColor = .appSecondaryBackground
config.leadingSwipeActionsConfigurationProvider = { [unowned self] in config.leadingSwipeActionsConfigurationProvider = { [unowned self] in
(collectionView.cellForItem(at: $0) as? TimelineStatusCollectionViewCell)?.leadingSwipeActions() (collectionView.cellForItem(at: $0) as? TimelineStatusCollectionViewCell)?.leadingSwipeActions()
} }
@ -59,7 +59,7 @@ class ConversationCollectionViewController: UIViewController, CollectionViewCont
} }
// we're not using contenetInsetsReference = .readableContent here because it always insets the cells even if // we're not using contenetInsetsReference = .readableContent here because it always insets the cells even if
// the collection view's actual width is narrow enough to fit in the readable width, resulting in a bit of the // the collection view's actual width is narrow enough to fit in the readable width, resulting in a bit of the
// background color always peaking through the edges // background color always peeking through the edges
let layout = UICollectionViewCompositionalLayout.list(using: config) let layout = UICollectionViewCompositionalLayout.list(using: config)
view = UICollectionView(frame: .zero, collectionViewLayout: layout) view = UICollectionView(frame: .zero, collectionViewLayout: layout)
// something about the autoresizing mask breaks resizing the vc // something about the autoresizing mask breaks resizing the vc

View File

@ -94,7 +94,7 @@ class ConversationViewController: UIViewController {
title = NSLocalizedString("Conversation", comment: "conversation screen title") title = NSLocalizedString("Conversation", comment: "conversation screen title")
view.backgroundColor = .secondarySystemBackground view.backgroundColor = .appSecondaryBackground
collapseBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "eye.fill")!, style: .plain, target: self, action: #selector(toggleCollapseButtonPressed)) collapseBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "eye.fill")!, style: .plain, target: self, action: #selector(toggleCollapseButtonPressed))
updateVisibilityBarButtonItem() updateVisibilityBarButtonItem()

View File

@ -141,18 +141,13 @@ class ExpandThreadCollectionViewCell: UICollectionViewListCell {
} }
override func updateConfiguration(using state: UICellConfigurationState) { override func updateConfiguration(using state: UICellConfigurationState) {
var config = UIBackgroundConfiguration.listPlainCell() var config = UIBackgroundConfiguration.listPlainCell().updated(for: state)
if state.isSelected || state.isHighlighted { if state.isSelected || state.isHighlighted {
var hue: CGFloat = 0 config.backgroundColor = .appSelectedCellBackground
var saturation: CGFloat = 0
var brightness: CGFloat = 0
UIColor.secondarySystemBackground.getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: nil)
let sign: CGFloat = traitCollection.userInterfaceStyle == .dark ? 1 : -1
config.backgroundColor = UIColor(hue: hue, saturation: saturation, brightness: max(0, brightness + sign * 0.1), alpha: 1)
} else { } else {
config.backgroundColor = .secondarySystemBackground config.backgroundColor = .appSecondaryBackground
} }
backgroundConfiguration = config.updated(for: state) backgroundConfiguration = config
} }
} }

View File

@ -9,6 +9,20 @@
import SwiftUI import SwiftUI
import Pachyderm import Pachyderm
@available(iOS, obsoleted: 16.0)
struct AddHashtagPinnedTimelineRepresentable: UIViewControllerRepresentable {
typealias UIViewControllerType = UIHostingController<AddHashtagPinnedTimelineView>
@Binding var pinnedTimelines: [PinnedTimeline]
func makeUIViewController(context: Context) -> UIHostingController<AddHashtagPinnedTimelineView> {
return UIHostingController(rootView: AddHashtagPinnedTimelineView(pinnedTimelines: $pinnedTimelines))
}
func updateUIViewController(_ uiViewController: UIHostingController<AddHashtagPinnedTimelineView>, context: Context) {
}
}
struct AddHashtagPinnedTimelineView: View { struct AddHashtagPinnedTimelineView: View {
@EnvironmentObject private var mastodonController: MastodonController @EnvironmentObject private var mastodonController: MastodonController
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss
@ -34,6 +48,8 @@ struct AddHashtagPinnedTimelineView: View {
var body: some View { var body: some View {
NavigationView { NavigationView {
list list
.listStyle(.grouped)
.appGroupedListBackground(container: AddHashtagPinnedTimelineRepresentable.UIViewControllerType.self)
.navigationTitle("Add Hashtag") .navigationTitle("Add Hashtag")
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.searchable(text: $viewModel.searchQuery, placement: .navigationBarDrawer(displayMode: .always), prompt: Text("Search for hashtags")) .searchable(text: $viewModel.searchQuery, placement: .navigationBarDrawer(displayMode: .always), prompt: Text("Search for hashtags"))
@ -57,8 +73,9 @@ struct AddHashtagPinnedTimelineView: View {
}) })
} }
@ViewBuilder
private var list: some View { private var list: some View {
List { let list = List {
Section { Section {
if viewModel.searchQuery.isEmpty { if viewModel.searchQuery.isEmpty {
forEachTag(savedAndFollowedHashtags) forEachTag(savedAndFollowedHashtags)
@ -73,8 +90,17 @@ struct AddHashtagPinnedTimelineView: View {
.listRowBackground(EmptyView()) .listRowBackground(EmptyView())
.listRowSeparator(.hidden) .listRowSeparator(.hidden)
} }
.appGroupedListRowBackground()
} }
.listStyle(.grouped) .listStyle(.grouped)
if #available(iOS 16.0, *) {
list
.scrollContentBackground(.hidden)
.background(Color.appGroupedBackground)
} else {
list
}
} }
private func forEachTag(_ tags: [String]) -> some View { private func forEachTag(_ tags: [String]) -> some View {

View File

@ -51,6 +51,7 @@ struct CustomizeTimelinesList: View {
private var navigationBody: some View { private var navigationBody: some View {
List { List {
PinnedTimelinesView(accountPreferences: mastodonController.accountPreferences) PinnedTimelinesView(accountPreferences: mastodonController.accountPreferences)
.appGroupedListRowBackground()
Section { Section {
Toggle(isOn: $preferences.hideReblogsInTimelines) { Toggle(isOn: $preferences.hideReblogsInTimelines) {
@ -62,6 +63,7 @@ struct CustomizeTimelinesList: View {
} header: { } header: {
Text("Home Timeline") Text("Home Timeline")
} }
.appGroupedListRowBackground()
Section { Section {
filtersForEach(unexpiredFilters) filtersForEach(unexpiredFilters)
@ -75,6 +77,7 @@ struct CustomizeTimelinesList: View {
} header: { } header: {
Text("Active Filters") Text("Active Filters")
} }
.appGroupedListRowBackground()
if !expiredFilters.isEmpty { if !expiredFilters.isEmpty {
Section { Section {
@ -82,8 +85,11 @@ struct CustomizeTimelinesList: View {
} header: { } header: {
Text("Expired Filters") Text("Expired Filters")
} }
.appGroupedListRowBackground()
} }
} }
.listStyle(.insetGrouped)
.appGroupedListBackground(container: UIHostingController<CustomizeTimelinesList>.self)
.navigationTitle(Text("Customize Timelines")) .navigationTitle(Text("Customize Timelines"))
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.toolbar { .toolbar {

View File

@ -72,6 +72,7 @@ struct EditFilterView: View {
filter.title = newValue filter.title = newValue
})) }))
} }
.appGroupedListRowBackground()
} }
Section { Section {
@ -96,6 +97,7 @@ struct EditFilterView: View {
} }
} }
} }
.appGroupedListRowBackground()
Section { Section {
if mastodonController.instanceFeatures.filtersV2 { if mastodonController.instanceFeatures.filtersV2 {
@ -120,6 +122,7 @@ struct EditFilterView: View {
} }
} }
} }
.appGroupedListRowBackground()
Section { Section {
ForEach(FilterV1.Context.allCases, id: \.rawValue) { context in ForEach(FilterV1.Context.allCases, id: \.rawValue) { context in
@ -141,7 +144,10 @@ struct EditFilterView: View {
} header: { } header: {
Text("Contexts") Text("Contexts")
} }
.appGroupedListRowBackground()
} }
.appGroupedListBackground(container: UIHostingController<CustomizeTimelinesList>.self)
.scrollDismissesKeyboardInteractivelyIfAvailable()
.navigationTitle(create ? "Add Filter" : "Edit Filter") .navigationTitle(create ? "Add Filter" : "Edit Filter")
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.toolbar { .toolbar {

View File

@ -111,7 +111,13 @@ struct PinnedTimelinesView: View {
Text("Pinned Timelines") Text("Pinned Timelines")
} }
.sheet(isPresented: $isShowingAddHashtagSheet, content: { .sheet(isPresented: $isShowingAddHashtagSheet, content: {
AddHashtagPinnedTimelineView(pinnedTimelines: $pinnedTimelines) if #available(iOS 16.0, *) {
AddHashtagPinnedTimelineView(pinnedTimelines: $pinnedTimelines)
.edgesIgnoringSafeArea(.bottom)
} else {
AddHashtagPinnedTimelineRepresentable(pinnedTimelines: $pinnedTimelines)
.edgesIgnoringSafeArea(.bottom)
}
}) })
.sheet(isPresented: $isShowingAddInstanceSheet, content: { .sheet(isPresented: $isShowingAddInstanceSheet, content: {
AddInstancePinnedTimelineView(pinnedTimelines: $pinnedTimelines) AddInstancePinnedTimelineView(pinnedTimelines: $pinnedTimelines)

View File

@ -34,15 +34,15 @@ class AddSavedHashtagViewController: UIViewController {
title = NSLocalizedString("Search", comment: "search screen title") title = NSLocalizedString("Search", comment: "search screen title")
view.backgroundColor = .systemGroupedBackground
var config = UICollectionLayoutListConfiguration(appearance: .grouped) var config = UICollectionLayoutListConfiguration(appearance: .grouped)
config.backgroundColor = .appGroupedBackground
config.headerMode = .supplementary config.headerMode = .supplementary
let layout = UICollectionViewCompositionalLayout.list(using: config) let layout = UICollectionViewCompositionalLayout.list(using: config)
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout) collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight] collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
collectionView.delegate = self collectionView.delegate = self
collectionView.allowsFocus = true collectionView.allowsFocus = true
collectionView.backgroundColor = .appGroupedBackground
view.addSubview(collectionView) view.addSubview(collectionView)
let sectionHeaderCell = UICollectionView.SupplementaryRegistration<UICollectionViewListCell>(elementKind: UICollectionView.elementKindSectionHeader) { (headerView, collectionView, indexPath) in let sectionHeaderCell = UICollectionView.SupplementaryRegistration<UICollectionViewListCell>(elementKind: UICollectionView.elementKindSectionHeader) { (headerView, collectionView, indexPath) in

View File

@ -43,7 +43,8 @@ class ExploreViewController: UIViewController, UICollectionViewDelegate, Collect
super.viewDidLoad() super.viewDidLoad()
var configuration = UICollectionLayoutListConfiguration(appearance: .insetGrouped) var configuration = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
configuration.trailingSwipeActionsConfigurationProvider = self.trailingSwipeActionsForCell(at:) configuration.backgroundColor = .appGroupedBackground
configuration.trailingSwipeActionsConfigurationProvider = { [unowned self] in self.trailingSwipeActionsForCell(at: $0) }
configuration.headerMode = .supplementary configuration.headerMode = .supplementary
let layout = UICollectionViewCompositionalLayout.list(using: configuration) let layout = UICollectionViewCompositionalLayout.list(using: configuration)
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout) collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
@ -100,12 +101,23 @@ class ExploreViewController: UIViewController, UICollectionViewDelegate, Collect
override func viewWillAppear(_ animated: Bool) { override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated) super.viewWillAppear(animated)
// UISearchController exists outside of the normal VC hierarchy,
// so we manually propagate this down to the results controller
// so that it can deselect on appear
if searchController.isActive {
resultsController.viewWillAppear(animated)
}
clearSelectionOnAppear(animated: animated) clearSelectionOnAppear(animated: animated)
} }
override func viewDidAppear(_ animated: Bool) { override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated) super.viewDidAppear(animated)
if searchController.isActive {
resultsController.viewDidAppear(animated)
}
// this is a workaround for the issue that setting isActive on a search controller that is not visible // 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 // does not cause it to automatically become active once it becomes visible
// see FB7814561 // see FB7814561
@ -130,6 +142,16 @@ class ExploreViewController: UIViewController, UICollectionViewDelegate, Collect
config.image = item.image config.image = item.image
cell.contentConfiguration = config cell.contentConfiguration = config
cell.configurationUpdateHandler = { cell, state in
var config = UIBackgroundConfiguration.listGroupedCell()
if state.isHighlighted || state.isSelected {
config.backgroundColor = .appSelectedCellBackground
} else {
config.backgroundColor = .appGroupedCellBackground
}
cell.backgroundConfiguration = config
}
switch item { switch item {
case .addList, .addSavedHashtag, .findInstance: case .addList, .addSavedHashtag, .findInstance:
cell.accessories = [] cell.accessories = []

View File

@ -39,6 +39,7 @@ class FeaturedProfileCollectionViewCell: UICollectionViewCell {
noteTextView.textContainerInset = UIEdgeInsets(top: 16, left: 4, bottom: 16, right: 4) noteTextView.textContainerInset = UIEdgeInsets(top: 16, left: 4, bottom: 16, right: 4)
backgroundColor = .clear backgroundColor = .clear
clippingView.backgroundColor = .appBackground
clippingView.layer.cornerRadius = 5 clippingView.layer.cornerRadius = 5
clippingView.layer.borderWidth = 1 clippingView.layer.borderWidth = 1
clippingView.layer.masksToBounds = true clippingView.layer.masksToBounds = true

View File

@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="19529" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES"> <document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="21507" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_1" orientation="portrait" appearance="light"/> <device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies> <dependencies>
<deployment identifier="iOS"/> <deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="19519"/> <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21505"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/> <capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/> <capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
@ -55,7 +55,6 @@
</label> </label>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" userInteractionEnabled="NO" contentMode="scaleToFill" verticalCompressionResistancePriority="749" scrollEnabled="NO" editable="NO" textAlignment="natural" selectable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="bvj-F0-ggC" customClass="StatusContentTextView" customModule="Tusker" customModuleProvider="target"> <textView clipsSubviews="YES" multipleTouchEnabled="YES" userInteractionEnabled="NO" contentMode="scaleToFill" verticalCompressionResistancePriority="749" scrollEnabled="NO" editable="NO" textAlignment="natural" selectable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="bvj-F0-ggC" customClass="StatusContentTextView" customModule="Tusker" customModuleProvider="target">
<rect key="frame" x="8" y="102" width="384" height="98"/> <rect key="frame" x="8" y="102" width="384" height="98"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<string key="text">Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda.</string> <string key="text">Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda.</string>
<color key="textColor" systemColor="labelColor"/> <color key="textColor" systemColor="labelColor"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/> <fontDescription key="fontDescription" type="system" pointSize="14"/>
@ -102,7 +101,7 @@
</objects> </objects>
<resources> <resources>
<systemColor name="labelColor"> <systemColor name="labelColor">
<color white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor> </systemColor>
<systemColor name="systemBackgroundColor"> <systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>

View File

@ -0,0 +1,84 @@
//
// MoreTrendsFooterCollectionViewCell.swift
// Tusker
//
// Created by Shadowfacts on 2/9/23.
// Copyright © 2023 Shadowfacts. All rights reserved.
//
import UIKit
class MoreTrendsFooterCollectionViewCell: UICollectionViewCell {
weak var delegate: TuskerNavigationDelegate?
private var button = UIButton()
private var kind: Kind!
override init(frame: CGRect) {
super.init(frame: frame)
button.addTarget(self, action: #selector(buttonPressed), for: .touchUpInside)
button.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(button)
NSLayoutConstraint.activate([
button.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
button.topAnchor.constraint(equalTo: contentView.topAnchor),
button.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func updateUI(_ kind: Kind) {
guard self.kind != kind else {
return
}
self.kind = kind
var config = UIButton.Configuration.plain()
var title: AttributedString
switch kind {
case .hashtags:
title = "More Trending Hashtags"
case .links:
title = "More Trending Links"
case .profileSuggestions:
title = "More Suggested Accounts"
}
title.font = .preferredFont(forTextStyle: .body).withTraits(.traitBold)!
config.attributedTitle = title
config.image = UIImage(systemName: "chevron.right")
config.imagePlacement = .trailing
config.buttonSize = .mini
button.configuration = config
}
@objc private func buttonPressed() {
guard let delegate else {
return
}
switch kind {
case nil:
return
case .hashtags:
delegate.show(TrendingHashtagsViewController(mastodonController: delegate.apiController))
case .links:
delegate.show(TrendingLinksViewController(mastodonController: delegate.apiController))
case .profileSuggestions:
delegate.show(SuggestedProfilesViewController(mastodonController: delegate.apiController))
}
}
}
extension MoreTrendsFooterCollectionViewCell {
enum Kind {
case hashtags
case links
case profileSuggestions
}
}

View File

@ -64,7 +64,7 @@ class ProfileDirectoryViewController: UIViewController {
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout) collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight] collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
collectionView.backgroundColor = .secondarySystemBackground collectionView.backgroundColor = .appSecondaryBackground
collectionView.register(UINib(nibName: "FeaturedProfileCollectionViewCell", bundle: .main), forCellWithReuseIdentifier: "featuredProfileCell") collectionView.register(UINib(nibName: "FeaturedProfileCollectionViewCell", bundle: .main), forCellWithReuseIdentifier: "featuredProfileCell")
collectionView.delegate = self collectionView.delegate = self
collectionView.dragDelegate = self collectionView.dragDelegate = self

View File

@ -36,6 +36,7 @@ class SuggestedProfileCardCollectionViewCell: UICollectionViewCell {
layer.shadowOffset = .zero layer.shadowOffset = .zero
layer.masksToBounds = false layer.masksToBounds = false
contentView.layer.cornerRadius = 12.5 contentView.layer.cornerRadius = 12.5
contentView.backgroundColor = .appGroupedCellBackground
updateLayerColors() updateLayerColors()
headerImageView.cache = .headers headerImageView.cache = .headers
@ -189,7 +190,7 @@ private struct SuggestionSourceView: View {
var body: some View { var body: some View {
VStack(alignment: .leading) { VStack(alignment: .leading) {
HStack { HStack {
Image(uiImage: source.image) Image(uiImage: source.image.withRenderingMode(.alwaysTemplate))
Text(source.title) Text(source.title)
Spacer() Spacer()
} }

View File

@ -0,0 +1,212 @@
//
// SuggestedProfilesViewController.swift
// Tusker
//
// Created by Shadowfacts on 2/11/23.
// Copyright © 2023 Shadowfacts. All rights reserved.
//
import UIKit
import Pachyderm
class SuggestedProfilesViewController: UIViewController, CollectionViewController {
weak var mastodonController: MastodonController!
var collectionView: UICollectionView!
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
private var state = State.unloaded
init(mastodonController: MastodonController) {
self.mastodonController = mastodonController
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
title = "Suggested Accounts"
let layout = UICollectionViewCompositionalLayout { [unowned self] sectionIndex, environment in
switch dataSource.sectionIdentifier(for: sectionIndex) {
case nil:
fatalError()
case .loadingIndicator:
var config = UICollectionLayoutListConfiguration(appearance: .grouped)
config.backgroundColor = .appGroupedBackground
config.showsSeparators = false
return .list(using: config, layoutEnvironment: environment)
case .accounts:
let size = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(280))
let item = NSCollectionLayoutItem(layoutSize: size)
let item2 = NSCollectionLayoutItem(layoutSize: size)
let group = NSCollectionLayoutGroup.vertical(layoutSize: size, subitems: [item, item2])
group.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16)
group.interItemSpacing = .fixed(16)
let section = NSCollectionLayoutSection(group: group)
section.interGroupSpacing = 16
section.contentInsets = NSDirectionalEdgeInsets(top: 16, leading: 0, bottom: 16, trailing: 0)
return section
}
}
collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.delegate = self
collectionView.dragDelegate = self
collectionView.backgroundColor = .appGroupedBackground
collectionView.allowsFocus = true
view.addSubview(collectionView)
NSLayoutConstraint.activate([
collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
collectionView.topAnchor.constraint(equalTo: view.topAnchor),
collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
dataSource = createDataSource()
}
private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> {
let loadingCell = UICollectionView.CellRegistration<LoadingCollectionViewCell, Void> { cell, indexPath, itemIdentifier in
cell.indicator.startAnimating()
}
let accountCell = UICollectionView.CellRegistration<SuggestedProfileCardCollectionViewCell, (String, Suggestion.Source)>(cellNib: UINib(nibName: "SuggestedProfileCardCollectionViewCell", bundle: .main)) { cell, indexPath, item in
cell.delegate = self
cell.updateUI(accountID: item.0, source: item.1)
}
return UICollectionViewDiffableDataSource(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in
switch itemIdentifier {
case .loadingIndicator:
return collectionView.dequeueConfiguredReusableCell(using: loadingCell, for: indexPath, item: ())
case .account(let id, let source):
return collectionView.dequeueConfiguredReusableCell(using: accountCell, for: indexPath, item: (id, source))
}
}
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
Task {
await loadInitial()
}
}
@MainActor
private func loadInitial() async {
guard case .unloaded = state else {
return
}
state = .loading
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.loadingIndicator])
snapshot.appendItems([.loadingIndicator])
await dataSource.apply(snapshot)
do {
let request = Client.getSuggestions(limit: 80)
let (suggestions, _) = try await mastodonController.run(request)
await mastodonController.persistentContainer.addAll(accounts: suggestions.map(\.account))
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.accounts])
snapshot.appendItems(suggestions.map { .account($0.account.id, $0.source) })
await dataSource.apply(snapshot)
state = .loaded
} catch {
state = .unloaded
let config = ToastConfiguration(from: error, with: "Error Loading Suggested Accounts", in: self) { [weak self] toast in
toast.dismissToast(animated: true)
await self?.loadInitial()
}
showToast(configuration: config, animated: true)
}
}
}
extension SuggestedProfilesViewController {
enum State {
case unloaded
case loading
case loaded
}
}
extension SuggestedProfilesViewController {
enum Section {
case loadingIndicator
case accounts
}
enum Item: Hashable {
case loadingIndicator
case account(String, Suggestion.Source)
}
}
extension SuggestedProfilesViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool {
if case .account(_, _) = dataSource.itemIdentifier(for: indexPath) {
return true
} else {
return false
}
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
guard case .account(let id, _) = dataSource.itemIdentifier(for: indexPath) else {
return
}
selected(account: id)
}
func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
guard case .account(let id, _) = dataSource.itemIdentifier(for: indexPath),
let cell = collectionView.cellForItem(at: indexPath) else {
return nil
}
return UIContextMenuConfiguration {
ProfileViewController(accountID: id, mastodonController: self.mastodonController)
} actionProvider: { _ in
UIMenu(children: self.actionsForProfile(accountID: id, source: .view(cell)))
}
}
func collectionView(_ collectionView: UICollectionView, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) {
MenuPreviewHelper.willPerformPreviewAction(animator: animator, presenter: self)
}
}
extension SuggestedProfilesViewController: UICollectionViewDragDelegate {
func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
guard case .account(let id, _) = dataSource.itemIdentifier(for: indexPath),
let account = mastodonController.persistentContainer.account(for: id) else {
return []
}
let provider = NSItemProvider(object: account.url as NSURL)
let activity = UserActivityManager.showProfileActivity(id: id, accountID: mastodonController.accountInfo!.id)
provider.registerObject(activity, visibility: .all)
return [UIDragItem(itemProvider: provider)]
}
}
extension SuggestedProfilesViewController: TuskerNavigationDelegate {
var apiController: MastodonController! { mastodonController }
}
extension SuggestedProfilesViewController: MenuActionProvider {
}
extension SuggestedProfilesViewController: ToastableViewController {
}

View File

@ -9,6 +9,7 @@
import UIKit import UIKit
import Pachyderm import Pachyderm
import WebURLFoundationExtras import WebURLFoundationExtras
import Combine
class TrendingHashtagsViewController: UIViewController { class TrendingHashtagsViewController: UIViewController {
@ -17,6 +18,9 @@ class TrendingHashtagsViewController: UIViewController {
private var collectionView: UICollectionView! private var collectionView: UICollectionView!
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>! private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
private var state = State.unloaded
private var confirmLoadMore = PassthroughSubject<Void, Never>()
init(mastodonController: MastodonController) { init(mastodonController: MastodonController) {
self.mastodonController = mastodonController self.mastodonController = mastodonController
@ -32,9 +36,21 @@ class TrendingHashtagsViewController: UIViewController {
title = NSLocalizedString("Trending Hashtags", comment: "trending hashtags screen title") title = NSLocalizedString("Trending Hashtags", comment: "trending hashtags screen title")
view.backgroundColor = .systemGroupedBackground view.backgroundColor = .appGroupedBackground
let config = UICollectionLayoutListConfiguration(appearance: .grouped) var config = UICollectionLayoutListConfiguration(appearance: .grouped)
config.backgroundColor = .appGroupedBackground
config.itemSeparatorHandler = { [unowned self] indexPath, sectionConfig in
guard let item = dataSource.itemIdentifier(for: indexPath) else {
return sectionConfig
}
var config = sectionConfig
if item.hideSeparators {
config.topSeparatorVisibility = .hidden
config.bottomSeparatorVisibility = .hidden
}
return config
}
let layout = UICollectionViewCompositionalLayout.list(using: config) let layout = UICollectionViewCompositionalLayout.list(using: config)
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout) collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight] collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
@ -43,14 +59,24 @@ class TrendingHashtagsViewController: UIViewController {
collectionView.allowsFocus = true collectionView.allowsFocus = true
view.addSubview(collectionView) view.addSubview(collectionView)
let registration = UICollectionView.CellRegistration<TrendingHashtagCollectionViewCell, Hashtag> { cell, indexPath, hashtag in let loadingCell = UICollectionView.CellRegistration<LoadingCollectionViewCell, Void> { cell, indexPath, itemIdentifier in
cell.indicator.startAnimating()
}
let hashtagCell = UICollectionView.CellRegistration<TrendingHashtagCollectionViewCell, Hashtag> { cell, indexPath, hashtag in
cell.updateUI(hashtag: hashtag) cell.updateUI(hashtag: hashtag)
} }
let confirmLoadMoreCell = UICollectionView.CellRegistration<ConfirmLoadMoreCollectionViewCell, Bool> { [unowned self] cell, indexPath, isLoading in
cell.confirmLoadMore = self.confirmLoadMore
cell.isLoading = isLoading
}
dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) { (collectionView, indexPath, item) in dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) { (collectionView, indexPath, item) in
switch item { switch item {
case let .tag(hashtag): case .loadingIndicator:
return collectionView.dequeueConfiguredReusableCell(using: registration, for: indexPath, item: hashtag) return collectionView.dequeueConfiguredReusableCell(using: loadingCell, for: indexPath, item: ())
case .tag(let hashtag):
return collectionView.dequeueConfiguredReusableCell(using: hashtagCell, for: indexPath, item: hashtag)
case .confirmLoadMore(let loading):
return collectionView.dequeueConfiguredReusableCell(using: confirmLoadMoreCell, for: indexPath, item: loading)
} }
} }
} }
@ -58,18 +84,104 @@ class TrendingHashtagsViewController: UIViewController {
override func viewWillAppear(_ animated: Bool) { override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated) super.viewWillAppear(animated)
let request = Client.getTrendingHashtags(limit: 10)
Task { Task {
guard let (hashtags, _) = try? await mastodonController.run(request) else { await loadInitial()
return
}
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.trendingTags])
snapshot.appendItems(hashtags.map { .tag($0) })
await dataSource.apply(snapshot)
} }
} }
private func request(offset: Int?) -> Request<[Hashtag]> {
if mastodonController.instanceFeatures.hasMastodonVersion(3, 5, 0) {
return Client.getTrendingHashtags(offset: offset)
} else {
return Client.getTrendingHashtagsDeprecated(offset: offset)
}
}
@MainActor
private func loadInitial() async {
guard case .unloaded = state else {
return
}
state = .loadingInitial
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.trendingTags])
snapshot.appendItems([.loadingIndicator])
await dataSource.apply(snapshot)
do {
let request = self.request(offset: nil)
let (hashtags, _) = try await mastodonController.run(request)
snapshot.deleteItems([.loadingIndicator])
snapshot.appendItems(hashtags.map { .tag($0) })
state = .loaded
await dataSource.apply(snapshot)
} catch {
snapshot.deleteItems([.loadingIndicator])
await dataSource.apply(snapshot)
state = .unloaded
let config = ToastConfiguration(from: error, with: "Error Loading Trending Tags", in: self) { [weak self] toast in
toast.dismissToast(animated: true)
await self?.loadInitial()
}
self.showToast(configuration: config, animated: true)
}
}
@MainActor
private func loadOlder() async {
guard case .loaded = state else {
return
}
state = .loadingOlder
let origSnapshot = dataSource.snapshot()
var snapshot = origSnapshot
if Preferences.shared.disableInfiniteScrolling {
snapshot.appendItems([.confirmLoadMore(false)])
await dataSource.apply(snapshot)
for await _ in confirmLoadMore.values {
break
}
snapshot.deleteItems([.confirmLoadMore(false)])
snapshot.appendItems([.confirmLoadMore(true)])
await dataSource.apply(snapshot, animatingDifferences: false)
} else {
snapshot.appendItems([.loadingIndicator])
await dataSource.apply(snapshot)
}
do {
let request = self.request(offset: snapshot.itemIdentifiers.count - 1)
let (hashtags, _) = try await mastodonController.run(request)
var snapshot = origSnapshot
snapshot.appendItems(hashtags.map { .tag($0) })
await dataSource.apply(snapshot)
} catch {
await dataSource.apply(origSnapshot)
let config = ToastConfiguration(from: error, with: "Error Loading More Tags", in: self) { [weak self] toast in
toast.dismissToast(animated: true)
await self?.loadOlder()
}
self.showToast(configuration: config, animated: true)
}
state = .loaded
}
}
extension TrendingHashtagsViewController {
enum State {
case unloaded
case loadingInitial
case loaded
case loadingOlder
}
} }
extension TrendingHashtagsViewController { extension TrendingHashtagsViewController {
@ -77,11 +189,45 @@ extension TrendingHashtagsViewController {
case trendingTags case trendingTags
} }
enum Item: Hashable { enum Item: Hashable {
case loadingIndicator
case tag(Hashtag) case tag(Hashtag)
case confirmLoadMore(Bool)
var hideSeparators: Bool {
switch self {
case .loadingIndicator:
return true
case .tag(_):
return false
case .confirmLoadMore(_):
return false
}
}
var shouldSelect: Bool {
if case .tag(_) = self {
return true
} else {
return false
}
}
} }
} }
extension TrendingHashtagsViewController: UICollectionViewDelegate { extension TrendingHashtagsViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
if indexPath.section == collectionView.numberOfSections - 1,
indexPath.row == collectionView.numberOfItems(inSection: indexPath.section) - 1 {
Task {
await self.loadOlder()
}
}
}
func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool {
return dataSource.itemIdentifier(for: indexPath)?.shouldSelect ?? false
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
guard let item = dataSource.itemIdentifier(for: indexPath), guard let item = dataSource.itemIdentifier(for: indexPath),
case let .tag(hashtag) = item else { case let .tag(hashtag) = item else {

View File

@ -21,6 +21,29 @@ class TrendingLinkCardCollectionViewCell: UICollectionViewCell {
@IBOutlet weak var providerLabel: UILabel! @IBOutlet weak var providerLabel: UILabel!
@IBOutlet weak var activityLabel: UILabel! @IBOutlet weak var activityLabel: UILabel!
@IBOutlet weak var historyView: TrendHistoryView! @IBOutlet weak var historyView: TrendHistoryView!
private var thumbnailAspectRatioConstraint: NSLayoutConstraint?
var verticalSize: VerticalSize! {
didSet {
guard oldValue != verticalSize else {
return
}
switch verticalSize {
case nil:
fatalError()
case .regular:
thumbnailAspectRatioConstraint?.isActive = false
thumbnailAspectRatioConstraint = thumbnailView.widthAnchor.constraint(equalTo: thumbnailView.heightAnchor, multiplier: 4/3)
thumbnailAspectRatioConstraint!.isActive = true
descriptionLabel.numberOfLines = 3
case .compact:
thumbnailAspectRatioConstraint?.isActive = false
thumbnailAspectRatioConstraint = thumbnailView.widthAnchor.constraint(equalTo: thumbnailView.heightAnchor, multiplier: 2/1)
thumbnailAspectRatioConstraint!.isActive = true
descriptionLabel.numberOfLines = 5
}
}
}
private var hoverGestureAnimator: UIViewPropertyAnimator? private var hoverGestureAnimator: UIViewPropertyAnimator?
@ -34,8 +57,11 @@ class TrendingLinkCardCollectionViewCell: UICollectionViewCell {
layer.shadowOffset = .zero layer.shadowOffset = .zero
layer.masksToBounds = false layer.masksToBounds = false
contentView.layer.cornerRadius = 12.5 contentView.layer.cornerRadius = 12.5
contentView.backgroundColor = .appGroupedCellBackground
updateLayerColors() updateLayerColors()
verticalSize = .regular
addGestureRecognizer(UIHoverGestureRecognizer(target: self, action: #selector(hoverRecognized))) addGestureRecognizer(UIHoverGestureRecognizer(target: self, action: #selector(hoverRecognized)))
} }
@ -136,3 +162,9 @@ class TrendingLinkCardCollectionViewCell: UICollectionViewCell {
} }
} }
extension TrendingLinkCardCollectionViewCell {
enum VerticalSize {
case regular, compact
}
}

View File

@ -21,7 +21,7 @@
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="h3b-Mf-lD6" customClass="CachedImageView" customModule="Tusker" customModuleProvider="target"> <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="h3b-Mf-lD6" customClass="CachedImageView" customModule="Tusker" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="300" height="225"/> <rect key="frame" x="0.0" y="0.0" width="300" height="225"/>
<constraints> <constraints>
<constraint firstAttribute="width" secondItem="h3b-Mf-lD6" secondAttribute="height" multiplier="4:3" id="QDY-8a-LYC"/> <constraint firstAttribute="width" secondItem="h3b-Mf-lD6" secondAttribute="height" multiplier="4:3" placeholder="YES" id="QDY-8a-LYC"/>
</constraints> </constraints>
</imageView> </imageView>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="top" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="LpU-m4-guC"> <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="top" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="LpU-m4-guC">
@ -56,9 +56,9 @@
</constraints> </constraints>
</view> </view>
<visualEffectView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="cWo-9n-z42"> <visualEffectView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="cWo-9n-z42">
<rect key="frame" x="0.0" y="196.33333333333334" width="300" height="28.666666666666657"/> <rect key="frame" x="0.0" y="196.66666666666666" width="300" height="28.333333333333343"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="ktv-3s-cp9"> <view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="ktv-3s-cp9">
<rect key="frame" x="0.0" y="0.0" width="300" height="28.666666666666657"/> <rect key="frame" x="0.0" y="0.0" width="300" height="28.333333333333343"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews> <subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsLetterSpacingToFitWidth="YES" showsExpansionTextWhenTruncated="YES" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Ho3-cU-IGi"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsLetterSpacingToFitWidth="YES" showsExpansionTextWhenTruncated="YES" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Ho3-cU-IGi">

View File

@ -80,6 +80,16 @@ class TrendingLinkTableViewCell: UITableViewCell {
thumbnailView.layer.cornerRadius = 0.05 * thumbnailView.bounds.width thumbnailView.layer.cornerRadius = 0.05 * thumbnailView.bounds.width
} }
override func updateConfiguration(using state: UICellConfigurationState) {
var config = UIBackgroundConfiguration.listGroupedCell().updated(for: state)
if state.isHighlighted || state.isSelected {
config.backgroundColor = .appSelectedCellBackground
} else {
config.backgroundColor = .appGroupedCellBackground
}
backgroundConfiguration = config
}
func updateUI(card: Card) { func updateUI(card: Card) {
self.card = card self.card = card
self.thumbnailView.image = nil self.thumbnailView.image = nil

View File

@ -10,19 +10,22 @@ import UIKit
import Pachyderm import Pachyderm
import WebURLFoundationExtras import WebURLFoundationExtras
import SafariServices import SafariServices
import Combine
class TrendingLinksViewController: EnhancedTableViewController { class TrendingLinksViewController: UIViewController, CollectionViewController {
weak var mastodonController: MastodonController! weak var mastodonController: MastodonController!
private var dataSource: UITableViewDiffableDataSource<Section, Item>! var collectionView: UICollectionView!
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
private var state = State.unloaded
private let confirmLoadMore = PassthroughSubject<Void, Never>()
init(mastodonController: MastodonController) { init(mastodonController: MastodonController) {
self.mastodonController = mastodonController self.mastodonController = mastodonController
super.init(style: .grouped) super.init(nibName: nil, bundle: nil)
dragEnabled = true
} }
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
@ -34,72 +37,259 @@ class TrendingLinksViewController: EnhancedTableViewController {
title = NSLocalizedString("Trending Links", comment: "trending links screen title") title = NSLocalizedString("Trending Links", comment: "trending links screen title")
tableView.register(TrendingLinkTableViewCell.self, forCellReuseIdentifier: "trendingLinkCell") let layout = UICollectionViewCompositionalLayout { [unowned self] sectionIndex, environment in
tableView.estimatedRowHeight = 100 switch dataSource.sectionIdentifier(for: sectionIndex) {
tableView.allowsFocus = true case nil:
fatalError()
dataSource = UITableViewDiffableDataSource(tableView: tableView, cellProvider: { tableView, indexPath, item in case .loadingIndicator:
let cell = tableView.dequeueReusableCell(withIdentifier: "trendingLinkCell", for: indexPath) as! TrendingLinkTableViewCell var config = UICollectionLayoutListConfiguration(appearance: .grouped)
cell.updateUI(card: item.card) config.backgroundColor = .appGroupedBackground
return cell config.showsSeparators = false
}) return .list(using: config, layoutEnvironment: environment)
case .links:
let size = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(280))
let item = NSCollectionLayoutItem(layoutSize: size)
let item2 = NSCollectionLayoutItem(layoutSize: size)
let group = NSCollectionLayoutGroup.vertical(layoutSize: size, subitems: [item, item2])
group.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16)
group.interItemSpacing = .fixed(16)
let section = NSCollectionLayoutSection(group: group)
section.interGroupSpacing = 16
section.contentInsets = NSDirectionalEdgeInsets(top: 16, leading: 0, bottom: 16, trailing: 0)
return section
}
}
collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.delegate = self
collectionView.dragDelegate = self
collectionView.backgroundColor = .appGroupedBackground
collectionView.allowsFocus = true
view.addSubview(collectionView)
NSLayoutConstraint.activate([
collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
collectionView.topAnchor.constraint(equalTo: view.topAnchor),
collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
dataSource = createDataSource()
}
private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> {
let loadingCell = UICollectionView.CellRegistration<LoadingCollectionViewCell, Void> { cell, indexPath, itemIdentifier in
cell.indicator.startAnimating()
}
let linkCell = UICollectionView.CellRegistration<TrendingLinkCardCollectionViewCell, Card>(cellNib: UINib(nibName: "TrendingLinkCardCollectionViewCell", bundle: .main)) { cell, indexPath, item in
cell.verticalSize = .compact
cell.updateUI(card: item)
}
let confirmLoadMoreCell = UICollectionView.CellRegistration<ConfirmLoadMoreCollectionViewCell, Bool> { cell, indexPath, isLoading in
cell.confirmLoadMore = self.confirmLoadMore
cell.isLoading = isLoading
}
return UICollectionViewDiffableDataSource(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in
switch itemIdentifier {
case .loadingIndicator:
return collectionView.dequeueConfiguredReusableCell(using: loadingCell, for: indexPath, item: ())
case .link(let card):
return collectionView.dequeueConfiguredReusableCell(using: linkCell, for: indexPath, item: card)
case .confirmLoadMore(let loading):
return collectionView.dequeueConfiguredReusableCell(using: confirmLoadMoreCell, for: indexPath, item: loading)
}
}
} }
override func viewWillAppear(_ animated: Bool) { override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated) super.viewWillAppear(animated)
let request = Client.getTrendingLinks()
Task { Task {
guard let (links, _) = try? await mastodonController.run(request) else { await loadInitial()
return
}
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.links])
snapshot.appendItems(links.map(Item.init))
await dataSource.apply(snapshot)
} }
} }
// MARK: - Table View Delegate @MainActor
private func loadInitial() async {
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { guard case .unloaded = state else {
guard let item = dataSource.itemIdentifier(for: indexPath),
let url = URL(item.card.url) else {
return return
} }
selected(url: url) state = .loading
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.loadingIndicator])
snapshot.appendItems([.loadingIndicator])
await dataSource.apply(snapshot)
do {
let request = Client.getTrendingLinks()
let (links, _) = try await mastodonController.run(request)
snapshot.deleteSections([.loadingIndicator])
snapshot.appendSections([.links])
snapshot.appendItems(links.map { .link($0) })
state = .loaded
await dataSource.apply(snapshot)
} catch {
await dataSource.apply(NSDiffableDataSourceSnapshot())
state = .unloaded
let config = ToastConfiguration(from: error, with: "Error Loading Trending Links", in: self) { [weak self] toast in
toast.dismissToast(animated: true)
await self?.loadInitial()
}
self.showToast(configuration: config, animated: true)
}
} }
override func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? { @MainActor
guard let item = dataSource.itemIdentifier(for: indexPath), private func loadOlder() async {
let url = URL(item.card.url) else { guard case .loaded = state else {
return nil return
} }
return UIContextMenuConfiguration(identifier: nil) { state = .loadingOlder
let vc = SFSafariViewController(url: url)
vc.preferredControlTintColor = Preferences.shared.accentColor.color let origSnapshot = dataSource.snapshot()
return vc var snapshot = origSnapshot
} actionProvider: { _ in if Preferences.shared.disableInfiniteScrolling {
return UIMenu(children: self.actionsForTrendingLink(card: item.card)) snapshot.appendSections([.loadingIndicator])
snapshot.appendItems([.confirmLoadMore(false)], toSection: .loadingIndicator)
await dataSource.apply(snapshot)
for await _ in confirmLoadMore.values {
break
}
snapshot.deleteItems([.confirmLoadMore(false)])
snapshot.appendItems([.confirmLoadMore(true)], toSection: .loadingIndicator)
await dataSource.apply(snapshot, animatingDifferences: false)
} else {
snapshot.appendSections([.loadingIndicator])
snapshot.appendItems([.loadingIndicator], toSection: .loadingIndicator)
await dataSource.apply(snapshot)
}
do {
let request = Client.getTrendingLinks(offset: origSnapshot.itemIdentifiers.count)
let (links, _) = try await mastodonController.run(request)
var snapshot = origSnapshot
snapshot.appendItems(links.map { .link($0) }, toSection: .links)
await dataSource.apply(snapshot)
} catch {
await dataSource.apply(origSnapshot)
let config = ToastConfiguration(from: error, with: "Erorr Loading More Links", in: self) { [weak self] toast in
toast.dismissToast(animated: true)
await self?.loadOlder()
}
self.showToast(configuration: config, animated: true)
} }
} }
} }
extension TrendingLinksViewController {
enum State {
case unloaded
case loading
case loaded
case loadingOlder
}
}
extension TrendingLinksViewController { extension TrendingLinksViewController {
enum Section { enum Section {
case loadingIndicator
case links case links
} }
struct Item: Hashable { enum Item: Hashable {
let card: Card case loadingIndicator
case link(Card)
case confirmLoadMore(Bool)
static func ==(lhs: Item, rhs: Item) -> Bool { static func ==(lhs: Item, rhs: Item) -> Bool {
return lhs.card.url == rhs.card.url switch (lhs, rhs) {
case (.loadingIndicator, .loadingIndicator):
return true
case (.link(let a), .link(let b)):
return a.url == b.url
case (.confirmLoadMore(let a), .confirmLoadMore(let b)):
return a == b
default:
return false
}
} }
func hash(into hasher: inout Hasher) { func hash(into hasher: inout Hasher) {
hasher.combine(card.url) switch self {
case .loadingIndicator:
hasher.combine(0)
case .link(let card):
hasher.combine(1)
hasher.combine(card.url)
case .confirmLoadMore(let loading):
hasher.combine(2)
hasher.combine(loading)
}
} }
var shouldSelect: Bool {
if case .link(_) = self {
return true
} else {
return false
}
}
}
}
extension TrendingLinksViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
if indexPath.section == collectionView.numberOfSections - 1,
indexPath.row == collectionView.numberOfItems(inSection: indexPath.section) - 1 {
Task {
await loadOlder()
}
}
}
func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool {
return dataSource.itemIdentifier(for: indexPath)?.shouldSelect ?? false
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
guard case .link(let card) = dataSource.itemIdentifier(for: indexPath),
let url = URL(card.url) else {
return
}
selected(url: url)
}
func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
guard case .link(let card) = dataSource.itemIdentifier(for: indexPath),
let url = URL(card.url),
let cell = collectionView.cellForItem(at: indexPath) else {
return nil
}
return UIContextMenuConfiguration {
let vc = SFSafariViewController(url: url)
vc.preferredControlTintColor = Preferences.shared.accentColor.color
return vc
} actionProvider: { _ in
UIMenu(children: self.actionsForTrendingLink(card: card, source: .view(cell)))
}
}
func collectionView(_ collectionView: UICollectionView, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) {
MenuPreviewHelper.willPerformPreviewAction(animator: animator, presenter: self)
}
}
extension TrendingLinksViewController: UICollectionViewDragDelegate {
func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
guard case .link(let card) = dataSource.itemIdentifier(for: indexPath),
let url = URL(card.url) else {
return []
}
return [UIDragItem(itemProvider: NSItemProvider(object: url as NSURL))]
} }
} }

View File

@ -9,6 +9,7 @@
import UIKit import UIKit
import Pachyderm import Pachyderm
import SafariServices import SafariServices
import Combine
class TrendsViewController: UIViewController, CollectionViewController { class TrendsViewController: UIViewController, CollectionViewController {
@ -18,6 +19,8 @@ class TrendsViewController: UIViewController, CollectionViewController {
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>! private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
private var loadTask: Task<Void, Never>? private var loadTask: Task<Void, Never>?
private var trendingStatusesState = TrendingStatusesState.unloaded
private let confirmLoadMoreStatuses = PassthroughSubject<Void, Never>()
private var isShowingTrends = false private var isShowingTrends = false
private var shouldShowTrends: Bool { private var shouldShowTrends: Bool {
@ -42,9 +45,17 @@ class TrendsViewController: UIViewController, CollectionViewController {
let layout = UICollectionViewCompositionalLayout { [unowned self] sectionIndex, environment in let layout = UICollectionViewCompositionalLayout { [unowned self] sectionIndex, environment in
let sectionIdentifier = self.dataSource.snapshot().sectionIdentifiers[sectionIndex] let sectionIdentifier = self.dataSource.snapshot().sectionIdentifiers[sectionIndex]
switch sectionIdentifier { switch sectionIdentifier {
case .loadingIndicator:
var listConfig = UICollectionLayoutListConfiguration(appearance: .grouped)
listConfig.backgroundColor = .appGroupedBackground
listConfig.showsSeparators = false
return .list(using: listConfig, layoutEnvironment: environment)
case .trendingHashtags: case .trendingHashtags:
var listConfig = UICollectionLayoutListConfiguration(appearance: .grouped) var listConfig = UICollectionLayoutListConfiguration(appearance: .grouped)
listConfig.headerMode = .supplementary listConfig.headerMode = .supplementary
listConfig.footerMode = .supplementary
listConfig.backgroundColor = .appGroupedBackground
return .list(using: listConfig, layoutEnvironment: environment) return .list(using: listConfig, layoutEnvironment: environment)
case .trendingLinks: case .trendingLinks:
@ -56,9 +67,10 @@ class TrendsViewController: UIViewController, CollectionViewController {
let section = NSCollectionLayoutSection(group: group) let section = NSCollectionLayoutSection(group: group)
section.orthogonalScrollingBehavior = .groupPaging section.orthogonalScrollingBehavior = .groupPaging
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),
NSCollectionLayoutBoundarySupplementaryItem(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(30)), elementKind: UICollectionView.elementKindSectionFooter, alignment: .bottomLeading),
] ]
section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 0, bottom: 16, trailing: 0) section.contentInsets = .zero
return section return section
case .profileSuggestions: case .profileSuggestions:
@ -70,14 +82,25 @@ class TrendsViewController: UIViewController, CollectionViewController {
let section = NSCollectionLayoutSection(group: group) let section = NSCollectionLayoutSection(group: group)
section.orthogonalScrollingBehavior = .groupPaging section.orthogonalScrollingBehavior = .groupPaging
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),
NSCollectionLayoutBoundarySupplementaryItem(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(30)), elementKind: UICollectionView.elementKindSectionFooter, alignment: .bottomLeading),
] ]
section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 0, bottom: 16, trailing: 0) section.contentInsets = .zero
return section return section
case .trendingStatuses: case .trendingStatuses:
var listConfig = UICollectionLayoutListConfiguration(appearance: .grouped) var listConfig = UICollectionLayoutListConfiguration(appearance: .grouped)
listConfig.headerMode = .supplementary listConfig.headerMode = .supplementary
listConfig.backgroundColor = .appGroupedBackground
listConfig.itemSeparatorHandler = { [unowned self] indexPath, sectionConfig in
var config = sectionConfig
if let item = dataSource.itemIdentifier(for: indexPath),
item.hideListSeparators {
config.topSeparatorVisibility = .hidden
config.bottomSeparatorVisibility = .hidden
}
return config
}
return .list(using: listConfig, layoutEnvironment: environment) return .list(using: listConfig, layoutEnvironment: environment)
} }
} }
@ -85,7 +108,7 @@ class TrendsViewController: UIViewController, CollectionViewController {
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight] collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
collectionView.delegate = self collectionView.delegate = self
collectionView.dragDelegate = self collectionView.dragDelegate = self
collectionView.backgroundColor = .secondarySystemBackground collectionView.backgroundColor = .appGroupedBackground
collectionView.allowsFocus = true collectionView.allowsFocus = true
view.addSubview(collectionView) view.addSubview(collectionView)
@ -96,19 +119,35 @@ class TrendsViewController: UIViewController, CollectionViewController {
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.sectionIdentifier(for: indexPath.section)!
var config = UIListContentConfiguration.groupedHeader() var config = UIListContentConfiguration.groupedHeader()
config.text = section.title config.text = section.title
headerView.contentConfiguration = config headerView.contentConfiguration = config
} }
let moreCell = UICollectionView.SupplementaryRegistration<MoreTrendsFooterCollectionViewCell>(elementKind: UICollectionView.elementKindSectionFooter) { [unowned self] supplementaryView, elementKind, indexPath in
supplementaryView.delegate = self
switch self.dataSource.sectionIdentifier(for: indexPath.section) {
case nil, .loadingIndicator, .trendingStatuses:
fatalError()
case .trendingHashtags:
supplementaryView.updateUI(.hashtags)
case .trendingLinks:
supplementaryView.updateUI(.links)
case .profileSuggestions:
supplementaryView.updateUI(.profileSuggestions)
}
}
let loadingCell = UICollectionView.CellRegistration<LoadingCollectionViewCell, Void> { cell, indexPath, itemIdentifier in
cell.indicator.startAnimating()
}
let trendingHashtagCell = UICollectionView.CellRegistration<TrendingHashtagCollectionViewCell, Hashtag> { (cell, indexPath, hashtag) in let trendingHashtagCell = UICollectionView.CellRegistration<TrendingHashtagCollectionViewCell, Hashtag> { (cell, indexPath, hashtag) in
cell.updateUI(hashtag: hashtag) cell.updateUI(hashtag: hashtag)
} }
let trendingLinkCell = UICollectionView.CellRegistration<TrendingLinkCardCollectionViewCell, Card>(cellNib: UINib(nibName: "TrendingLinkCardCollectionViewCell", bundle: .main)) { (cell, indexPath, card) in let trendingLinkCell = UICollectionView.CellRegistration<TrendingLinkCardCollectionViewCell, Card>(cellNib: UINib(nibName: "TrendingLinkCardCollectionViewCell", bundle: .main)) { (cell, indexPath, card) in
cell.updateUI(card: card) cell.updateUI(card: card)
} }
let statusCell = UICollectionView.CellRegistration<TimelineStatusCollectionViewCell, (String, CollapseState)> { [unowned self] cell, indexPath, item in let statusCell = UICollectionView.CellRegistration<TrendingStatusCollectionViewCell, (String, CollapseState)> { [unowned self] cell, indexPath, item in
cell.delegate = self cell.delegate = self
// TODO: filter trends // TODO: filter trends
cell.updateUI(statusID: item.0, state: item.1, filterResult: .allow, precomputedContent: nil) cell.updateUI(statusID: item.0, state: item.1, filterResult: .allow, precomputedContent: nil)
@ -117,9 +156,16 @@ class TrendsViewController: UIViewController, CollectionViewController {
cell.delegate = self cell.delegate = self
cell.updateUI(accountID: item.0, source: item.1) cell.updateUI(accountID: item.0, source: item.1)
} }
let confirmLoadMoreCell = UICollectionView.CellRegistration<ConfirmLoadMoreCollectionViewCell, Bool> { [unowned self] cell, indexPath, isLoading in
cell.confirmLoadMore = self.confirmLoadMoreStatuses
cell.isLoading = isLoading
}
let dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) { collectionView, indexPath, item in let dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) { collectionView, indexPath, item in
switch item { switch item {
case .loadingIndicator:
return collectionView.dequeueConfiguredReusableCell(using: loadingCell, for: indexPath, item: ())
case let .tag(hashtag): case let .tag(hashtag):
return collectionView.dequeueConfiguredReusableCell(using: trendingHashtagCell, for: indexPath, item: hashtag) return collectionView.dequeueConfiguredReusableCell(using: trendingHashtagCell, for: indexPath, item: hashtag)
@ -131,11 +177,16 @@ class TrendsViewController: UIViewController, CollectionViewController {
case let .account(id, source): case let .account(id, source):
return collectionView.dequeueConfiguredReusableCell(using: accountCell, for: indexPath, item: (id, source)) return collectionView.dequeueConfiguredReusableCell(using: accountCell, for: indexPath, item: (id, source))
case let .confirmLoadMoreStatuses(loading):
return collectionView.dequeueConfiguredReusableCell(using: confirmLoadMoreCell, for: indexPath, item: loading)
} }
} }
dataSource.supplementaryViewProvider = { (collectionView, elementKind, indexPath) in dataSource.supplementaryViewProvider = { (collectionView, elementKind, indexPath) in
if elementKind == UICollectionView.elementKindSectionHeader { if elementKind == UICollectionView.elementKindSectionHeader {
return collectionView.dequeueConfiguredReusableSupplementary(using: sectionHeaderCell, for: indexPath) return collectionView.dequeueConfiguredReusableSupplementary(using: sectionHeaderCell, for: indexPath)
} else if elementKind == UICollectionView.elementKindSectionFooter {
return collectionView.dequeueConfiguredReusableSupplementary(using: moreCell, for: indexPath)
} else { } else {
return nil return nil
} }
@ -169,6 +220,11 @@ class TrendsViewController: UIViewController, CollectionViewController {
} }
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>() var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.loadingIndicator])
snapshot.appendItems([.loadingIndicator])
await apply(snapshot: snapshot)
snapshot = NSDiffableDataSourceSnapshot()
let hashtagsReq = Client.getTrendingHashtags(limit: 5) let hashtagsReq = Client.getTrendingHashtags(limit: 5)
let hashtags = try? await mastodonController.run(hashtagsReq).0 let hashtags = try? await mastodonController.run(hashtagsReq).0
@ -212,9 +268,60 @@ class TrendsViewController: UIViewController, CollectionViewController {
if !Task.isCancelled { if !Task.isCancelled {
await apply(snapshot: snapshot) await apply(snapshot: snapshot)
if snapshot.sectionIdentifiers.contains(.trendingStatuses) {
self.trendingStatusesState = .loaded
} else {
self.trendingStatusesState = .unloaded
}
} }
} }
@MainActor
private func loadOlderStatuses() async {
guard case .loaded = trendingStatusesState else {
return
}
trendingStatusesState = .loadingOlder
let origSnapshot = dataSource.snapshot()
var snapshot = origSnapshot
if Preferences.shared.disableInfiniteScrolling {
snapshot.appendItems([.confirmLoadMoreStatuses(false)], toSection: .trendingStatuses)
await apply(snapshot: snapshot)
for await _ in confirmLoadMoreStatuses.values {
break
}
snapshot.deleteItems([.confirmLoadMoreStatuses(false)])
snapshot.appendItems([.confirmLoadMoreStatuses(true)], toSection: .trendingStatuses)
await apply(snapshot: snapshot, animatingDifferences: false)
} else {
snapshot.appendItems([.loadingIndicator], toSection: .trendingStatuses)
await apply(snapshot: snapshot)
}
do {
let request = Client.getTrendingStatuses(offset: origSnapshot.itemIdentifiers(inSection: .trendingStatuses).count)
let (statuses, _) = try await mastodonController.run(request)
await mastodonController.persistentContainer.addAll(statuses: statuses)
var snapshot = origSnapshot
snapshot.appendItems(statuses.map { .status($0.id, .unknown) }, toSection: .trendingStatuses)
await apply(snapshot: snapshot)
} catch {
await apply(snapshot: origSnapshot)
let config = ToastConfiguration(from: error, with: "Error Loading More Trending Statuses", in: self) { [weak self] toast in
toast.dismissToast(animated: true)
await self?.loadOlderStatuses()
}
showToast(configuration: config, animated: true)
}
trendingStatusesState = .loaded
}
@objc private func preferencesChanged() { @objc private func preferencesChanged() {
if isShowingTrends != shouldShowTrends { if isShowingTrends != shouldShowTrends {
loadTask?.cancel() loadTask?.cancel()
@ -224,9 +331,9 @@ class TrendsViewController: UIViewController, CollectionViewController {
} }
} }
private func apply(snapshot: NSDiffableDataSourceSnapshot<Section, Item>) async { private func apply(snapshot: NSDiffableDataSourceSnapshot<Section, Item>, animatingDifferences: Bool = true) async {
await Task { @MainActor in await Task { @MainActor in
self.dataSource.apply(snapshot) self.dataSource.apply(snapshot, animatingDifferences: animatingDifferences)
}.value }.value
} }
@ -249,15 +356,26 @@ class TrendsViewController: UIViewController, CollectionViewController {
} }
} }
extension TrendsViewController {
enum TrendingStatusesState {
case unloaded
case loaded
case loadingOlder
}
}
extension TrendsViewController { extension TrendsViewController {
enum Section { enum Section {
case loadingIndicator
case trendingHashtags case trendingHashtags
case trendingLinks case trendingLinks
case profileSuggestions case profileSuggestions
case trendingStatuses case trendingStatuses
var title: String { var title: String? {
switch self { switch self {
case .loadingIndicator:
return nil
case .trendingHashtags: case .trendingHashtags:
return "Trending Hashtags" return "Trending Hashtags"
case .trendingLinks: case .trendingLinks:
@ -270,13 +388,17 @@ extension TrendsViewController {
} }
} }
enum Item: Equatable, Hashable { enum Item: Equatable, Hashable {
case loadingIndicator
case status(String, CollapseState) case status(String, CollapseState)
case tag(Hashtag) case tag(Hashtag)
case link(Card) case link(Card)
case account(String, Suggestion.Source) case account(String, Suggestion.Source)
case confirmLoadMoreStatuses(Bool)
static func == (lhs: Item, rhs: Item) -> Bool { static func == (lhs: Item, rhs: Item) -> Bool {
switch (lhs, rhs) { switch (lhs, rhs) {
case (.loadingIndicator, .loadingIndicator):
return true
case let (.status(a, _), .status(b, _)): case let (.status(a, _), .status(b, _)):
return a == b return a == b
case let (.tag(a), .tag(b)): case let (.tag(a), .tag(b)):
@ -285,6 +407,8 @@ extension TrendsViewController {
return a.url == b.url return a.url == b.url
case let (.account(a, _), .account(b, _)): case let (.account(a, _), .account(b, _)):
return a == b return a == b
case (.confirmLoadMoreStatuses(let a), .confirmLoadMoreStatuses(let b)):
return a == b
default: default:
return false return false
} }
@ -292,6 +416,8 @@ extension TrendsViewController {
func hash(into hasher: inout Hasher) { func hash(into hasher: inout Hasher) {
switch self { switch self {
case .loadingIndicator:
hasher.combine("loadingIndicator")
case let .status(id, _): case let .status(id, _):
hasher.combine("status") hasher.combine("status")
hasher.combine(id) hasher.combine(id)
@ -304,17 +430,54 @@ extension TrendsViewController {
case let .account(id, _): case let .account(id, _):
hasher.combine("account") hasher.combine("account")
hasher.combine(id) hasher.combine(id)
case let .confirmLoadMoreStatuses(loading):
hasher.combine("confirmLoadMoreStatuses")
hasher.combine(loading)
}
}
var shouldSelect: Bool {
switch self {
case .loadingIndicator, .confirmLoadMoreStatuses(_):
return false
default:
return true
}
}
var hideListSeparators: Bool {
switch self {
case .loadingIndicator, .confirmLoadMoreStatuses(_):
return true
default:
return false
} }
} }
} }
} }
extension TrendsViewController: UICollectionViewDelegate { extension TrendsViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
if case .trendingStatuses = dataSource.sectionIdentifier(for: indexPath.section),
indexPath.row == collectionView.numberOfItems(inSection: indexPath.section) - 1 {
Task {
await self.loadOlderStatuses()
}
}
}
func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool {
return dataSource.itemIdentifier(for: indexPath)?.shouldSelect ?? false
}
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
} }
switch item { switch item {
case .loadingIndicator, .confirmLoadMoreStatuses(_):
return
case let .tag(hashtag): case let .tag(hashtag):
show(HashtagTimelineViewController(for: hashtag, mastodonController: mastodonController), sender: nil) show(HashtagTimelineViewController(for: hashtag, mastodonController: mastodonController), sender: nil)
@ -338,6 +501,9 @@ extension TrendsViewController: UICollectionViewDelegate {
} }
switch item { switch item {
case .loadingIndicator, .confirmLoadMoreStatuses(_):
return nil
case let .tag(hashtag): case let .tag(hashtag):
return UIContextMenuConfiguration(identifier: nil) { return UIContextMenuConfiguration(identifier: nil) {
HashtagTimelineViewController(for: hashtag, mastodonController: self.mastodonController) HashtagTimelineViewController(for: hashtag, mastodonController: self.mastodonController)
@ -349,12 +515,13 @@ extension TrendsViewController: UICollectionViewDelegate {
guard let url = URL(card.url) else { guard let url = URL(card.url) else {
return nil return nil
} }
let cell = collectionView.cellForItem(at: indexPath)!
return UIContextMenuConfiguration { return UIContextMenuConfiguration {
let vc = SFSafariViewController(url: url) let vc = SFSafariViewController(url: url)
vc.preferredControlTintColor = Preferences.shared.accentColor.color vc.preferredControlTintColor = Preferences.shared.accentColor.color
return vc return vc
} actionProvider: { _ in } actionProvider: { _ in
UIMenu(children: self.actionsForTrendingLink(card: card)) UIMenu(children: self.actionsForTrendingLink(card: card, source: .view(cell)))
} }
case let .status(id, state): case let .status(id, state):
@ -422,6 +589,9 @@ extension TrendsViewController: UICollectionViewDragDelegate {
return [] return []
} }
switch item { switch item {
case .loadingIndicator, .confirmLoadMoreStatuses(_):
return []
case let .tag(hashtag): case let .tag(hashtag):
guard let url = URL(hashtag.url) else { guard let url = URL(hashtag.url) else {
return [] return []

View File

@ -50,7 +50,7 @@ class FindInstanceViewController: InstanceSelectorTableViewController {
extension FindInstanceViewController: InstanceSelectorTableViewControllerDelegate { extension FindInstanceViewController: InstanceSelectorTableViewControllerDelegate {
func didSelectInstance(url: URL) { func didSelectInstance(url: URL) {
let instanceTimelineController = InstanceTimelineViewController(for: url, parentMastodonController: parentMastodonController!) let instanceTimelineController = InstanceTimelineViewController(for: url, parentMastodonController: parentMastodonController!)
instanceTimelineController.delegate = instanceTimelineDelegate instanceTimelineController.instanceTimelineDelegate = instanceTimelineDelegate
instanceTimelineController.browsingEnabled = false instanceTimelineController.browsingEnabled = false
show(instanceTimelineController, sender: self) show(instanceTimelineController, sender: self)
} }

View File

@ -54,6 +54,7 @@ class EditListAccountsViewController: EnhancedTableViewController {
tableView.rowHeight = UITableView.automaticDimension tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 66 tableView.estimatedRowHeight = 66
tableView.allowsSelection = false tableView.allowsSelection = false
tableView.backgroundColor = .appGroupedBackground
dataSource = DataSource(tableView: tableView, cellProvider: { (tableView, indexPath, item) -> UITableViewCell? in dataSource = DataSource(tableView: tableView, cellProvider: { (tableView, indexPath, item) -> UITableViewCell? in
guard case let .account(id) = item else { fatalError() } guard case let .account(id) = item else { fatalError() }
@ -61,6 +62,15 @@ class EditListAccountsViewController: EnhancedTableViewController {
let cell = tableView.dequeueReusableCell(withIdentifier: "accountCell", for: indexPath) as! AccountTableViewCell let cell = tableView.dequeueReusableCell(withIdentifier: "accountCell", for: indexPath) as! AccountTableViewCell
cell.delegate = self cell.delegate = self
cell.updateUI(accountID: id) cell.updateUI(accountID: id)
cell.configurationUpdateHandler = { cell, state in
var config = UIBackgroundConfiguration.listGroupedCell().updated(for: state)
if state.isHighlighted || state.isSelected {
config.backgroundColor = .appSelectedCellBackground
} else {
config.backgroundColor = .appGroupedCellBackground
}
cell.backgroundConfiguration = config
}
return cell return cell
}) })
dataSource.editListAccountsController = self dataSource.editListAccountsController = self

View File

@ -45,6 +45,7 @@ class LocalPredicateStatusesViewController: UIViewController, CollectionViewCont
override func loadView() { override func loadView() {
var config = UICollectionLayoutListConfiguration(appearance: .plain) var config = UICollectionLayoutListConfiguration(appearance: .plain)
config.backgroundColor = .appBackground
config.leadingSwipeActionsConfigurationProvider = { [unowned self] in config.leadingSwipeActionsConfigurationProvider = { [unowned self] in
(collectionView.cellForItem(at: $0) as? TimelineStatusCollectionViewCell)?.leadingSwipeActions() (collectionView.cellForItem(at: $0) as? TimelineStatusCollectionViewCell)?.leadingSwipeActions()
} }

View File

@ -85,6 +85,7 @@ struct MuteAccountView: View {
Text("This user's posts will be hidden from your timeline. You can still receive notifications from them.") Text("This user's posts will be hidden from your timeline. You can still receive notifications from them.")
} }
} }
.appGroupedListRowBackground()
Section { Section {
Picker(selection: $duration) { Picker(selection: $duration) {
@ -99,6 +100,7 @@ struct MuteAccountView: View {
Text("The mute will automatically be removed after the selected time.") Text("The mute will automatically be removed after the selected time.")
} }
} }
.appGroupedListRowBackground()
Button(action: self.mute) { Button(action: self.mute) {
if isMuting { if isMuting {
@ -113,7 +115,9 @@ struct MuteAccountView: View {
} }
} }
.disabled(isMuting) .disabled(isMuting)
.appGroupedListRowBackground()
} }
.appGroupedListBackground(container: UIHostingController<MuteAccountView>.self)
.alertWithData("Erorr Muting", data: $error, actions: { error in .alertWithData("Erorr Muting", data: $error, actions: { error in
Button("OK") {} Button("OK") {}
}, message: { error in }, message: { error in

View File

@ -58,6 +58,7 @@ class NotificationsTableViewController: DiffableTimelineLikeTableViewController<
tableView.cellLayoutMarginsFollowReadableWidth = UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac tableView.cellLayoutMarginsFollowReadableWidth = UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac
tableView.allowsFocus = true tableView.allowsFocus = true
tableView.backgroundColor = .appBackground
NotificationCenter.default.addObserver(self, selector: #selector(handleStatusDeleted), name: .statusDeleted, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(handleStatusDeleted), name: .statusDeleted, object: nil)
} }

View File

@ -63,6 +63,7 @@ class InstanceSelectorTableViewController: UITableViewController {
appearance.configureWithDefaultBackground() appearance.configureWithDefaultBackground()
navigationItem.scrollEdgeAppearance = appearance navigationItem.scrollEdgeAppearance = appearance
tableView.backgroundColor = .appGroupedBackground
tableView.keyboardDismissMode = .interactive tableView.keyboardDismissMode = .interactive
tableView.register(UINib(nibName: "InstanceTableViewCell", bundle: .main), forCellReuseIdentifier: instanceCell) tableView.register(UINib(nibName: "InstanceTableViewCell", bundle: .main), forCellReuseIdentifier: instanceCell)
tableView.rowHeight = UITableView.automaticDimension tableView.rowHeight = UITableView.automaticDimension
@ -107,6 +108,10 @@ class InstanceSelectorTableViewController: UITableViewController {
} }
.debounce(for: .seconds(1), scheduler: RunLoop.main) .debounce(for: .seconds(1), scheduler: RunLoop.main)
.sink { [weak self] in self?.updateSpecificInstance(domain: $0) } .sink { [weak self] in self?.updateSpecificInstance(domain: $0) }
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
loadRecommendedInstances() loadRecommendedInstances()
} }
@ -203,7 +208,7 @@ class InstanceSelectorTableViewController: UITableViewController {
private func createActivityIndicatorHeader() { private func createActivityIndicatorHeader() {
let header = UITableViewHeaderFooterView() let header = UITableViewHeaderFooterView()
header.translatesAutoresizingMaskIntoConstraints = false header.translatesAutoresizingMaskIntoConstraints = false
header.contentView.backgroundColor = .systemGroupedBackground header.contentView.backgroundColor = .appGroupedBackground
activityIndicator = UIActivityIndicatorView(style: .large) activityIndicator = UIActivityIndicatorView(style: .large)
activityIndicator.translatesAutoresizingMaskIntoConstraints = false activityIndicator.translatesAutoresizingMaskIntoConstraints = false

View File

@ -42,6 +42,7 @@ struct AboutView: View {
} }
} }
} }
.appGroupedListRowBackground()
Section { Section {
Link("Website", destination: URL(string: "https://vaccor.space/tusker")!) Link("Website", destination: URL(string: "https://vaccor.space/tusker")!)
@ -67,7 +68,10 @@ struct AboutView: View {
Link("Source Code", destination: URL(string: "https://git.shadowfacts.net/shadowfacts/Tusker")!) Link("Source Code", destination: URL(string: "https://git.shadowfacts.net/shadowfacts/Tusker")!)
Link("Issue Tracker", destination: URL(string: "https://git.shadowfacts.net/shadowfacts/Tusker/issues")!) Link("Issue Tracker", destination: URL(string: "https://git.shadowfacts.net/shadowfacts/Tusker/issues")!)
} }
.appGroupedListRowBackground()
} }
.listStyle(.insetGrouped)
.appGroupedListBackground(container: PreferencesNavigationController.self)
.sheet(isPresented: $isShowingMailSheet) { .sheet(isPresented: $isShowingMailSheet) {
MailSheet(logData: logData) MailSheet(logData: logData)
} }

View File

@ -14,6 +14,7 @@ struct AcknowledgementsView: View {
Text(text) Text(text)
.padding(.horizontal, 16) .padding(.horizontal, 16)
} }
.appGroupedListBackground(container: PreferencesNavigationController.self)
.navigationTitle("Acknowledgements") .navigationTitle("Acknowledgements")
} }

View File

@ -23,7 +23,8 @@ struct AdvancedPrefsView : View {
errorReportingSection errorReportingSection
cachingSection cachingSection
} }
.listStyle(InsetGroupedListStyle()) .listStyle(.insetGrouped)
.appGroupedListBackground(container: PreferencesNavigationController.self)
.navigationBarTitle(Text("Advanced")) .navigationBarTitle(Text("Advanced"))
} }
@ -50,6 +51,7 @@ struct AdvancedPrefsView : View {
// see FB6838291 // see FB6838291
} }
} }
.appGroupedListRowBackground()
} }
var cloudKitSection: some View { var cloudKitSection: some View {
@ -74,7 +76,9 @@ struct AdvancedPrefsView : View {
Text(String(describing: cloudKitStatus!)) Text(String(describing: cloudKitStatus!))
} }
} }
}.task { }
.appGroupedListRowBackground()
.task {
CKContainer.default().accountStatus { status, error in CKContainer.default().accountStatus { status, error in
if let error { if let error {
Logging.general.error("Unable to get CloudKit status: \(String(describing: error))") Logging.general.error("Unable to get CloudKit status: \(String(describing: error))")
@ -99,6 +103,7 @@ struct AdvancedPrefsView : View {
.lineLimit(nil) .lineLimit(nil)
} }
} }
.appGroupedListRowBackground()
} }
var cachingSection: some View { var cachingSection: some View {
@ -120,7 +125,9 @@ struct AdvancedPrefsView : View {
s += AttributedString("\nMastodon cache size: \(ByteCountFormatter().string(fromByteCount: mastodonCacheSize))") s += AttributedString("\nMastodon cache size: \(ByteCountFormatter().string(fromByteCount: mastodonCacheSize))")
} }
return Text(s) return Text(s)
}.task { }
.appGroupedListRowBackground()
.task {
imageCacheSize = [ imageCacheSize = [
ImageCache.avatars, ImageCache.avatars,
.headers, .headers,

View File

@ -6,21 +6,19 @@
// //
import SwiftUI import SwiftUI
import Combine
struct AppearancePrefsView : View { struct AppearancePrefsView : View {
@ObservedObject var preferences = Preferences.shared @ObservedObject var preferences = Preferences.shared
private var theme: Binding<UIUserInterfaceStyle> = Binding(get: { private var appearanceChangePublisher: some Publisher<Void, Never> {
Preferences.shared.theme preferences.$theme
}, set: { .map { _ in () }
Preferences.shared.theme = $0 .merge(with: preferences.$pureBlackDarkMode.map { _ in () },
NotificationCenter.default.post(name: .themePreferenceChanged, object: nil) preferences.$accentColor.map { _ in () }
}) )
private var accentColor: Binding<Preferences.AccentColor> = Binding { // the prefrence publishers are all willSet, but want to notify after the change, so wait one runloop iteration
Preferences.shared.accentColor .receive(on: DispatchQueue.main)
} set: {
Preferences.shared.accentColor = $0
NotificationCenter.default.post(name: .themePreferenceChanged, object: nil)
} }
private var useCircularAvatars: Binding<Bool> = Binding(get: { private var useCircularAvatars: Binding<Bool> = Binding(get: {
@ -50,19 +48,27 @@ struct AppearancePrefsView : View {
accountsSection accountsSection
postsSection postsSection
} }
.listStyle(InsetGroupedListStyle()) .listStyle(.insetGrouped)
.appGroupedListBackground(container: PreferencesNavigationController.self)
.navigationBarTitle(Text("Appearance")) .navigationBarTitle(Text("Appearance"))
} }
private var themeSection: some View { private var themeSection: some View {
Section { Section {
Picker(selection: theme, label: Text("Theme")) { Picker(selection: $preferences.theme, label: Text("Theme")) {
Text("Use System Theme").tag(UIUserInterfaceStyle.unspecified) Text("Use System Theme").tag(UIUserInterfaceStyle.unspecified)
Text("Light").tag(UIUserInterfaceStyle.light) Text("Light").tag(UIUserInterfaceStyle.light)
Text("Dark").tag(UIUserInterfaceStyle.dark) Text("Dark").tag(UIUserInterfaceStyle.dark)
} }
Picker(selection: accentColor, label: Text("Accent Color")) { // macOS system dark mode isn't pure black, so this isn't necessary
if !ProcessInfo.processInfo.isMacCatalystApp && !ProcessInfo.processInfo.isiOSAppOnMac {
Toggle(isOn: $preferences.pureBlackDarkMode) {
Text("Pure Black Dark Mode")
}
}
Picker(selection: $preferences.accentColor, label: Text("Accent Color")) {
ForEach(accentColorsAndImages, id: \.0.rawValue) { (color, image) in ForEach(accentColorsAndImages, id: \.0.rawValue) { (color, image) in
HStack { HStack {
Text(color.name) Text(color.name)
@ -75,6 +81,10 @@ struct AppearancePrefsView : View {
} }
} }
} }
.onReceive(appearanceChangePublisher) { _ in
NotificationCenter.default.post(name: .themePreferenceChanged, object: nil)
}
.appGroupedListRowBackground()
} }
private var accountsSection: some View { private var accountsSection: some View {
@ -86,6 +96,7 @@ struct AppearancePrefsView : View {
Text("Hide Custom Emoji in Usernames") Text("Hide Custom Emoji in Usernames")
} }
} }
.appGroupedListRowBackground()
} }
private var postsSection: some View { private var postsSection: some View {
@ -113,6 +124,7 @@ struct AppearancePrefsView : View {
.navigationTitle("Trailing Swipe Actions") .navigationTitle("Trailing Swipe Actions")
} }
} }
.appGroupedListRowBackground()
} }
} }

View File

@ -18,7 +18,8 @@ struct BehaviorPrefsView: View {
linksSection linksSection
contentWarningsSection contentWarningsSection
} }
.listStyle(InsetGroupedListStyle()) .listStyle(.insetGrouped)
.appGroupedListBackground(container: PreferencesNavigationController.self)
.navigationBarTitle(Text("Behavior")) .navigationBarTitle(Text("Behavior"))
} }
@ -28,6 +29,7 @@ struct BehaviorPrefsView: View {
Text("Require Confirmation Before Reblogging") Text("Require Confirmation Before Reblogging")
} }
} }
.appGroupedListRowBackground()
} }
private var timelineSection: some View { private var timelineSection: some View {
@ -38,6 +40,7 @@ struct BehaviorPrefsView: View {
} header: { } header: {
Text("Timeline") Text("Timeline")
} }
.appGroupedListRowBackground()
} }
private var linksSection: some View { private var linksSection: some View {
@ -52,6 +55,7 @@ struct BehaviorPrefsView: View {
Text("Always Use Reader Mode in In-App Safari") Text("Always Use Reader Mode in In-App Safari")
}.disabled(!preferences.useInAppSafari) }.disabled(!preferences.useInAppSafari)
} }
.appGroupedListRowBackground()
} }
private var contentWarningsSection: some View { private var contentWarningsSection: some View {
@ -68,6 +72,7 @@ struct BehaviorPrefsView: View {
Text(preferences.expandAllContentWarnings ? "Collapse Posts with Keywords in CWs" : "Expand Posts with Keywords in CWs") Text(preferences.expandAllContentWarnings ? "Collapse Posts with Keywords in CWs" : "Expand Posts with Keywords in CWs")
} }
} }
.appGroupedListRowBackground()
} }
} }

View File

@ -19,7 +19,8 @@ struct ComposingPrefsView: View {
replyingSection replyingSection
writingSection writingSection
} }
.listStyle(InsetGroupedListStyle()) .listStyle(.insetGrouped)
.appGroupedListBackground(container: PreferencesNavigationController.self)
.navigationBarTitle("Composing") .navigationBarTitle("Composing")
} }
@ -51,6 +52,7 @@ struct ComposingPrefsView: View {
} footer: { } footer: {
Text("When starting a reply, Tusker will use your preferred visibility or the visibility of the post to which you're replying, whichever is narrower.") Text("When starting a reply, Tusker will use your preferred visibility or the visibility of the post to which you're replying, whichever is narrower.")
} }
.appGroupedListRowBackground()
} }
var composingSection: some View { var composingSection: some View {
@ -62,6 +64,7 @@ struct ComposingPrefsView: View {
Text("Require Attachment Descriptions") Text("Require Attachment Descriptions")
} }
} }
.appGroupedListRowBackground()
} }
var replyingSection: some View { var replyingSection: some View {
@ -75,6 +78,7 @@ struct ComposingPrefsView: View {
Text("Mention Reblogger") Text("Mention Reblogger")
} }
} }
.appGroupedListRowBackground()
} }
var writingSection: some View { var writingSection: some View {
@ -83,6 +87,7 @@ struct ComposingPrefsView: View {
Text("Show @ and # on Keyboard") Text("Show @ and # on Keyboard")
} }
} }
.appGroupedListRowBackground()
} }
} }

View File

@ -15,7 +15,8 @@ struct MediaPrefsView: View {
List { List {
viewingSection viewingSection
} }
.listStyle(InsetGroupedListStyle()) .listStyle(.insetGrouped)
.appGroupedListBackground(container: PreferencesNavigationController.self)
.navigationBarTitle("Media") .navigationBarTitle("Media")
} }
@ -42,6 +43,7 @@ struct MediaPrefsView: View {
Text("Show Uncropped Media Inline") Text("Show Uncropped Media Inline")
} }
} }
.appGroupedListRowBackground()
} }
} }

View File

@ -23,8 +23,8 @@ struct OppositeCollapseKeywordsView: View {
ZStack { ZStack {
// the background from the grouped ListStyle clips to the safe area, so when the keyboard is hiding/showing // the background from the grouped ListStyle clips to the safe area, so when the keyboard is hiding/showing
// the color behind it can be seen, which looks odd // the color behind it can be seen, which looks odd
Color(UIColor.secondarySystemBackground) // Color(UIColor.secondarySystemBackground)
.edgesIgnoringSafeArea(.bottom) // .edgesIgnoringSafeArea(.bottom)
List { List {
Section(footer: Text("A post matches if its content warning contains the text of a keyword, ignoring case.")) { Section(footer: Text("A post matches if its content warning contains the text of a keyword, ignoring case.")) {
@ -37,9 +37,11 @@ struct OppositeCollapseKeywordsView: View {
FocusableTextField(placeholder: "Add Keyword", text: $valueToAdd, becomeFirstResponder: $makeAddFieldFirstResponder, onCommit: self.addKeyword) FocusableTextField(placeholder: "Add Keyword", text: $valueToAdd, becomeFirstResponder: $makeAddFieldFirstResponder, onCommit: self.addKeyword)
} }
.appGroupedListRowBackground()
} }
.animation(.default, value: keywords.map(\.id)) .animation(.default, value: keywords.map(\.id))
.listStyle(GroupedListStyle()) .listStyle(.grouped)
.appGroupedListBackground(container: PreferencesNavigationController.self)
} }
.onAppear(perform: updateAppearance) .onAppear(perform: updateAppearance)
.navigationBarTitle(preferences.expandAllContentWarnings ? "Collapse Post CW Keywords" : "Expand Post CW Keywords") .navigationBarTitle(preferences.expandAllContentWarnings ? "Collapse Post CW Keywords" : "Expand Post CW Keywords")

View File

@ -24,6 +24,7 @@ struct PreferencesView: View {
aboutSection aboutSection
} }
.listStyle(.insetGrouped) .listStyle(.insetGrouped)
.appGroupedListBackground(container: PreferencesNavigationController.self)
.navigationBarTitle("Preferences") .navigationBarTitle("Preferences")
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
} }
@ -84,6 +85,7 @@ struct PreferencesView: View {
} header: { } header: {
Text("Accounts") Text("Accounts")
} }
.appGroupedListRowBackground()
} }
private var preferencesSection: some View { private var preferencesSection: some View {
@ -107,6 +109,7 @@ struct PreferencesView: View {
Text("Advanced") Text("Advanced")
} }
} }
.appGroupedListRowBackground()
} }
private var aboutSection: some View { private var aboutSection: some View {
@ -121,6 +124,7 @@ struct PreferencesView: View {
AcknowledgementsView() AcknowledgementsView()
} }
} }
.appGroupedListRowBackground()
} }
func logoutPressed() { func logoutPressed() {

View File

@ -42,6 +42,7 @@ class SwipeActionsPrefsViewController: UIViewController, UICollectionViewDelegat
override func loadView() { override func loadView() {
let layout = UICollectionViewCompositionalLayout { [unowned self] sectionIndex, environment in let layout = UICollectionViewCompositionalLayout { [unowned self] sectionIndex, environment in
var config = UICollectionLayoutListConfiguration(appearance: .insetGrouped) var config = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
config.backgroundColor = .appGroupedBackground
if dataSource.sectionIdentifier(for: sectionIndex) == .selected { if dataSource.sectionIdentifier(for: sectionIndex) == .selected {
config.headerMode = .supplementary config.headerMode = .supplementary
} }
@ -59,6 +60,15 @@ class SwipeActionsPrefsViewController: UIViewController, UICollectionViewDelegat
config.image = UIImage(systemName: item.systemImageName) config.image = UIImage(systemName: item.systemImageName)
cell.contentConfiguration = config cell.contentConfiguration = config
cell.accessories = [.reorder(displayed: .always)] cell.accessories = [.reorder(displayed: .always)]
cell.configurationUpdateHandler = { cell, state in
var config = UIBackgroundConfiguration.listGroupedCell().updated(for: state)
if state.isHighlighted || state.isSelected {
config.backgroundColor = .appSelectedCellBackground
} else {
config.backgroundColor = .appGroupedCellBackground
}
cell.backgroundConfiguration = config
}
} }
let dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in let dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in
return collectionView.dequeueConfiguredReusableCell(using: listCell, for: indexPath, item: itemIdentifier) return collectionView.dequeueConfiguredReusableCell(using: listCell, for: indexPath, item: itemIdentifier)

View File

@ -26,8 +26,12 @@ struct TipJarView: View {
@StateObject private var observer = UbiquitousKeyValueStoreObserver() @StateObject private var observer = UbiquitousKeyValueStoreObserver()
var body: some View { var body: some View {
productsView ZStack {
.overlay { Color.appGroupedBackground
.edgesIgnoringSafeArea(.all)
productsView
if showConfetti { if showConfetti {
ConfettiView() ConfettiView()
.transition(.opacity.animation(.default)) .transition(.opacity.animation(.default))

View File

@ -19,7 +19,8 @@ struct WellnessPrefsView: View {
disableInfiniteScrolling disableInfiniteScrolling
hideTrends hideTrends
} }
.listStyle(InsetGroupedListStyle()) .listStyle(.insetGrouped)
.appGroupedListBackground(container: PreferencesNavigationController.self)
.navigationBarTitle(Text("Digital Wellness")) .navigationBarTitle(Text("Digital Wellness"))
} }
@ -29,6 +30,7 @@ struct WellnessPrefsView: View {
Text("Favorite and Reblog Counts in Conversations") Text("Favorite and Reblog Counts in Conversations")
} }
} }
.appGroupedListRowBackground()
} }
private var notificationsMode: some View { private var notificationsMode: some View {
@ -39,6 +41,7 @@ struct WellnessPrefsView: View {
} }
} }
} }
.appGroupedListRowBackground()
} }
private var grayscaleImages: some View { private var grayscaleImages: some View {
@ -47,6 +50,7 @@ struct WellnessPrefsView: View {
Text("Grayscale Images") Text("Grayscale Images")
} }
} }
.appGroupedListRowBackground()
} }
private var disableInfiniteScrolling: some View { private var disableInfiniteScrolling: some View {
@ -55,6 +59,7 @@ struct WellnessPrefsView: View {
Text("Disable Infinite Scrolling") Text("Disable Infinite Scrolling")
} }
} }
.appGroupedListRowBackground()
} }
private var hideTrends: some View { private var hideTrends: some View {
@ -63,6 +68,7 @@ struct WellnessPrefsView: View {
Text("Hide Trends") Text("Hide Trends")
} }
} }
.appGroupedListRowBackground()
} }
} }

View File

@ -23,7 +23,7 @@ class ProfileHeaderCollectionViewCell: UICollectionViewCell {
override init(frame: CGRect) { override init(frame: CGRect) {
super.init(frame: frame) super.init(frame: frame)
contentView.backgroundColor = .systemBackground contentView.backgroundColor = .appBackground
isOpaque = true isOpaque = true
contentView.isOpaque = true contentView.isOpaque = true
} }

View File

@ -56,6 +56,7 @@ class ProfileStatusesViewController: UIViewController, TimelineLikeCollectionVie
override func loadView() { override func loadView() {
var config = UICollectionLayoutListConfiguration(appearance: .plain) var config = UICollectionLayoutListConfiguration(appearance: .plain)
config.backgroundColor = .appBackground
config.leadingSwipeActionsConfigurationProvider = { [unowned self] in config.leadingSwipeActionsConfigurationProvider = { [unowned self] in
(collectionView.cellForItem(at: $0) as? TimelineStatusCollectionViewCell)?.leadingSwipeActions() (collectionView.cellForItem(at: $0) as? TimelineStatusCollectionViewCell)?.leadingSwipeActions()
} }
@ -82,7 +83,9 @@ class ProfileStatusesViewController: UIViewController, TimelineLikeCollectionVie
} }
let layout = UICollectionViewCompositionalLayout { [unowned self] sectionIndex, environment in let layout = UICollectionViewCompositionalLayout { [unowned self] sectionIndex, environment in
if case .header = dataSource.sectionIdentifier(for: sectionIndex) { if case .header = dataSource.sectionIdentifier(for: sectionIndex) {
return .list(using: .init(appearance: .plain), layoutEnvironment: environment) var config = UICollectionLayoutListConfiguration(appearance: .plain)
config.backgroundColor = .appBackground
return .list(using: config, layoutEnvironment: environment)
} else { } else {
let section = NSCollectionLayoutSection.list(using: config, layoutEnvironment: environment) let section = NSCollectionLayoutSection.list(using: config, layoutEnvironment: environment)
if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac { if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {

View File

@ -68,7 +68,7 @@ class ProfileViewController: UIViewController {
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
view.backgroundColor = .systemBackground view.backgroundColor = .appBackground
for pageController in pageControllers { for pageController in pageControllers {
pageController.profileHeaderDelegate = self pageController.profileHeaderDelegate = self

View File

@ -34,25 +34,57 @@ struct ReportAddStatusView: View {
ReportStatusView(status: status, mastodonController: mastodonController) ReportStatusView(status: status, mastodonController: mastodonController)
} }
} }
.appGroupedListRowBackground()
} }
.modifier(ScrollBackgroundModifier())
} else { } else {
ProgressView() ZStack {
.progressViewStyle(.circular) // because the background needs to fill the entire screen
.alertWithData("Error Loading Posts", data: $error, actions: { _ in Color.appGroupedBackground
Button("OK") {} .edgesIgnoringSafeArea(.all)
}, message: { error in
Text(error.localizedDescription) ProgressView()
}) .progressViewStyle(.circular)
.task { @MainActor in .alertWithData("Error Loading Posts", data: $error, actions: { _ in
do { Button("OK") {}
let req = Account.getStatuses(report.accountID, range: .count(40), excludeReplies: false, excludeReblogs: true) }, message: { error in
let (statuses, _) = try await mastodonController.run(req) Text(error.localizedDescription)
await mastodonController.persistentContainer.addAll(statuses: statuses) })
self.statuses = statuses.compactMap { mastodonController.persistentContainer.status(for: $0.id) } .task { @MainActor in
} catch { do {
self.error = error let req = Account.getStatuses(report.accountID, range: .count(40), excludeReplies: false, excludeReblogs: true)
let (statuses, _) = try await mastodonController.run(req)
await mastodonController.persistentContainer.addAll(statuses: statuses)
self.statuses = statuses.compactMap { mastodonController.persistentContainer.status(for: $0.id) }
} catch {
self.error = error
}
} }
} }
}
}
}
private struct ScrollBackgroundModifier: ViewModifier {
@Environment(\.colorScheme) private var colorScheme
func body(content: Content) -> some View {
if #available(iOS 16.0, *) {
content
.scrollContentBackground(.hidden)
.background {
// otherwise the pureBlackDarkMode isn't propagated, for some reason?
// even though it is for ReportSelectRulesView??
let traits: UITraitCollection = {
let t = UITraitCollection(userInterfaceStyle: colorScheme == .dark ? .dark : .light)
t.pureBlackDarkMode = true
return t
}()
Color(uiColor: .appGroupedBackground.resolvedColor(with: traits))
.edgesIgnoringSafeArea(.all)
}
} else {
content
} }
} }
} }

View File

@ -47,11 +47,27 @@ struct ReportSelectRulesView: View {
.foregroundColor(selectedRuleIDs.contains(rule.id) ? .accentColor : .clear) .foregroundColor(selectedRuleIDs.contains(rule.id) ? .accentColor : .clear)
} }
} }
.appGroupedListRowBackground()
} }
.withAppBackgroundIfAvailable()
.navigationTitle("Rules") .navigationTitle("Rules")
} }
} }
private extension View {
@available(iOS, obsoleted: 16.0)
@ViewBuilder
func withAppBackgroundIfAvailable() -> some View {
if #available(iOS 16.0, *) {
self
.scrollContentBackground(.hidden)
.background(Color.appGroupedBackground)
} else {
self
}
}
}
//struct ReportSelectRulesView_Previews: PreviewProvider { //struct ReportSelectRulesView_Previews: PreviewProvider {
// static var previews: some View { // static var previews: some View {
// ReportSelectRulesView() // ReportSelectRulesView()

View File

@ -32,6 +32,7 @@ struct ReportView: View {
if #available(iOS 16.0, *) { if #available(iOS 16.0, *) {
NavigationStack { NavigationStack {
navigationViewContent navigationViewContent
.scrollDismissesKeyboard(.interactively)
} }
} else { } else {
NavigationView { NavigationView {
@ -93,12 +94,14 @@ struct ReportView: View {
} header: { } header: {
Text("Reason") Text("Reason")
} }
.appGroupedListRowBackground()
Section { Section {
ComposeTextView(text: $report.comment, placeholder: Text("Add any additional comments")) ComposeTextView(text: $report.comment, placeholder: Text("Add any additional comments"))
.backgroundColor(.clear) .backgroundColor(.clear)
.listRowInsets(EdgeInsets(top: 8, leading: 8, bottom: 8, trailing: 8)) .listRowInsets(EdgeInsets(top: 8, leading: 8, bottom: 8, trailing: 8))
} }
.appGroupedListRowBackground()
Section { Section {
ForEach(report.statusIDs, id: \.self) { id in ForEach(report.statusIDs, id: \.self) { id in
@ -116,12 +119,14 @@ struct ReportView: View {
} footer: { } footer: {
Text("Attach posts to your report to provide additional context for moderators.") Text("Attach posts to your report to provide additional context for moderators.")
} }
.appGroupedListRowBackground()
Section { Section {
Toggle("Forward", isOn: $report.forward) Toggle("Forward", isOn: $report.forward)
} footer: { } footer: {
Text("You can choose to anonymously forward your report to the moderators of **\(account.url.host!)**.") Text("You can choose to anonymously forward your report to the moderators of **\(account.url.host!)**.")
} }
.appGroupedListRowBackground()
Button(action: self.sendReport) { Button(action: self.sendReport) {
if isReporting { if isReporting {
@ -134,7 +139,10 @@ struct ReportView: View {
} }
} }
.disabled(isReporting) .disabled(isReporting)
.appGroupedListRowBackground()
} }
.listStyle(.insetGrouped)
.appGroupedListBackground(container: UIHostingController<ReportView>.self, applyBackground: true)
.alertWithData("Error Reporting", data: $error, actions: { error in .alertWithData("Error Reporting", data: $error, actions: { error in
Button("OK") {} Button("OK") {}
}, message: { error in }, message: { error in

View File

@ -9,6 +9,7 @@
import UIKit import UIKit
import Combine import Combine
import Pachyderm import Pachyderm
import WebURLFoundationExtras
fileprivate let accountCell = "accountCell" fileprivate let accountCell = "accountCell"
fileprivate let statusCell = "statusCell" fileprivate let statusCell = "statusCell"
@ -26,17 +27,15 @@ extension SearchResultsViewControllerDelegate {
func selectedSearchResult(status statusID: String) {} func selectedSearchResult(status statusID: String) {}
} }
class SearchResultsViewController: EnhancedTableViewController { class SearchResultsViewController: UIViewController, CollectionViewController {
weak var mastodonController: MastodonController! weak var mastodonController: MastodonController!
weak var exploreNavigationController: UINavigationController? weak var exploreNavigationController: UINavigationController?
weak var delegate: SearchResultsViewControllerDelegate? weak var delegate: SearchResultsViewControllerDelegate?
var dataSource: UITableViewDiffableDataSource<Section, Item>! var collectionView: UICollectionView! { view as? UICollectionView }
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
private var activityIndicator: UIActivityIndicatorView!
private var errorLabel: UILabel!
/// Types of results to search for. /// Types of results to search for.
var scope: Scope var scope: Scope
@ -50,9 +49,7 @@ class SearchResultsViewController: EnhancedTableViewController {
self.mastodonController = mastodonController self.mastodonController = mastodonController
self.scope = scope self.scope = scope
super.init(style: .grouped) super.init(nibName: nil, bundle: nil)
dragEnabled = true
title = NSLocalizedString("Search", comment: "search screen title") title = NSLocalizedString("Search", comment: "search screen title")
} }
@ -61,59 +58,45 @@ class SearchResultsViewController: EnhancedTableViewController {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
override func loadView() {
let layout = UICollectionViewCompositionalLayout { [unowned self] sectionIndex, environment in
var config = UICollectionLayoutListConfiguration(appearance: .grouped)
config.backgroundColor = .appGroupedBackground
config.headerMode = .supplementary
switch self.dataSource.sectionIdentifier(for: sectionIndex) {
case .loadingIndicator:
config.showsSeparators = false
config.headerMode = .none
case .statuses:
config.leadingSwipeActionsConfigurationProvider = { [unowned self] in
(self.collectionView.cellForItem(at: $0) as? TimelineStatusCollectionViewCell)?.leadingSwipeActions()
}
config.trailingSwipeActionsConfigurationProvider = { [unowned self] in
(self.collectionView.cellForItem(at: $0) as? TimelineStatusCollectionViewCell)?.trailingSwipeActions()
}
config.separatorConfiguration.topSeparatorInsets = TimelineStatusCollectionViewCell.separatorInsets
config.separatorConfiguration.bottomSeparatorInsets = TimelineStatusCollectionViewCell.separatorInsets
default:
break
}
let section = NSCollectionLayoutSection.list(using: config, layoutEnvironment: environment)
if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
section.contentInsetsReference = .readableContent
}
return section
}
view = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.delegate = self
collectionView.dragDelegate = self
collectionView.allowsFocus = true
collectionView.backgroundColor = .appGroupedBackground
dataSource = createDataSource()
}
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
errorLabel = UILabel()
errorLabel.translatesAutoresizingMaskIntoConstraints = false
errorLabel.font = .preferredFont(forTextStyle: .callout)
errorLabel.textColor = .secondaryLabel
errorLabel.numberOfLines = 0
errorLabel.textAlignment = .center
errorLabel.setContentHuggingPriority(.defaultLow, for: .horizontal)
tableView.addSubview(errorLabel)
NSLayoutConstraint.activate([
errorLabel.centerYAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerYAnchor),
errorLabel.centerXAnchor.constraint(equalTo: tableView.centerXAnchor),
errorLabel.leadingAnchor.constraint(equalToSystemSpacingAfter: tableView.leadingAnchor, multiplier: 1),
tableView.trailingAnchor.constraint(equalToSystemSpacingAfter: errorLabel.trailingAnchor, multiplier: 1),
])
tableView.register(UINib(nibName: "AccountTableViewCell", bundle: .main), forCellReuseIdentifier: accountCell)
tableView.register(UINib(nibName: "TimelineStatusTableViewCell", bundle: .main), forCellReuseIdentifier: statusCell)
tableView.register(UINib(nibName: "HashtagTableViewCell", bundle: .main), forCellReuseIdentifier: hashtagCell)
tableView.allowsFocus = true
dataSource = DataSource(tableView: tableView, cellProvider: { (tableView, indexPath, item) -> UITableViewCell? in
switch item {
case let .account(id):
let cell = tableView.dequeueReusableCell(withIdentifier: accountCell, for: indexPath) as! AccountTableViewCell
cell.delegate = self
cell.updateUI(accountID: id)
return cell
case let .hashtag(tag):
let cell = tableView.dequeueReusableCell(withIdentifier: hashtagCell, for: indexPath) as! HashtagTableViewCell
cell.delegate = self
cell.updateUI(hashtag: tag)
return cell
case let .status(id, state):
let cell = tableView.dequeueReusableCell(withIdentifier: statusCell, for: indexPath) as! TimelineStatusTableViewCell
cell.delegate = self
cell.updateUI(statusID: id, state: state)
return cell
}
})
activityIndicator = UIActivityIndicatorView(style: .large)
activityIndicator.translatesAutoresizingMaskIntoConstraints = false
activityIndicator.isHidden = true
view.addSubview(activityIndicator)
NSLayoutConstraint.activate([
activityIndicator.centerXAnchor.constraint(equalTo: view.centerXAnchor),
activityIndicator.topAnchor.constraint(equalTo: view.topAnchor, constant: 8)
])
_ = searchSubject _ = searchSubject
.debounce(for: .milliseconds(500), scheduler: RunLoop.main) .debounce(for: .milliseconds(500), scheduler: RunLoop.main)
.map { $0?.trimmingCharacters(in: .whitespacesAndNewlines) } .map { $0?.trimmingCharacters(in: .whitespacesAndNewlines) }
@ -125,6 +108,66 @@ class SearchResultsViewController: EnhancedTableViewController {
NotificationCenter.default.addObserver(self, selector: #selector(handleStatusDeleted), name: .statusDeleted, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(handleStatusDeleted), name: .statusDeleted, object: nil)
} }
private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> {
let sectionHeader = UICollectionView.SupplementaryRegistration<UICollectionViewListCell>(elementKind: UICollectionView.elementKindSectionHeader) { [unowned self] supplementaryView, elementKind, indexPath in
let section = self.dataSource.sectionIdentifier(for: indexPath.section)!
var config = UIListContentConfiguration.groupedHeader()
config.text = section.displayName
supplementaryView.contentConfiguration = config
}
let loadingCell = UICollectionView.CellRegistration<LoadingCollectionViewCell, Void> { cell, indexPath, itemIdentifier in
cell.indicator.startAnimating()
}
let accountCell = UICollectionView.CellRegistration<AccountCollectionViewCell, String> { cell, indexPath, itemIdentifier in
cell.delegate = self
cell.updateUI(accountID: itemIdentifier)
}
let hashtagCell = UICollectionView.CellRegistration<TrendingHashtagCollectionViewCell, Hashtag> { cell, indexPath, itemIdentifier in
cell.updateUI(hashtag: itemIdentifier)
}
let statusCell = UICollectionView.CellRegistration<TimelineStatusCollectionViewCell, (String, CollapseState)> { [unowned self] cell, indexPath, itemIdentifier in
cell.delegate = self
cell.updateUI(statusID: itemIdentifier.0, state: itemIdentifier.1, filterResult: .allow, precomputedContent: nil)
}
let dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in
let cell: UICollectionViewCell
switch itemIdentifier {
case .loadingIndicator:
return collectionView.dequeueConfiguredReusableCell(using: loadingCell, for: indexPath, item: ())
case .account(let accountID):
cell = collectionView.dequeueConfiguredReusableCell(using: accountCell, for: indexPath, item: accountID)
case .hashtag(let hashtag):
cell = collectionView.dequeueConfiguredReusableCell(using: hashtagCell, for: indexPath, item: hashtag)
case .status(let id, let state):
cell = collectionView.dequeueConfiguredReusableCell(using: statusCell, for: indexPath, item: (id, state))
}
cell.configurationUpdateHandler = { cell, state in
var config = UIBackgroundConfiguration.listGroupedCell().updated(for: state)
if state.isHighlighted || state.isSelected {
config.backgroundColor = .appSelectedCellBackground
} else {
config.backgroundColor = .appGroupedCellBackground
}
cell.backgroundConfiguration = config
}
return cell
}
dataSource.supplementaryViewProvider = { collectionView, elementKind, indexPath in
if elementKind == UICollectionView.elementKindSectionHeader {
return collectionView.dequeueConfiguredReusableSupplementary(using: sectionHeader, for: indexPath)
} else {
return nil
}
}
return dataSource
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
clearSelectionOnAppear(animated: animated)
}
override func targetViewController(forAction action: Selector, sender: Any?) -> UIViewController? { override func targetViewController(forAction action: Selector, sender: Any?) -> UIViewController? {
// if we're showing a view controller, we need to go up to the explore VC's nav controller // if we're showing a view controller, we need to go up to the explore VC's nav controller
// the UISearchController that is our parent is not part of the normal VC hierarchy and itself doesn't have a parent // the UISearchController that is our parent is not part of the normal VC hierarchy and itself doesn't have a parent
@ -149,25 +192,19 @@ class SearchResultsViewController: EnhancedTableViewController {
} }
self.currentQuery = query self.currentQuery = query
activityIndicator.isHidden = false var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
activityIndicator.startAnimating() snapshot.appendSections([.loadingIndicator])
errorLabel.isHidden = true snapshot.appendItems([.loadingIndicator])
dataSource.apply(snapshot)
let request = Client.search(query: query, types: scope.resultTypes, resolve: true, limit: 10, following: following) let request = Client.search(query: query, types: scope.resultTypes, resolve: true, limit: 10, following: following)
mastodonController.run(request) { (response) in mastodonController.run(request) { (response) in
switch response { switch response {
case let .success(results, _): case let .success(results, _):
guard self.currentQuery == query else { return } guard self.currentQuery == query else { return }
DispatchQueue.main.async {
self.activityIndicator.isHidden = true
self.activityIndicator.stopAnimating()
}
self.showSearchResults(results) self.showSearchResults(results)
case let .failure(error): case let .failure(error):
DispatchQueue.main.async { DispatchQueue.main.async {
self.activityIndicator.isHidden = true
self.activityIndicator.stopAnimating()
self.showSearchError(error) self.showSearchError(error)
} }
} }
@ -195,7 +232,6 @@ class SearchResultsViewController: EnhancedTableViewController {
} }
}, completion: { }, completion: {
DispatchQueue.main.async { DispatchQueue.main.async {
self.errorLabel.isHidden = true
self.dataSource.apply(snapshot) self.dataSource.apply(snapshot)
} }
}) })
@ -205,8 +241,11 @@ class SearchResultsViewController: EnhancedTableViewController {
let snapshot = NSDiffableDataSourceSnapshot<Section, Item>() let snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
dataSource.apply(snapshot) dataSource.apply(snapshot)
errorLabel.isHidden = false let config = ToastConfiguration(from: error, with: "Error Searching", in: self) { [unowned self] toast in
errorLabel.text = error.localizedDescription toast.dismissToast(animated: true)
self.performSearch(query: self.currentQuery)
}
showToast(configuration: config, animated: true)
} }
@objc private func handleStatusDeleted(_ notification: Foundation.Notification) { @objc private func handleStatusDeleted(_ notification: Foundation.Notification) {
@ -230,26 +269,6 @@ class SearchResultsViewController: EnhancedTableViewController {
} }
} }
// MARK: - Table view delegate
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if let delegate = delegate {
switch dataSource.itemIdentifier(for: indexPath) {
case nil:
return
case let .account(id):
delegate.selectedSearchResult(account: id)
case let .hashtag(hashtag):
delegate.selectedSearchResult(hashtag: hashtag)
case let .status(id, _):
delegate.selectedSearchResult(status: id)
}
} else {
super.tableView(tableView, didSelectRowAt: indexPath)
}
}
} }
extension SearchResultsViewController { extension SearchResultsViewController {
@ -289,12 +308,15 @@ extension SearchResultsViewController {
extension SearchResultsViewController { extension SearchResultsViewController {
enum Section: CaseIterable { enum Section: CaseIterable {
case loadingIndicator
case accounts case accounts
case hashtags case hashtags
case statuses case statuses
var displayName: String { var displayName: String? {
switch self { switch self {
case .loadingIndicator:
return nil
case .accounts: case .accounts:
return NSLocalizedString("People", comment: "accounts search results section") return NSLocalizedString("People", comment: "accounts search results section")
case .hashtags: case .hashtags:
@ -305,12 +327,15 @@ extension SearchResultsViewController {
} }
} }
enum Item: Hashable { enum Item: Hashable {
case loadingIndicator
case account(String) case account(String)
case hashtag(Hashtag) case hashtag(Hashtag)
case status(String, CollapseState) case status(String, CollapseState)
func hash(into hasher: inout Hasher) { func hash(into hasher: inout Hasher) {
switch self { switch self {
case .loadingIndicator:
hasher.combine("loadingIndicator")
case let .account(id): case let .account(id):
hasher.combine("account") hasher.combine("account")
hasher.combine(id) hasher.combine(id)
@ -322,16 +347,121 @@ extension SearchResultsViewController {
hasher.combine(id) hasher.combine(id)
} }
} }
static func ==(lhs: Item, rhs: Item) -> Bool {
switch (lhs, rhs) {
case (.loadingIndicator, .loadingIndicator):
return true
case (.account(let a), .account(let b)):
return a == b
case (.hashtag(let a), .hashtag(let b)):
return a.name == b.name
case (.status(let a, _), .status(let b, _)):
return a == b
default:
return false
}
}
}
}
extension SearchResultsViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool {
switch dataSource.itemIdentifier(for: indexPath) {
case .loadingIndicator:
return false
default:
return true
}
} }
class DataSource: UITableViewDiffableDataSource<Section, Item> { func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
override func tableView(_ tableView: UITableView, titleForHeaderInSection sectionIndex: Int) -> String? { switch dataSource.itemIdentifier(for: indexPath) {
let currentSnapshot = snapshot() case nil, .loadingIndicator:
for section in Section.allCases where currentSnapshot.indexOfSection(section) == sectionIndex { return
return section.displayName case let .account(id):
if let delegate {
delegate.selectedSearchResult(account: id)
} else {
selected(account: id)
} }
case let .hashtag(hashtag):
if let delegate {
delegate.selectedSearchResult(hashtag: hashtag)
} else {
selected(tag: hashtag)
}
case let .status(id, state):
if let delegate {
delegate.selectedSearchResult(status: id)
} else {
selected(status: id, state: state.copy())
}
}
}
func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
guard let item = dataSource.itemIdentifier(for: indexPath),
let cell = collectionView.cellForItem(at: indexPath) else {
return nil return nil
} }
switch item {
case .loadingIndicator:
return nil
case .account(let id):
return UIContextMenuConfiguration {
ProfileViewController(accountID: id, mastodonController: self.mastodonController)
} actionProvider: { _ in
UIMenu(children: self.actionsForProfile(accountID: id, source: .view(cell)))
}
case .hashtag(let tag):
return UIContextMenuConfiguration {
HashtagTimelineViewController(for: tag, mastodonController: self.mastodonController)
} actionProvider: { _ in
UIMenu(children: self.actionsForHashtag(tag, source: .view(cell)))
}
case .status(_, _):
return (cell as? TimelineStatusCollectionViewCell)?.contextMenuConfiguration()
}
}
func collectionView(_ collectionView: UICollectionView, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) {
MenuPreviewHelper.willPerformPreviewAction(animator: animator, presenter: self)
}
}
extension SearchResultsViewController: UICollectionViewDragDelegate {
func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
guard let accountInfo = mastodonController.accountInfo,
let item = dataSource.itemIdentifier(for: indexPath) else {
return []
}
let url: URL
let activity: NSUserActivity
switch item {
case .loadingIndicator:
return []
case .account(let id):
guard let account = mastodonController.persistentContainer.account(for: id) else {
return []
}
url = account.url
activity = UserActivityManager.showProfileActivity(id: id, accountID: accountInfo.id)
case .hashtag(let tag):
url = URL(tag.url)!
activity = UserActivityManager.showTimelineActivity(timeline: .tag(hashtag: tag.name), accountID: accountInfo.id)!
case .status(let id, _):
guard let status = mastodonController.persistentContainer.status(for: id),
status.url != nil else {
return []
}
url = status.url!
activity = UserActivityManager.showConversationActivity(mainStatusID: id, accountID: accountInfo.id)
}
activity.displaysAuxiliaryScene = true
let provider = NSItemProvider(object: url as NSURL)
provider.registerObject(activity, visibility: .all)
return [UIDragItem(itemProvider: provider)]
} }
} }
@ -348,8 +478,25 @@ extension SearchResultsViewController: UISearchBarDelegate {
} }
func searchBar(_ searchBar: UISearchBar, selectedScopeButtonIndexDidChange selectedScope: Int) { func searchBar(_ searchBar: UISearchBar, selectedScopeButtonIndexDidChange selectedScope: Int) {
self.scope = Scope.allCases[selectedScope] let newQuery = searchBar.text?.trimmingCharacters(in: .whitespacesAndNewlines)
performSearch(query: searchBar.text?.trimmingCharacters(in: .whitespacesAndNewlines)) let newScope = Scope.allCases[selectedScope]
if self.scope == .all && currentQuery == newQuery {
self.scope = newScope
var snapshot = dataSource.snapshot()
if snapshot.sectionIdentifiers.contains(.accounts) && scope != .people {
snapshot.deleteSections([.accounts])
}
if snapshot.sectionIdentifiers.contains(.hashtags) && scope != .hashtags {
snapshot.deleteSections([.hashtags])
}
if snapshot.sectionIdentifiers.contains(.statuses) && scope != .posts {
snapshot.deleteSections([.statuses])
}
dataSource.apply(snapshot)
} else {
self.scope = newScope
performSearch(query: newQuery)
}
} }
} }
@ -363,9 +510,16 @@ extension SearchResultsViewController: ToastableViewController {
extension SearchResultsViewController: MenuActionProvider { extension SearchResultsViewController: MenuActionProvider {
} }
extension SearchResultsViewController: StatusTableViewCellDelegate { extension SearchResultsViewController: StatusCollectionViewCellDelegate {
func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) { func statusCellNeedsReconfigure(_ cell: StatusCollectionViewCell, animated: Bool, completion: (() -> Void)?) {
tableView.beginUpdates() if let indexPath = collectionView.indexPath(for: cell) {
tableView.endUpdates() var snapshot = dataSource.snapshot()
snapshot.reconfigureItems([dataSource.itemIdentifier(for: indexPath)!])
dataSource.apply(snapshot, animatingDifferences: animated, completion: completion)
}
}
func statusCellShowFiltered(_ cell: StatusCollectionViewCell) {
// not yet supported
} }
} }

View File

@ -86,10 +86,30 @@ class StatusActionAccountListCollectionViewController: UIViewController, Collect
let statusCell = UICollectionView.CellRegistration<TimelineStatusCollectionViewCell, (String, CollapseState)> { [unowned self] cell, indexPath, item in let statusCell = UICollectionView.CellRegistration<TimelineStatusCollectionViewCell, (String, CollapseState)> { [unowned self] cell, indexPath, item in
cell.delegate = self cell.delegate = self
cell.updateUI(statusID: item.0, state: item.1, filterResult: .allow, precomputedContent: nil) cell.updateUI(statusID: item.0, state: item.1, filterResult: .allow, precomputedContent: nil)
cell.configurationUpdateHandler = { cell, state in
var config = UIBackgroundConfiguration.listGroupedCell().updated(for: state)
if state.isHighlighted || state.isSelected {
config.backgroundColor = .appSelectedCellBackground
} else {
config.backgroundColor = .appGroupedCellBackground
}
cell.backgroundConfiguration = config
}
} }
let accountCell = UICollectionView.CellRegistration<AccountCollectionViewCell, String> { [unowned self] cell, indexPath, item in let accountCell = UICollectionView.CellRegistration<AccountCollectionViewCell, String> { [unowned self] cell, indexPath, item in
cell.delegate = self cell.delegate = self
cell.updateUI(accountID: item) cell.updateUI(accountID: item)
cell.configurationUpdateHandler = { cell, state in
var config = UIBackgroundConfiguration.listGroupedCell().updated(for: state)
if state.isHighlighted || state.isSelected {
config.backgroundColor = .appSelectedCellBackground
} else {
config.backgroundColor = .appGroupedCellBackground
}
cell.backgroundConfiguration = config
}
} }
let loadingCell = UICollectionView.CellRegistration<LoadingCollectionViewCell, Void> { cell, indexPath, item in let loadingCell = UICollectionView.CellRegistration<LoadingCollectionViewCell, Void> { cell, indexPath, item in
cell.indicator.startAnimating() cell.indicator.startAnimating()

View File

@ -88,7 +88,7 @@ class StatusActionAccountListViewController: UIViewController {
title = NSLocalizedString("Reblogged By", comment: "status reblogged by accounts list title") title = NSLocalizedString("Reblogged By", comment: "status reblogged by accounts list title")
} }
view.backgroundColor = .systemBackground view.backgroundColor = .appBackground
NotificationCenter.default.addObserver(self, selector: #selector(handleStatusDeleted), name: .statusDeleted, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(handleStatusDeleted), name: .statusDeleted, object: nil)
} }

View File

@ -16,7 +16,7 @@ protocol InstanceTimelineViewControllerDelegate: AnyObject {
class InstanceTimelineViewController: TimelineViewController { class InstanceTimelineViewController: TimelineViewController {
weak var delegate: InstanceTimelineViewControllerDelegate? weak var instanceTimelineDelegate: InstanceTimelineViewControllerDelegate?
weak var parentMastodonController: MastodonController? weak var parentMastodonController: MastodonController?
@ -102,7 +102,7 @@ class InstanceTimelineViewController: TimelineViewController {
} }
collectionView.isHidden = true collectionView.isHidden = true
view.backgroundColor = .systemBackground view.backgroundColor = .appBackground
let image = UIImageView(image: UIImage(systemName: "lock.fill")) let image = UIImageView(image: UIImage(systemName: "lock.fill"))
image.tintColor = .secondaryLabel image.tintColor = .secondaryLabel
@ -145,10 +145,10 @@ class InstanceTimelineViewController: TimelineViewController {
let existing = try? context.fetch(req).first let existing = try? context.fetch(req).first
if let existing = existing { if let existing = existing {
context.delete(existing) context.delete(existing)
delegate?.didUnsaveInstance(url: instanceURL) instanceTimelineDelegate?.didUnsaveInstance(url: instanceURL)
} else { } else {
_ = SavedInstance(url: instanceURL, account: parentMastodonController!.accountInfo!, context: context) _ = SavedInstance(url: instanceURL, account: parentMastodonController!.accountInfo!, context: context)
delegate?.didSaveInstance(url: instanceURL) instanceTimelineDelegate?.didSaveInstance(url: instanceURL)
} }
mastodonController.persistentContainer.save(context: context) mastodonController.persistentContainer.save(context: context)
} }

View File

@ -19,7 +19,7 @@ class TimelineGapCollectionViewCell: UICollectionViewCell {
override var isHighlighted: Bool { override var isHighlighted: Bool {
didSet { didSet {
backgroundColor = isHighlighted ? .systemFill : .systemGroupedBackground backgroundColor = isHighlighted ? .appFill : .appGroupedBackground
} }
} }
@ -38,7 +38,7 @@ class TimelineGapCollectionViewCell: UICollectionViewCell {
override init(frame: CGRect) { override init(frame: CGRect) {
super.init(frame: frame) super.init(frame: frame)
backgroundColor = .systemGroupedBackground backgroundColor = .appGroupedBackground
indicator.isHidden = true indicator.isHidden = true
indicator.color = .tintColor indicator.color = .tintColor

View File

@ -0,0 +1,175 @@
//
// TimelineJumpButton.swift
// Tusker
//
// Created by Shadowfacts on 2/6/23.
// Copyright © 2023 Shadowfacts. All rights reserved.
//
import UIKit
class TimelineJumpButton: UIView {
var action: ((Mode) async -> Void)?
override var intrinsicContentSize: CGSize {
CGSize(width: UIView.noIntrinsicMetric, height: 44)
}
private let button: UIButton = {
var config = UIButton.Configuration.plain()
config.image = UIImage(systemName: "arrow.up")
config.contentInsets = .zero
return UIButton(configuration: config)
}()
private(set) var mode = Mode.jump
var offscreen = false {
didSet {
updateOffscreenTransform()
}
}
private(set) var isSyncing = false
init() {
super.init(frame: .zero)
layer.masksToBounds = true
button.addAction(UIAction(handler: { [unowned self] _ in
Task {
switch self.mode {
case .jump:
await self.jumpAction()
case .sync:
await self.syncAction()
}
}
}), for: .touchUpInside)
button.translatesAutoresizingMaskIntoConstraints = false
addSubview(button)
NSLayoutConstraint.activate([
button.leadingAnchor.constraint(equalTo: leadingAnchor),
button.trailingAnchor.constraint(equalTo: trailingAnchor),
button.topAnchor.constraint(equalTo: topAnchor),
button.bottomAnchor.constraint(equalTo: bottomAnchor),
])
let jumpToPresentAction = UIAction(title: "Jump to Present", image: UIImage(systemName: "arrow.up")) { [unowned self] _ in
Task {
self.setMode(.jump, animated: false)
await self.jumpAction()
}
}
jumpToPresentAction.accessibilityAttributedLabel = TimelinesPageViewController.jumpToPresentTitle
button.menu = UIMenu(children: [
jumpToPresentAction,
UIAction(title: "Sync Position", image: UIImage(systemName: "arrow.triangle.2.circlepath"), handler: { [unowned self] _ in
Task {
self.setMode(.sync, animated: false)
await self.syncAction()
}
})
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
updateOffscreenTransform()
}
private func updateOffscreenTransform() {
if offscreen {
button.transform = CGAffineTransform(translationX: 0, y: button.bounds.height)
} else {
button.transform = .identity
}
}
func setMode(_ mode: Mode, animated: Bool) {
guard self.mode != mode else {
return
}
self.mode = mode
var config = UIButton.Configuration.plain()
config.contentInsets = .zero
switch mode {
case .jump:
config.image = UIImage(systemName: "arrow.up")
case .sync:
config.image = UIImage(systemName: "arrow.triangle.2.circlepath")
}
if animated,
let snapshot = button.snapshotView(afterScreenUpdates: false) {
snapshot.translatesAutoresizingMaskIntoConstraints = false
addSubview(snapshot)
NSLayoutConstraint.activate([
snapshot.centerXAnchor.constraint(equalTo: centerXAnchor),
snapshot.centerYAnchor.constraint(equalTo: centerYAnchor),
])
button.configuration = config
button.layer.opacity = 0
UIView.animate(withDuration: 0.5, delay: 0) {
self.button.layer.opacity = 1
snapshot.layer.opacity = 0
} completion: { _ in
snapshot.removeFromSuperview()
}
} else {
button.configuration = config
}
}
private func jumpAction() async {
button.isUserInteractionEnabled = false
var config = button.configuration!
config.showsActivityIndicator = true
button.configuration = config
await action?(.jump)
config.showsActivityIndicator = false
button.configuration = config
button.isUserInteractionEnabled = true
}
private func syncAction() async {
isSyncing = true
button.isUserInteractionEnabled = false
UIView.animateKeyframes(withDuration: 1, delay: 0, options: .repeat) {
UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.5) {
self.button.imageView!.transform = CGAffineTransform(rotationAngle: 0.5 * .pi)
}
UIView.addKeyframe(withRelativeStartTime: 0.5, relativeDuration: 0.5) {
// the translation is because the symbol isn't perfectly centered
self.button.imageView!.transform = CGAffineTransform(translationX: -0.5, y: 0).rotated(by: .pi)
}
} completion: { _ in
}
await action?(.sync)
button.imageView!.layer.removeAllAnimations()
button.imageView!.transform = .identity
button.isUserInteractionEnabled = true
isSyncing = false
setMode(.jump, animated: true)
}
}
extension TimelineJumpButton {
enum Mode {
case jump
case sync
}
}

View File

@ -11,7 +11,23 @@ import Pachyderm
import Combine import Combine
import Sentry import Sentry
protocol TimelineViewControllerDelegate: AnyObject {
func timelineViewController(_ timelineViewController: TimelineViewController, willShowJumpToPresentToastWith animator: UIViewPropertyAnimator?)
func timelineViewController(_ timelineViewController: TimelineViewController, willDismissJumpToPresentToastWith animator: UIViewPropertyAnimator?)
func timelineViewController(_ timelineViewController: TimelineViewController, willShowSyncToastWith animator: UIViewPropertyAnimator?)
func timelineViewController(_ timelineViewController: TimelineViewController, willDismissSyncToastWith animator: UIViewPropertyAnimator?)
}
extension TimelineViewControllerDelegate {
func timelineViewController(_ timelineViewController: TimelineViewController, willShowJumpToPresentToastWith animator: UIViewPropertyAnimator?) {}
func timelineViewController(_ timelineViewController: TimelineViewController, willDismissJumpToPresentToastWith animator: UIViewPropertyAnimator?) {}
func timelineViewController(_ timelineViewController: TimelineViewController, willShowSyncToastWith animator: UIViewPropertyAnimator?) {}
func timelineViewController(_ timelineViewController: TimelineViewController, willDismissSyncToastWith animator: UIViewPropertyAnimator?) {}
}
class TimelineViewController: UIViewController, TimelineLikeCollectionViewController, CollectionViewController, RefreshableViewController { class TimelineViewController: UIViewController, TimelineLikeCollectionViewController, CollectionViewController, RefreshableViewController {
weak var delegate: TimelineViewControllerDelegate?
let timeline: Timeline let timeline: Timeline
weak var mastodonController: MastodonController! weak var mastodonController: MastodonController!
let filterer: Filterer let filterer: Filterer
@ -63,6 +79,7 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
super.viewDidLoad() super.viewDidLoad()
var config = UICollectionLayoutListConfiguration(appearance: .plain) var config = UICollectionLayoutListConfiguration(appearance: .plain)
config.backgroundColor = .appBackground
config.leadingSwipeActionsConfigurationProvider = { [unowned self] in config.leadingSwipeActionsConfigurationProvider = { [unowned self] in
(collectionView.cellForItem(at: $0) as? TimelineStatusCollectionViewCell)?.leadingSwipeActions() (collectionView.cellForItem(at: $0) as? TimelineStatusCollectionViewCell)?.leadingSwipeActions()
} }
@ -99,6 +116,7 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
collectionView.delegate = self collectionView.delegate = self
collectionView.dragDelegate = self collectionView.dragDelegate = self
collectionView.allowsFocus = true collectionView.allowsFocus = true
collectionView.backgroundColor = .appBackground
collectionView.translatesAutoresizingMaskIntoConstraints = false collectionView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(collectionView) view.addSubview(collectionView)
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
@ -143,7 +161,9 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
} }
.debounce(for: .milliseconds(500), scheduler: DispatchQueue.main) .debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)
.sink { [unowned self] _ in .sink { [unowned self] _ in
_ = syncPositionIfNecessary(alwaysPrompt: true) Task {
_ = await syncPositionIfNecessary(alwaysPrompt: true)
}
} }
.store(in: &cancellables) .store(in: &cancellables)
NotificationCenter.default.addObserver(self, selector: #selector(handleStatusDeleted), name: .statusDeleted, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(handleStatusDeleted), name: .statusDeleted, object: nil)
@ -225,7 +245,7 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
if case .notLoadedInitial = controller.state { if case .notLoadedInitial = controller.state {
Task { Task {
if await restoreState() { if await restoreState() {
await checkPresent(jumpImmediately: false) await checkPresent(jumpImmediately: false, animateImmediateJump: false)
} else { } else {
await controller.loadInitial() await controller.loadInitial()
} }
@ -357,28 +377,12 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
@MainActor @MainActor
private func loadStatusesToRestore(position: TimelinePosition) async -> Bool { private func loadStatusesToRestore(position: TimelinePosition) async -> Bool {
{
let crumb = Breadcrumb(level: .info, category: "TimelineViewController")
crumb.message = "Original statusIDs before filtering"
crumb.data = [
"statusIDs": position.statusIDs,
]
SentrySDK.addBreadcrumb(crumb)
}()
let originalPositionStatusIDs = position.statusIDs let originalPositionStatusIDs = position.statusIDs
let unloaded = position.statusIDs.filter({ mastodonController.persistentContainer.status(for: $0) == nil }) let unloaded = position.statusIDs.filter({ mastodonController.persistentContainer.status(for: $0) == nil })
guard !unloaded.isEmpty else { guard !unloaded.isEmpty else {
return true return true
} }
{
let crumb = Breadcrumb(level: .info, category: "TimelineViewController")
crumb.message = "Unloaded ids"
crumb.data = [
"unloaded": unloaded
]
SentrySDK.addBreadcrumb(crumb)
}()
let results = await withTaskGroup(of: (String, Result<Status, Swift.Error>).self) { group -> [(String, Result<Status, Swift.Error>)] in let results = await withTaskGroup(of: (String, Result<Status, Swift.Error>).self) { group -> [(String, Result<Status, Swift.Error>)] in
for id in unloaded { for id in unloaded {
group.addTask { group.addTask {
@ -399,9 +403,6 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
switch result { switch result {
case .success(let status): case .success(let status):
statuses.append(status) statuses.append(status)
let crumb = Breadcrumb(level: .info, category: "TimelineViewController")
crumb.message = "Loaded status \(id)"
SentrySDK.addBreadcrumb(crumb)
case .failure(let error): case .failure(let error):
let crumb = Breadcrumb(level: .error, category: "TimelineViewController") let crumb = Breadcrumb(level: .error, category: "TimelineViewController")
crumb.message = "Error loading status" crumb.message = "Error loading status"
@ -414,15 +415,6 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
} }
await mastodonController.persistentContainer.addAll(statuses: statuses, in: mastodonController.persistentContainer.viewContext) await mastodonController.persistentContainer.addAll(statuses: statuses, in: mastodonController.persistentContainer.viewContext)
_ = {
let crumb = Breadcrumb(level: .info, category: "TimelineViewController")
crumb.message = "Position statusIDs before filtering"
crumb.data = [
"statusIDs": position.statusIDs,
]
SentrySDK.addBreadcrumb(crumb)
}()
// if an icloud sync completed in between starting to load the statuses and finishing, try to load again // if an icloud sync completed in between starting to load the statuses and finishing, try to load again
if position.statusIDs != originalPositionStatusIDs { if position.statusIDs != originalPositionStatusIDs {
let crumb = Breadcrumb(level: .info, category: "TimelineViewController") let crumb = Breadcrumb(level: .info, category: "TimelineViewController")
@ -444,15 +436,6 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
!unloaded.contains(id) || statuses.contains(where: { $0.id == id }) !unloaded.contains(id) || statuses.contains(where: { $0.id == id })
} }
{
let crumb = Breadcrumb(level: .info, category: "TimelineViewController")
crumb.message = "Filtered position statusIDs"
crumb.data = [
"statusIDs": position.statusIDs,
]
SentrySDK.addBreadcrumb(crumb)
}()
return !position.statusIDs.isEmpty return !position.statusIDs.isEmpty
} }
@ -563,7 +546,7 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
saveState() saveState()
} }
private func syncPositionIfNecessary(alwaysPrompt: Bool) -> Bool { func syncPositionIfNecessary(alwaysPrompt: Bool) async -> Bool {
guard persistsState, guard persistsState,
let timelinePosition = mastodonController.persistentContainer.getTimelinePosition(timeline: timeline) else { let timelinePosition = mastodonController.persistentContainer.getTimelinePosition(timeline: timeline) else {
return false return false
@ -591,9 +574,7 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
} }
stateRestorationLogger.info("Potential restore with centerStatusID: \(timelinePosition.centerStatusID ?? "<none>")") stateRestorationLogger.info("Potential restore with centerStatusID: \(timelinePosition.centerStatusID ?? "<none>")")
if !alwaysPrompt { if !alwaysPrompt {
Task { _ = await restoreState()
_ = await restoreState()
}
} else { } else {
var config = ToastConfiguration(title: "Sync Position") var config = ToastConfiguration(title: "Sync Position")
config.edge = .top config.edge = .top
@ -615,6 +596,12 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
toast.dismissToast(animated: true) toast.dismissToast(animated: true)
} }
} }
config.onAppear = { [unowned self] animator in
self.delegate?.timelineViewController(self, willShowSyncToastWith: animator)
}
config.onDismiss = { [unowned self] animator in
self.delegate?.timelineViewController(self, willDismissSyncToastWith: animator)
}
showToast(configuration: config, animated: true) showToast(configuration: config, animated: true)
UIAccessibility.post(notification: .announcement, argument: "Synced Position Updated") UIAccessibility.post(notification: .announcement, argument: "Synced Position Updated")
} }
@ -652,30 +639,31 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
return return
} }
self.disappearedAt = nil self.disappearedAt = nil
if syncPositionIfNecessary(alwaysPrompt: false) { Task {
// no-op if await syncPositionIfNecessary(alwaysPrompt: false) {
} else { // no-op
Task { } else {
await checkPresent(jumpImmediately: false) await checkPresent(jumpImmediately: false, animateImmediateJump: false)
} }
} }
} }
func checkPresent(jumpImmediately: Bool) async { func checkPresent(jumpImmediately: Bool, animateImmediateJump: Bool) async {
if case .idle = controller.state, guard case .idle = controller.state,
let presentItems = try? await loadInitial(), let presentItems = try? await loadInitial(),
!presentItems.isEmpty { !presentItems.isEmpty else {
if jumpImmediately { return
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>() }
snapshot.appendSections([.statuses]) if jumpImmediately {
snapshot.appendItems(presentItems.map { .status(id: $0, collapseState: .unknown, filterState: .unknown) }, toSection: .statuses) var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
dataSource.apply(snapshot, animatingDifferences: false) { snapshot.appendSections([.statuses])
self.collectionView.scrollToItem(at: IndexPath(row: 0, section: 0), at: .top, animated: false) snapshot.appendItems(presentItems.map { .status(id: $0, collapseState: .unknown, filterState: .unknown) }, toSection: .statuses)
UIAccessibility.post(notification: .screenChanged, argument: self.collectionView.cellForItem(at: IndexPath(row: 0, section: 0))) dataSource.apply(snapshot, animatingDifferences: animateImmediateJump) {
} self.collectionView.scrollToItem(at: IndexPath(row: 0, section: 0), at: .top, animated: animateImmediateJump)
} else { UIAccessibility.post(notification: .screenChanged, argument: self.collectionView.cellForItem(at: IndexPath(row: 0, section: 0)))
insertPresentItemsAndShowJumpToast(presentItems)
} }
} else {
insertPresentItemsAndShowJumpToast(presentItems)
} }
} }
@ -778,6 +766,12 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
self.showToast(configuration: config, animated: true) self.showToast(configuration: config, animated: true)
} }
} }
config.onAppear = { [unowned self] animator in
self.delegate?.timelineViewController(self, willShowJumpToPresentToastWith: animator)
}
config.onDismiss = { [unowned self] animator in
self.delegate?.timelineViewController(self, willDismissJumpToPresentToastWith: animator)
}
self.showToast(configuration: config, animated: true) self.showToast(configuration: config, animated: true)
} }
} }

View File

@ -13,12 +13,22 @@ import Combine
class TimelinesPageViewController: SegmentedPageViewController<TimelinesPageViewController.Page> { class TimelinesPageViewController: SegmentedPageViewController<TimelinesPageViewController.Page> {
static let jumpToPresentTitle: NSAttributedString = {
let s = NSMutableAttributedString("Jump to Present")
// otherwise it pronounces it as 'pɹizˈənt'
// its IPA is also bad, this should be an alveolar approximant not a trill
s.addAttribute(.accessibilitySpeechIPANotation, value: "ˈprɛ.zənt", range: NSRange(location: "Jump to ".count, length: "Present".count))
return s
}()
private let homeTitle = NSLocalizedString("Home", comment: "home timeline tab title") private let homeTitle = NSLocalizedString("Home", comment: "home timeline tab title")
private let federatedTitle = NSLocalizedString("Federated", comment: "federated timeline tab title") private let federatedTitle = NSLocalizedString("Federated", comment: "federated timeline tab title")
private let localTitle = NSLocalizedString("Local", comment: "local timeline tab title") private let localTitle = NSLocalizedString("Local", comment: "local timeline tab title")
weak var mastodonController: MastodonController! weak var mastodonController: MastodonController!
private let jumpButton = TimelineJumpButton()
private var cancellables = Set<AnyCancellable>() private var cancellables = Set<AnyCancellable>()
init(mastodonController: MastodonController) { init(mastodonController: MastodonController) {
@ -46,30 +56,17 @@ class TimelinesPageViewController: SegmentedPageViewController<TimelinesPageView
customizeItem.accessibilityLabel = "Customize Timelines" customizeItem.accessibilityLabel = "Customize Timelines"
navigationItem.rightBarButtonItem = customizeItem navigationItem.rightBarButtonItem = customizeItem
let jumpToPresentName = NSMutableAttributedString("Jump to Present") jumpButton.action = { [unowned self] mode in
// otherwise it pronounces it as 'pɹizˈənt' switch mode {
// its IPA is also bad, this should be an alveolar approximant not a trill case .jump:
jumpToPresentName.addAttribute(.accessibilitySpeechIPANotation, value: "ˈprɛ.zənt", range: NSRange(location: "Jump to ".count, length: "Present".count)) await (self.currentViewController as! TimelineViewController).checkPresent(jumpImmediately: true, animateImmediateJump: true)
segmentedControl.accessibilityCustomActions = [ case .sync:
UIAccessibilityCustomAction(attributedName: jumpToPresentName, actionHandler: { [unowned self] _ in _ = await (self.currentViewController as! TimelineViewController).syncPositionIfNecessary(alwaysPrompt: false)
guard let vc = currentViewController as? TimelineViewController else { }
return false }
} let jumpItem = UIBarButtonItem(customView: jumpButton)
Task { jumpItem.accessibilityAttributedLabel = Self.jumpToPresentTitle
await vc.checkPresent(jumpImmediately: true) navigationItem.leftBarButtonItem = jumpItem
}
return true
}),
UIAccessibilityCustomAction(name: "Jump to Sync Position", actionHandler: { [unowned self] _ in
guard let vc = currentViewController as? TimelineViewController else {
return false
}
Task {
_ = await vc.restoreState()
}
return true
}),
]
mastodonController.accountPreferences.publisher(for: \.pinnedTimelinesData) mastodonController.accountPreferences.publisher(for: \.pinnedTimelinesData)
.map { _ in () } .map { _ in () }
@ -87,6 +84,23 @@ class TimelinesPageViewController: SegmentedPageViewController<TimelinesPageView
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
override func configureViewController(_ viewController: UIViewController) {
let vc = viewController as! TimelineViewController
vc.delegate = self
}
override func selectPage(_ page: Page, animated: Bool) {
super.selectPage(page, animated: animated)
if jumpButton.offscreen {
jumpButton.setMode(.jump, animated: false)
UIView.animate(withDuration: 0.2, delay: 0) {
self.jumpButton.offscreen = false
}
} else {
jumpButton.setMode(.jump, animated: true)
}
}
func selectTimeline(_ timeline: PinnedTimeline, animated: Bool) { func selectTimeline(_ timeline: PinnedTimeline, animated: Bool) {
self.selectPage(Page(mastodonController: mastodonController, timeline: timeline), animated: animated) self.selectPage(Page(mastodonController: mastodonController, timeline: timeline), animated: animated)
} }
@ -136,3 +150,75 @@ extension TimelinesPageViewController {
} }
} }
} }
extension TimelinesPageViewController: TimelineViewControllerDelegate {
func timelineViewController(_ timelineViewController: TimelineViewController, willShowJumpToPresentToastWith animator: UIViewPropertyAnimator?) {
guard timelineViewController === currentViewController else {
return
}
if let animator {
animator.addAnimations {
self.jumpButton.offscreen = true
}
} else {
self.jumpButton.offscreen = true
}
}
func timelineViewController(_ timelineViewController: TimelineViewController, willDismissJumpToPresentToastWith animator: UIViewPropertyAnimator?) {
guard timelineViewController === currentViewController else {
return
}
jumpButton.setMode(.jump, animated: false)
if let animator {
animator.addAnimations {
self.jumpButton.offscreen = false
}
} else {
self.jumpButton.offscreen = false
}
}
func timelineViewController(_ timelineViewController: TimelineViewController, willShowSyncToastWith animator: UIViewPropertyAnimator?) {
guard timelineViewController === currentViewController else {
return
}
if let animator {
animator.addAnimations {
self.jumpButton.offscreen = true
}
} else {
self.jumpButton.offscreen = true
}
}
func timelineViewController(_ timelineViewController: TimelineViewController, willDismissSyncToastWith animator: UIViewPropertyAnimator?) {
guard timelineViewController === currentViewController else {
return
}
jumpButton.setMode(.sync, animated: false)
func resetJumpButtonMode() {
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(5)) { [weak self] in
guard let self,
timelineViewController === self.currentViewController,
!self.jumpButton.isSyncing else { return }
self.jumpButton.setMode(.jump, animated: true)
}
}
if let animator {
animator.addAnimations {
self.jumpButton.offscreen = false
}
animator.addCompletion { position in
if position == .end {
resetJumpButtonMode()
}
}
} else {
self.jumpButton.offscreen = false
resetJumpButtonMode()
}
}
}

View File

@ -47,7 +47,7 @@ class CustomAlertController: UIViewController {
blurView = UIVisualEffectView(effect: UIBlurEffect(style: .systemChromeMaterial)) blurView = UIVisualEffectView(effect: UIBlurEffect(style: .systemChromeMaterial))
blurView.backgroundColor = .systemBackground blurView.backgroundColor = .appBackground
blurView.layer.cornerRadius = 15 blurView.layer.cornerRadius = 15
blurView.layer.cornerCurve = .continuous blurView.layer.cornerCurve = .continuous
blurView.layer.masksToBounds = true blurView.layer.masksToBounds = true

View File

@ -346,12 +346,16 @@ extension MenuActionProvider {
} }
} }
func actionsForTrendingLink(card: Card) -> [UIMenuElement] { func actionsForTrendingLink(card: Card, source: PopoverSource) -> [UIMenuElement] {
guard let url = URL(card.url) else { guard let url = URL(card.url) else {
return [] return []
} }
return [ return [
openInSafariAction(url: url), openInSafariAction(url: url),
createAction(identifier: "share", title: "Share", systemImageName: "square.and.arrow.up", handler: { [weak self] (_) in
guard let self else { return }
self.navigationDelegate?.showMoreOptions(forURL: url, source: source)
}),
createAction(identifier: "postlink", title: "Post this Link", systemImageName: "square.and.pencil", handler: { [weak self] _ in createAction(identifier: "postlink", title: "Post this Link", systemImageName: "square.and.pencil", handler: { [weak self] _ in
guard let self = self else { return } guard let self = self else { return }
let draft = self.mastodonController!.createDraft() let draft = self.mastodonController!.createDraft()

View File

@ -79,7 +79,7 @@ class SegmentedPageViewController<Page: SegmentedPageViewControllerPage>: UIView
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
view.backgroundColor = .systemBackground view.backgroundColor = .appBackground
selectPage(initialPage, animated: false) selectPage(initialPage, animated: false)
@ -94,6 +94,9 @@ class SegmentedPageViewController<Page: SegmentedPageViewControllerPage>: UIView
} }
} }
func configureViewController(_ viewController: UIViewController) {
}
func selectPage(_ page: Page, animated: Bool) { func selectPage(_ page: Page, animated: Bool) {
guard pages.contains(page) else { guard pages.contains(page) else {
fatalError("invalid page \(page) that is not in SegmentedPageViewController.pages") fatalError("invalid page \(page) that is not in SegmentedPageViewController.pages")
@ -116,6 +119,7 @@ class SegmentedPageViewController<Page: SegmentedPageViewControllerPage>: UIView
newController = existing newController = existing
} else { } else {
newController = pageProvider(page) newController = pageProvider(page)
configureViewController(newController)
pageControllers[page] = newController pageControllers[page] = newController
} }
@ -130,7 +134,16 @@ class SegmentedPageViewController<Page: SegmentedPageViewControllerPage>: UIView
animated != .none else { animated != .none else {
currentViewController?.removeViewAndController() currentViewController?.removeViewAndController()
newViewController.view.translatesAutoresizingMaskIntoConstraints = false newViewController.view.translatesAutoresizingMaskIntoConstraints = false
embedChild(newViewController) // don't use embedChild here because it triggers an appearance transition, even though this vc hasn't appeared yet
addChild(newViewController)
view.addSubview(newViewController.view)
NSLayoutConstraint.activate([
newViewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
newViewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
newViewController.view.topAnchor.constraint(equalTo: view.topAnchor),
newViewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
newViewController.didMove(toParent: self)
self.currentViewController = newViewController self.currentViewController = newViewController
return return
} }

View File

@ -12,8 +12,6 @@ import Pachyderm
protocol TuskerNavigationDelegate: UIViewController, ToastableViewController { protocol TuskerNavigationDelegate: UIViewController, ToastableViewController {
var apiController: MastodonController! { get } var apiController: MastodonController! { get }
func conversation(mainStatusID: String, state: CollapseState) -> ConversationViewController
} }
extension TuskerNavigationDelegate { extension TuskerNavigationDelegate {
@ -82,16 +80,12 @@ extension TuskerNavigationDelegate {
} }
} }
func conversation(mainStatusID: String, state: CollapseState) -> ConversationViewController {
return ConversationViewController(for: mainStatusID, state: state, mastodonController: apiController)
}
func selected(status statusID: String) { func selected(status statusID: String) {
self.selected(status: statusID, state: .unknown) self.selected(status: statusID, state: .unknown)
} }
func selected(status statusID: String, state: CollapseState) { func selected(status statusID: String, state: CollapseState) {
show(conversation(mainStatusID: statusID, state: state), sender: self) show(ConversationViewController(for: statusID, state: state, mastodonController: apiController), sender: self)
} }
func compose(editing draft: Draft, animated: Bool = true) { func compose(editing draft: Draft, animated: Bool = true) {
@ -119,15 +113,11 @@ extension TuskerNavigationDelegate {
compose(editing: draft, animated: animated) compose(editing: draft, animated: animated)
} }
func loadingLargeImage(url: URL, cache: ImageCache, description: String?, animatingFrom sourceView: UIImageView) -> LoadingLargeImageViewController { func showLoadingLargeImage(url: URL, cache: ImageCache, description: String?, animatingFrom sourceView: UIImageView) {
let vc = LoadingLargeImageViewController(url: url, cache: cache, imageDescription: description) let vc = LoadingLargeImageViewController(url: url, cache: cache, imageDescription: description)
vc.animationSourceView = sourceView vc.animationSourceView = sourceView
vc.transitioningDelegate = self vc.transitioningDelegate = self
return vc present(vc, animated: true)
}
func showLoadingLargeImage(url: URL, cache: ImageCache, description: String?, animatingFrom sourceView: UIImageView) {
present(loadingLargeImage(url: url, cache: cache, description: description, animatingFrom: sourceView), animated: true)
} }
func gallery(attachments: [Attachment], sourceViews: [UIImageView?], startIndex: Int) -> GalleryViewController { func gallery(attachments: [Attachment], sourceViews: [UIImageView?], startIndex: Int) -> GalleryViewController {
@ -183,16 +173,6 @@ extension TuskerNavigationDelegate {
present(vc, animated: true) present(vc, animated: true)
} }
func showFollowedByList(accountIDs: [String]) {
let vc = AccountListViewController(accountIDs: accountIDs, mastodonController: apiController)
vc.title = NSLocalizedString("Followed By", comment: "followed by accounts list title")
show(vc, sender: self)
}
func statusActionAccountList(action: StatusActionAccountListViewController.ActionType, statusID: String, statusState state: CollapseState, accountIDs: [String]?) -> StatusActionAccountListViewController {
return StatusActionAccountListViewController(actionType: action, statusID: statusID, statusState: state, accountIDs: accountIDs, mastodonController: apiController)
}
} }
enum PopoverSource { enum PopoverSource {

View File

@ -21,6 +21,16 @@ class AlbumTableViewCell: UITableViewCell {
thumbnailImageView.layer.cornerRadius = 0.05 * thumbnailImageView.bounds.width thumbnailImageView.layer.cornerRadius = 0.05 * thumbnailImageView.bounds.width
} }
override func updateConfiguration(using state: UICellConfigurationState) {
var config = UIBackgroundConfiguration.listGroupedCell().updated(for: state)
if state.isHighlighted || state.isSelected {
config.backgroundColor = .appSelectedCellBackground
} else {
config.backgroundColor = .appBackground
}
backgroundConfiguration = config
}
func updateUI(album: PHAssetCollection) { func updateUI(album: PHAssetCollection) {
albumTitleLabel.text = album.localizedTitle albumTitleLabel.text = album.localizedTitle

View File

@ -34,4 +34,14 @@ class AllPhotosTableViewCell: UITableViewCell {
} }
} }
override func updateConfiguration(using state: UICellConfigurationState) {
var config = UIBackgroundConfiguration.listGroupedCell().updated(for: state)
if state.isHighlighted || state.isSelected {
config.backgroundColor = .appSelectedCellBackground
} else {
config.backgroundColor = .appBackground
}
backgroundConfiguration = config
}
} }

View File

@ -28,7 +28,7 @@ class ConfirmLoadMoreCollectionViewCell: UICollectionViewCell {
override init(frame: CGRect) { override init(frame: CGRect) {
super.init(frame: frame) super.init(frame: frame)
backgroundColor = .systemGroupedBackground backgroundColor = .appGroupedBackground
let label = UILabel() let label = UILabel()
label.text = "Infinite scrolling is off. Do you want to keep going?" label.text = "Infinite scrolling is off. Do you want to keep going?"

View File

@ -324,7 +324,7 @@ extension ContentTextView: UIContextMenuInteractionDelegate {
// Create a dummy containerview for the snapshot view, since using a view with a CALayer mask and UIPreviewParameters(textLineRects:) // Create a dummy containerview for the snapshot view, since using a view with a CALayer mask and UIPreviewParameters(textLineRects:)
// causes the mask to be ignored. See FB7832297 // causes the mask to be ignored. See FB7832297
let snapshotContainer = UIView(frame: snapshot.bounds) let snapshotContainer = UIView(frame: snapshot.bounds)
snapshotContainer.backgroundColor = .systemBackground snapshotContainer.backgroundColor = .appBackground
snapshotContainer.addSubview(snapshot) snapshotContainer.addSubview(snapshot)
let preview = UITargetedPreview(view: snapshotContainer, parameters: parameters, target: target) let preview = UITargetedPreview(view: snapshotContainer, parameters: parameters, target: target)

View File

@ -1,32 +0,0 @@
//
// HashtagTableViewCell.swift
// Tusker
//
// Created by Shadowfacts on 9/14/19.
// Copyright © 2019 Shadowfacts. All rights reserved.
//
import UIKit
import Pachyderm
class HashtagTableViewCell: UITableViewCell {
weak var delegate: TuskerNavigationDelegate?
@IBOutlet weak var hashtagLabel: UILabel!
var hashtag: Hashtag!
func updateUI(hashtag: Hashtag) {
self.hashtag = hashtag
hashtagLabel.text = "#\(hashtag.name)"
}
}
extension HashtagTableViewCell: SelectableTableViewCell {
func didSelectCell() {
delegate?.selected(tag: hashtag)
}
}

View File

@ -1,40 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="17701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17703"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<tableViewCell contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" id="KGk-i7-Jjw" customClass="HashtagTableViewCell" customModule="Tusker" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="KGk-i7-Jjw" id="H2p-sc-9uM">
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="#hashtag" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="vVg-1C-zJr">
<rect key="frame" x="16" y="11" width="71.5" height="22"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<constraints>
<constraint firstItem="vVg-1C-zJr" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="topMargin" id="Mnk-L3-dz9"/>
<constraint firstAttribute="bottomMargin" secondItem="vVg-1C-zJr" secondAttribute="bottom" id="qgx-dQ-SGs"/>
<constraint firstItem="vVg-1C-zJr" firstAttribute="leading" secondItem="H2p-sc-9uM" secondAttribute="leadingMargin" id="ulh-dS-m0f"/>
</constraints>
</tableViewCellContentView>
<viewLayoutGuide key="safeArea" id="njF-e1-oar"/>
<connections>
<outlet property="hashtagLabel" destination="vVg-1C-zJr" id="zJM-sN-cvL"/>
</connections>
<point key="canvasLocation" x="132" y="154"/>
</tableViewCell>
</objects>
</document>

View File

@ -18,8 +18,6 @@ class TrendingHashtagCollectionViewCell: UICollectionViewCell {
override init(frame: CGRect) { override init(frame: CGRect) {
super.init(frame: frame) super.init(frame: frame)
backgroundColor = .systemBackground
hashtagLabel.font = .preferredFont(forTextStyle: .title2) hashtagLabel.font = .preferredFont(forTextStyle: .title2)
hashtagLabel.adjustsFontForContentSizeCategory = true hashtagLabel.adjustsFontForContentSizeCategory = true
peopleTodayLabel.font = .preferredFont(forTextStyle: .caption1) peopleTodayLabel.font = .preferredFont(forTextStyle: .caption1)
@ -60,6 +58,16 @@ class TrendingHashtagCollectionViewCell: UICollectionViewCell {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
override func updateConfiguration(using state: UICellConfigurationState) {
var config = UIBackgroundConfiguration.listGroupedCell().updated(for: state)
if state.isHighlighted || state.isSelected {
config.backgroundColor = .appSelectedCellBackground
} else {
config.backgroundColor = .appGroupedCellBackground
}
backgroundConfiguration = config
}
func updateUI(hashtag: Hashtag) { func updateUI(hashtag: Hashtag) {
hashtagLabel.text = "#\(hashtag.name)" hashtagLabel.text = "#\(hashtag.name)"
historyView.setHistory(hashtag.history) historyView.setHistory(hashtag.history)

View File

@ -39,6 +39,16 @@ class InstanceTableViewCell: UITableViewCell {
descriptionTextView.adjustsFontForContentSizeCategory = true descriptionTextView.adjustsFontForContentSizeCategory = true
} }
override func updateConfiguration(using state: UICellConfigurationState) {
var config = UIBackgroundConfiguration.listGroupedCell()
if state.isHighlighted || state.isSelected {
config.backgroundColor = .appSelectedCellBackground
} else {
config.backgroundColor = .appGroupedCellBackground
}
backgroundConfiguration = config
}
func updateUI(instance: InstanceSelector.Instance) { func updateUI(instance: InstanceSelector.Instance) {
self.selectorInstance = instance self.selectorInstance = instance
self.instance = nil self.instance = nil

View File

@ -14,6 +14,8 @@ class LoadingTableViewCell: UITableViewCell {
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier) super.init(style: style, reuseIdentifier: reuseIdentifier)
backgroundColor = .appBackground
indicator.translatesAutoresizingMaskIntoConstraints = false indicator.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(indicator) contentView.addSubview(indicator)
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([

View File

@ -48,6 +48,16 @@ class ActionNotificationGroupTableViewCell: UITableViewCell {
NotificationCenter.default.addObserver(self, selector: #selector(updateUIForPreferences), name: .preferencesChanged, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(updateUIForPreferences), name: .preferencesChanged, object: nil)
} }
override func updateConfiguration(using state: UICellConfigurationState) {
var config = UIBackgroundConfiguration.listPlainCell().updated(for: state)
if state.isHighlighted || state.isSelected {
config.backgroundColor = .appSelectedCellBackground
} else {
config.backgroundColor = .appBackground
}
backgroundConfiguration = config
}
@objc func updateUIForPreferences() { @objc func updateUIForPreferences() {
for case let imageView as UIImageView in actionAvatarStackView.arrangedSubviews { for case let imageView as UIImageView in actionAvatarStackView.arrangedSubviews {
imageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: imageView) imageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: imageView)
@ -263,7 +273,7 @@ extension ActionNotificationGroupTableViewCell: SelectableTableViewCell {
default: default:
fatalError() fatalError()
} }
let vc = delegate.statusActionAccountList(action: action, statusID: statusID, statusState: .unknown, accountIDs: accountIDs) let vc = StatusActionAccountListViewController(actionType: action, statusID: statusID, statusState: .unknown, accountIDs: accountIDs, mastodonController: mastodonController)
vc.showInacurateCountWarning = false vc.showInacurateCountWarning = false
delegate.show(vc) delegate.show(vc)
} }
@ -272,9 +282,6 @@ extension ActionNotificationGroupTableViewCell: SelectableTableViewCell {
extension ActionNotificationGroupTableViewCell: MenuPreviewProvider { extension ActionNotificationGroupTableViewCell: MenuPreviewProvider {
func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? { func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? {
return (content: { return (content: {
guard let delegate = self.delegate else {
return nil
}
let notifications = self.group.notifications let notifications = self.group.notifications
let accountIDs = notifications.map { $0.account.id } let accountIDs = notifications.map { $0.account.id }
let action: StatusActionAccountListViewController.ActionType let action: StatusActionAccountListViewController.ActionType
@ -286,7 +293,7 @@ extension ActionNotificationGroupTableViewCell: MenuPreviewProvider {
default: default:
fatalError() fatalError()
} }
let vc = delegate.statusActionAccountList(action: action, statusID: self.statusID, statusState: .unknown, accountIDs: accountIDs) let vc = StatusActionAccountListViewController(actionType: action, statusID: self.statusID, statusState: .unknown, accountIDs: accountIDs, mastodonController: self.mastodonController)
vc.showInacurateCountWarning = false vc.showInacurateCountWarning = false
return vc return vc
}, actions: { }, actions: {

View File

@ -43,6 +43,16 @@ class FollowNotificationGroupTableViewCell: UITableViewCell {
NotificationCenter.default.addObserver(self, selector: #selector(updateUIForPreferences), name: .preferencesChanged, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(updateUIForPreferences), name: .preferencesChanged, object: nil)
} }
override func updateConfiguration(using state: UICellConfigurationState) {
var config = UIBackgroundConfiguration.listPlainCell().updated(for: state)
if state.isHighlighted || state.isSelected {
config.backgroundColor = .appSelectedCellBackground
} else {
config.backgroundColor = .appBackground
}
backgroundConfiguration = config
}
@objc func updateUIForPreferences() { @objc func updateUIForPreferences() {
for case let imageView as UIImageView in avatarStackView.arrangedSubviews { for case let imageView as UIImageView in avatarStackView.arrangedSubviews {
imageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: imageView) imageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: imageView)
@ -213,7 +223,9 @@ extension FollowNotificationGroupTableViewCell: SelectableTableViewCell {
case 1: case 1:
delegate?.selected(account: accountIDs.first!) delegate?.selected(account: accountIDs.first!)
default: default:
delegate?.showFollowedByList(accountIDs: accountIDs) let vc = AccountListViewController(accountIDs: accountIDs, mastodonController: mastodonController)
vc.title = NSLocalizedString("Followed By", comment: "followed by accounts list title")
delegate?.show(vc)
} }
} }
} }

View File

@ -49,6 +49,16 @@ class FollowRequestNotificationTableViewCell: UITableViewCell {
updateUIForPreferences() updateUIForPreferences()
} }
override func updateConfiguration(using state: UICellConfigurationState) {
var config = UIBackgroundConfiguration.listPlainCell().updated(for: state)
if state.isHighlighted || state.isSelected {
config.backgroundColor = .appSelectedCellBackground
} else {
config.backgroundColor = .appBackground
}
backgroundConfiguration = config
}
@objc func updateUIForPreferences() { @objc func updateUIForPreferences() {
avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * 30 avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * 30

View File

@ -38,6 +38,16 @@ class PollFinishedTableViewCell: UITableViewCell {
displayNameLabel.adjustsFontForContentSizeCategory = true displayNameLabel.adjustsFontForContentSizeCategory = true
} }
override func updateConfiguration(using state: UICellConfigurationState) {
var config = UIBackgroundConfiguration.listPlainCell().updated(for: state)
if state.isHighlighted || state.isSelected {
config.backgroundColor = .appSelectedCellBackground
} else {
config.backgroundColor = .appBackground
}
backgroundConfiguration = config
}
func updateUI(notification: Pachyderm.Notification) { func updateUI(notification: Pachyderm.Notification) {
guard let statusID = notification.status?.id, guard let statusID = notification.status?.id,
let status = delegate?.apiController.persistentContainer.status(for: statusID), let status = delegate?.apiController.persistentContainer.status(for: statusID),
@ -120,8 +130,7 @@ extension PollFinishedTableViewCell: SelectableTableViewCell {
let status = notification?.status else { let status = notification?.status else {
return return
} }
let vc = delegate.conversation(mainStatusID: status.id, state: .unknown) delegate.selected(status: status.id)
delegate.show(vc)
} }
} }
@ -133,7 +142,7 @@ extension PollFinishedTableViewCell: MenuPreviewProvider {
return nil return nil
} }
return (content: { return (content: {
delegate.conversation(mainStatusID: statusID, state: .unknown) ConversationViewController(for: statusID, state: .unknown, mastodonController: delegate.apiController)
}, actions: { }, actions: {
delegate.actionsForStatus(status, source: .view(self)) delegate.actionsForStatus(status, source: .view(self))
}) })

View File

@ -35,6 +35,16 @@ class StatusUpdatedNotificationTableViewCell: UITableViewCell {
displayNameLabel.adjustsFontForContentSizeCategory = true displayNameLabel.adjustsFontForContentSizeCategory = true
} }
override func updateConfiguration(using state: UICellConfigurationState) {
var config = UIBackgroundConfiguration.listPlainCell().updated(for: state)
if state.isHighlighted || state.isSelected {
config.backgroundColor = .appSelectedCellBackground
} else {
config.backgroundColor = .appBackground
}
backgroundConfiguration = config
}
func updateUI(notification: Pachyderm.Notification) { func updateUI(notification: Pachyderm.Notification) {
guard notification.kind == .update, guard notification.kind == .update,
let status = notification.status else { let status = notification.status else {
@ -109,8 +119,7 @@ extension StatusUpdatedNotificationTableViewCell: SelectableTableViewCell {
let status = notification?.status else { let status = notification?.status else {
return return
} }
let vc = delegate.conversation(mainStatusID: status.id, state: .unknown) delegate.selected(status: status.id)
delegate.show(vc)
} }
} }
@ -122,7 +131,7 @@ extension StatusUpdatedNotificationTableViewCell: MenuPreviewProvider {
return nil return nil
} }
return (content: { return (content: {
delegate.conversation(mainStatusID: statusID, state: .unknown) ConversationViewController(for: statusID, state: .unknown, mastodonController: delegate.apiController)
}, actions: { }, actions: {
delegate.actionsForStatus(status, source: .view(self)) delegate.actionsForStatus(status, source: .view(self))
}) })

View File

@ -14,6 +14,8 @@ class ProfileFieldsView: UIView {
weak var delegate: ProfileHeaderViewDelegate? weak var delegate: ProfileHeaderViewDelegate?
private var fields = [Account.Field]()
private let stack = UIStackView() private let stack = UIStackView()
private var fieldViews: [(EmojiLabel, ProfileFieldValueView)] = [] private var fieldViews: [(EmojiLabel, ProfileFieldValueView)] = []
private var fieldConstraints: [NSLayoutConstraint] = [] private var fieldConstraints: [NSLayoutConstraint] = []
@ -62,9 +64,11 @@ class ProfileFieldsView: UIView {
func updateUI(account: AccountMO) { func updateUI(account: AccountMO) {
isHidden = account.fields.isEmpty isHidden = account.fields.isEmpty
guard !account.fields.isEmpty else { guard !account.fields.isEmpty,
fields != account.fields else {
return return
} }
fields = account.fields
for (name, value) in fieldViews { for (name, value) in fieldViews {
name.removeFromSuperview() name.removeFromSuperview()
@ -183,6 +187,7 @@ private class ProfileFieldValueView: UIView {
super.init(frame: .zero) super.init(frame: .zero)
textView.isSelectable = false textView.isSelectable = false
textView.backgroundColor = .clear
textView.defaultFont = .preferredFont(forTextStyle: .body) textView.defaultFont = .preferredFont(forTextStyle: .body)
textView.monospaceFont = UIFontMetrics.default.scaledFont(for: .monospacedSystemFont(ofSize: 17, weight: .regular)) textView.monospaceFont = UIFontMetrics.default.scaledFont(for: .monospacedSystemFont(ofSize: 17, weight: .regular))
textView.adjustsFontForContentSizeCategory = true textView.adjustsFontForContentSizeCategory = true
@ -221,7 +226,6 @@ private class ProfileFieldValueView: UIView {
textView.topAnchor.constraint(equalTo: topAnchor), textView.topAnchor.constraint(equalTo: topAnchor),
textView.bottomAnchor.constraint(equalTo: bottomAnchor), textView.bottomAnchor.constraint(equalTo: bottomAnchor),
]) ])
} }
required init?(coder: NSCoder) { required init?(coder: NSCoder) {

View File

@ -61,6 +61,9 @@ class ProfileHeaderView: UIView {
override func awakeFromNib() { override func awakeFromNib() {
super.awakeFromNib() super.awakeFromNib()
backgroundColor = .appBackground
avatarContainerView.backgroundColor = .appBackground
avatarContainerView.layer.masksToBounds = true avatarContainerView.layer.masksToBounds = true
avatarImageView.layer.masksToBounds = true avatarImageView.layer.masksToBounds = true
avatarImageView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(avatarPressed))) avatarImageView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(avatarPressed)))

View File

@ -79,7 +79,6 @@
</textView> </textView>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="vKC-m1-Sbs" customClass="ProfileFieldsView" customModule="Tusker" customModuleProvider="target"> <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="vKC-m1-Sbs" customClass="ProfileFieldsView" customModule="Tusker" customModuleProvider="target">
<rect key="frame" x="0.0" y="263.5" width="398" height="128"/> <rect key="frame" x="0.0" y="263.5" width="398" height="128"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints> <constraints>
<constraint firstAttribute="height" constant="128" placeholder="YES" id="xbR-M6-H0I"/> <constraint firstAttribute="height" constant="128" placeholder="YES" id="xbR-M6-H0I"/>
</constraints> </constraints>

View File

@ -317,6 +317,12 @@ class ConversationMainStatusCollectionViewCell: UICollectionViewListCell, Status
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
override func updateConfiguration(using state: UICellConfigurationState) {
var config = UIBackgroundConfiguration.listPlainCell().updated(for: state)
config.backgroundColor = .appBackground
backgroundConfiguration = config
}
// MARK: Configure UI // MARK: Configure UI
func updateUI(statusID: String, state: CollapseState) { func updateUI(statusID: String, state: CollapseState) {
@ -410,7 +416,7 @@ class ConversationMainStatusCollectionViewCell: UICollectionViewListCell, Status
guard let delegate else { guard let delegate else {
return return
} }
let vc = delegate.statusActionAccountList(action: .favorite, statusID: statusID, statusState: statusState.copy(), accountIDs: nil) let vc = StatusActionAccountListViewController(actionType: .favorite, statusID: statusID, statusState: statusState.copy(), accountIDs: nil, mastodonController: mastodonController)
// TODO: only show warning if the instance isn't the logged in one // TODO: only show warning if the instance isn't the logged in one
vc.showInacurateCountWarning = true vc.showInacurateCountWarning = true
delegate.show(vc) delegate.show(vc)
@ -420,7 +426,7 @@ class ConversationMainStatusCollectionViewCell: UICollectionViewListCell, Status
guard let delegate else { guard let delegate else {
return return
} }
let vc = delegate.statusActionAccountList(action: .reblog, statusID: statusID, statusState: statusState.copy(), accountIDs: nil) let vc = StatusActionAccountListViewController(actionType: .favorite, statusID: statusID, statusState: statusState.copy(), accountIDs: nil, mastodonController: mastodonController)
vc.showInacurateCountWarning = true vc.showInacurateCountWarning = true
delegate.show(vc) delegate.show(vc)
} }

View File

@ -351,6 +351,16 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti
} }
} }
override func updateConfiguration(using state: UICellConfigurationState) {
var config = UIBackgroundConfiguration.listPlainCell().updated(for: state)
if state.isHighlighted || state.isSelected {
config.backgroundColor = .appSelectedCellBackground
} else {
config.backgroundColor = .appBackground
}
backgroundConfiguration = config
}
// MARK: Accessibility // MARK: Accessibility
override var isAccessibilityElement: Bool { override var isAccessibilityElement: Bool {

View File

@ -93,6 +93,16 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
updateActionsVisibility() updateActionsVisibility()
} }
override func updateConfiguration(using state: UICellConfigurationState) {
var config = UIBackgroundConfiguration.listPlainCell().updated(for: state)
if state.isHighlighted || state.isSelected {
config.backgroundColor = .appSelectedCellBackground
} else {
config.backgroundColor = .appBackground
}
backgroundConfiguration = config
}
override func createObserversIfNecessary() { override func createObserversIfNecessary() {
super.createObserversIfNecessary() super.createObserversIfNecessary()

View File

@ -0,0 +1,21 @@
//
// TrendingStatusCollectionViewCell.swift
// Tusker
//
// Created by Shadowfacts on 2/2/23.
// Copyright © 2023 Shadowfacts. All rights reserved.
//
import UIKit
class TrendingStatusCollectionViewCell: TimelineStatusCollectionViewCell {
override func updateConfiguration(using state: UICellConfigurationState) {
var config = UIBackgroundConfiguration.listGroupedCell().updated(for: state)
if state.isHighlighted || state.isSelected {
config.backgroundColor = .appSelectedCellBackground
} else {
config.backgroundColor = .appGroupedCellBackground
}
backgroundConfiguration = config
}
}

View File

@ -22,6 +22,8 @@ struct ToastConfiguration {
var edge: Edge = .automatic var edge: Edge = .automatic
var dismissOnScroll = true var dismissOnScroll = true
var dismissAutomaticallyAfter: TimeInterval? = nil var dismissAutomaticallyAfter: TimeInterval? = nil
var onAppear: ((UIViewPropertyAnimator?) -> Void)?
var onDismiss: ((UIViewPropertyAnimator?) -> Void)?
init(title: String) { init(title: String) {
self.title = title self.title = title

View File

@ -127,22 +127,30 @@ class ToastView: UIView {
func dismissToast(animated: Bool) { func dismissToast(animated: Bool) {
guard animated else { guard animated else {
removeFromSuperview() removeFromSuperview()
configuration.onDismiss?(nil)
return return
} }
UIView.animate(withDuration: 0.25, delay: 0, options: .curveEaseInOut) { let animator = UIViewPropertyAnimator(duration: 0.25, curve: .easeInOut)
animator.addAnimations {
self.transform = CGAffineTransform(translationX: 0, y: self.offscreenTranslation) self.transform = CGAffineTransform(translationX: 0, y: self.offscreenTranslation)
} completion: { (_) in }
animator.addCompletion { _ in
self.removeFromSuperview() self.removeFromSuperview()
} }
configuration.onDismiss?(animator)
animator.startAnimation()
} }
func animateAppearance() { func animateAppearance() {
self.transform = CGAffineTransform(translationX: 0, y: offscreenTranslation) self.transform = CGAffineTransform(translationX: 0, y: offscreenTranslation)
let duration = 0.5 let duration = 0.5
let velocity = 0.5 let velocity = CGVector(dx: 0, dy: 0.5)
UIView.animate(withDuration: duration, delay: 0, usingSpringWithDamping: 0.65, initialSpringVelocity: velocity, options: []) { let animator = UIViewPropertyAnimator(duration: duration, timingParameters: UISpringTimingParameters(dampingRatio: 0.65, initialVelocity: velocity))
animator.addAnimations {
self.transform = .identity self.transform = .identity
} }
configuration.onAppear?(animator)
animator.startAnimation()
} }
func setupDismissOnScroll(connectedTo scrollView: UIScrollView) { func setupDismissOnScroll(connectedTo scrollView: UIScrollView) {
@ -184,11 +192,14 @@ class ToastView: UIView {
shrinkAnimator = nil shrinkAnimator = nil
} }
private var panGestureDismissAnimator: UIViewPropertyAnimator?
@objc private func panRecognized(_ recognizer: UIPanGestureRecognizer) { @objc private func panRecognized(_ recognizer: UIPanGestureRecognizer) {
let translation = recognizer.translation(in: self).y let translation = recognizer.translation(in: self).y
let isDraggingAwayFromDismissalEdge = (configuration.edge == .top && translation > 0) || (configuration.edge == .bottom && translation < 0) let isDraggingAwayFromDismissalEdge = (configuration.edge == .top && translation > 0) || (configuration.edge == .bottom && translation < 0)
switch recognizer.state { switch recognizer.state {
case .began: case .began:
recognizedGesture = true recognizedGesture = true
@ -196,19 +207,25 @@ class ToastView: UIView {
UIView.animate(withDuration: 0.1, delay: 0, options: [.curveEaseInOut, .allowUserInteraction]) { UIView.animate(withDuration: 0.1, delay: 0, options: [.curveEaseInOut, .allowUserInteraction]) {
self.transform = .identity self.transform = .identity
} }
break panGestureDismissAnimator = UIViewPropertyAnimator(duration: 0.2, curve: .easeInOut)
configuration.onDismiss?(panGestureDismissAnimator!)
case .changed: case .changed:
var distance: CGFloat var distance: CGFloat
var alongsideAnimationProgress: CGFloat
if isDraggingAwayFromDismissalEdge { if isDraggingAwayFromDismissalEdge {
distance = sqrt(abs(translation)) distance = sqrt(abs(translation))
if configuration.edge == .bottom { if configuration.edge == .bottom {
distance *= -1 distance *= -1
} }
alongsideAnimationProgress = 0
} else { } else {
distance = translation distance = translation
alongsideAnimationProgress = abs(translation) / (configuration.edgeSpacing + bounds.height)
} }
transform = CGAffineTransform(translationX: 0, y: distance) transform = CGAffineTransform(translationX: 0, y: distance)
panGestureDismissAnimator!.fractionComplete = alongsideAnimationProgress
case .ended, .cancelled: case .ended, .cancelled:
let velocity = recognizer.velocity(in: self).y let velocity = recognizer.velocity(in: self).y
@ -219,27 +236,31 @@ class ToastView: UIView {
let minDismissalVelocity: CGFloat = 250 let minDismissalVelocity: CGFloat = 250
let dismissDueToVelocity = configuration.edge == .bottom ? velocity > minDismissalDistance : velocity < -minDismissalVelocity let dismissDueToVelocity = configuration.edge == .bottom ? velocity > minDismissalDistance : velocity < -minDismissalVelocity
if dismissDueToDistance || dismissDueToVelocity { if dismissDueToDistance || dismissDueToVelocity {
if abs(translation) < abs(offscreenTranslation) { if abs(translation) < abs(offscreenTranslation) {
let distanceLeft = offscreenTranslation - translation panGestureDismissAnimator!.addAnimations {
let duration = 1 / TimeInterval(max(velocity, minDismissalVelocity) / distanceLeft)
UIView.animate(withDuration: duration, delay: 0, options: .allowUserInteraction) {
self.transform = CGAffineTransform(translationX: 0, y: self.offscreenTranslation) self.transform = CGAffineTransform(translationX: 0, y: self.offscreenTranslation)
} completion: { (_) in }
panGestureDismissAnimator!.addCompletion { _ in
self.removeFromSuperview() self.removeFromSuperview()
} }
} else { } else {
self.removeFromSuperview() self.removeFromSuperview()
} }
panGestureDismissAnimator!.startAnimation()
} else { } else {
let duration = 0.5 let duration = 0.5
let velocity = distance * duration let velocity = distance * duration
UIView.animate(withDuration: duration, delay: 0, usingSpringWithDamping: 0.65, initialSpringVelocity: velocity, options: .allowUserInteraction) { UIView.animate(withDuration: duration, delay: 0, usingSpringWithDamping: 0.65, initialSpringVelocity: velocity, options: .allowUserInteraction) {
self.transform = .identity self.transform = .identity
} }
panGestureDismissAnimator!.isReversed = true
panGestureDismissAnimator!.startAnimation()
} }
default: default:

View File

@ -77,6 +77,8 @@ extension ToastableViewController {
if animated { if animated {
toast.animateAppearance() toast.animateAppearance()
} else {
config.onAppear?(nil)
} }
if config.dismissOnScroll, if config.dismissOnScroll,

View File

@ -16,7 +16,7 @@ class TrendHistoryView: UIView {
private let curveRadius: CGFloat = 10 private let curveRadius: CGFloat = 10
/// The base background color used for the graph fill. /// The base background color used for the graph fill.
var effectiveBackgroundColor = UIColor.systemBackground var effectiveBackgroundColor = UIColor.appBackground
override func layoutSubviews() { override func layoutSubviews() {
super.layoutSubviews() super.layoutSubviews()