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
## 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)
Features/Improvements:
- Consolidate Trends into a single screen

View File

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

View File

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

View File

@ -41,8 +41,6 @@
D6114E1127F899B30080E273 /* TrendingLinksViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6114E1027F899B30080E273 /* TrendingLinksViewController.swift */; };
D6114E1327F89B440080E273 /* TrendingLinkTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6114E1227F89B440080E273 /* TrendingLinkTableViewCell.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 */; };
D61A45E828DF477D002BE511 /* LoadingCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61A45E728DF477D002BE511 /* LoadingCollectionViewCell.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 */; };
D681E4D9246E346E0053414F /* AccountActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D681E4D8246E346E0053414F /* AccountActivityItemSource.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 */; };
D6895DC228D65274006341DA /* CustomAlertController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6895DC128D65274006341DA /* CustomAlertController.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 */; };
D6C3F4F9298EDBF20009FCFF /* ConversationTree.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C3F4F8298EDBF20009FCFF /* ConversationTree.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 */; };
D6C693FC2162FE6F007D6A6D /* LoadingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693FB2162FE6F007D6A6D /* LoadingViewController.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 */; };
D6D706A029466649000827ED /* ScrollingSegmentedControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D7069F29466649000827ED /* ScrollingSegmentedControl.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 */; };
D6DD2A3F273C1F4900386A6C /* ComposeAttachmentImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD2A3E273C1F4900386A6C /* ComposeAttachmentImage.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 */; };
D6DD8FFD298495A8002AD3FD /* LogoutService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD8FFC298495A8002AD3FD /* LogoutService.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 */; };
D6DFC69E242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DFC69D242C490400ACC392 /* TrackpadScrollGestureRecognizer.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>"; };
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>"; };
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>"; };
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>"; };
@ -612,6 +614,7 @@
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>"; };
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>"; };
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>"; };
@ -710,6 +713,8 @@
D6C3F4F6298ED7F70009FCFF /* FavoritesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesViewController.swift; sourceTree = "<group>"; };
D6C3F4F8298EDBF20009FCFF /* ConversationTree.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationTree.swift; sourceTree = "<group>"; };
D6C3F4FA299035650009FCFF /* TrendsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendsViewController.swift; sourceTree = "<group>"; };
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>"; };
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>"; };
@ -743,6 +748,8 @@
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>"; };
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>"; };
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>"; };
@ -750,6 +757,7 @@
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>"; };
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>"; };
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>"; };
@ -853,8 +861,6 @@
D611C2CC232DC5FC00C86A49 /* Hashtag Cell */ = {
isa = PBXGroup;
children = (
D611C2CD232DC61100C86A49 /* HashtagTableViewCell.swift */,
D611C2CE232DC61100C86A49 /* HashtagTableViewCell.xib */,
D6E77D08286D25FA00D8B732 /* TrendingHashtagCollectionViewCell.swift */,
);
path = "Hashtag Cell";
@ -943,6 +949,8 @@
D693A72D25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift */,
D693A72E25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.xib */,
D6C3F4FA299035650009FCFF /* TrendsViewController.swift */,
D68329EE299540050026EB24 /* MoreTrendsFooterCollectionViewCell.swift */,
D6DD996A2998611A0015C962 /* SuggestedProfilesViewController.swift */,
);
path = Explore;
sourceTree = "<group>";
@ -1050,6 +1058,7 @@
D66C900A28DAB7FD00217BF2 /* TimelineViewController.swift */,
D6ADB6E728E8C878009924AB /* PublicTimelineDescriptionCollectionViewCell.swift */,
D6D12B2E2925D66500D528E1 /* TimelineGapCollectionViewCell.swift */,
D6C3F5182991F5D60009FCFF /* TimelineJumpButton.swift */,
);
path = Timeline;
sourceTree = "<group>";
@ -1181,6 +1190,7 @@
D6ADB6E928E91C30009924AB /* TimelineStatusCollectionViewCell.swift */,
D601FA60297B539E00A8E8B5 /* ConversationMainStatusCollectionViewCell.swift */,
D6ADB6EB28EA73CB009924AB /* StatusContentContainer.swift */,
D6D9498C298CBB4000C59229 /* TrendingStatusCollectionViewCell.swift */,
);
path = Status;
sourceTree = "<group>";
@ -1274,6 +1284,7 @@
D6DD353E22F502EC00A9563A /* Preferences+Notification.swift */,
D6BC9DB4232D4CE3002CA326 /* NotificationsMode.swift */,
D61F75872932DB6000C0B37F /* StatusSwipeAction.swift */,
D6D94954298963A900C59229 /* Colors.swift */,
);
path = Preferences;
sourceTree = "<group>";
@ -1303,6 +1314,7 @@
D63CC70F2911F1E4000E19DE /* UIScrollView+Top.swift */,
D61F758F29353B4300C0B37F /* FileManager+Size.swift */,
D61F75AC293AF39000C0B37F /* Filter+Helpers.swift */,
D6C3F5162991C1A00009FCFF /* View+AppListStyle.swift */,
);
path = Extensions;
sourceTree = "<group>";
@ -1843,7 +1855,6 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
D611C2D0232DC61100C86A49 /* HashtagTableViewCell.xib in Resources */,
D6E77D0D286E6B7300D8B732 /* TrendingLinkCardCollectionViewCell.xib in Resources */,
D693A73025CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.xib in Resources */,
D61AC1D9232EA42D00C54D2D /* InstanceTableViewCell.xib in Resources */,
@ -1960,6 +1971,7 @@
D6093FB725BE0CF3004811E6 /* TrendHistoryView.swift in Sources */,
D6EBF01723C55E0D00AE061B /* UISceneSession+MastodonController.swift in Sources */,
D61F759229365C6C00C0B37F /* CollectionViewController.swift in Sources */,
D6DD996B2998611A0015C962 /* SuggestedProfilesViewController.swift in Sources */,
D6DD8FFD298495A8002AD3FD /* LogoutService.swift in Sources */,
04DACE8C212CB14B009840C4 /* MainTabBarViewController.swift in Sources */,
D68E525D24A3E8F00054355A /* InlineTrendsViewController.swift in Sources */,
@ -1993,7 +2005,6 @@
0411610022B442870030A9B7 /* LoadingLargeImageViewController.swift in Sources */,
D6A4DCE525537C7A00D9DE31 /* FastSwitchingAccountView.swift in Sources */,
D6A4DCCC2553667800D9DE31 /* FastAccountSwitcherViewController.swift in Sources */,
D611C2CF232DC61100C86A49 /* HashtagTableViewCell.swift in Sources */,
D627944F23A9C99800D38C68 /* EditListAccountsViewController.swift in Sources */,
D6945C3423AC6431005C403C /* AddSavedHashtagViewController.swift in Sources */,
D6F6A552291F098700F496A8 /* RenameListService.swift in Sources */,
@ -2002,6 +2013,7 @@
D6AC956723C4347E008C9946 /* MainSceneDelegate.swift in Sources */,
D6311C5025B3765B00B27539 /* ImageDataCache.swift in Sources */,
D6BC9DD7232D7811002CA326 /* TimelinesPageViewController.swift in Sources */,
D6D9498D298CBB4000C59229 /* TrendingStatusCollectionViewCell.swift in Sources */,
D60E2F2E244248BF005F8713 /* MastodonCachePersistentStore.swift in Sources */,
D620483623D38075008A63EF /* ContentTextView.swift in Sources */,
D651C5B42915B00400236EF6 /* ProfileFieldsView.swift in Sources */,
@ -2011,6 +2023,7 @@
D6A3BC802321B7E600FD64D5 /* FollowNotificationGroupTableViewCell.swift in Sources */,
D627944D23A9A03D00D38C68 /* ListTimelineViewController.swift in Sources */,
D6D12B2F2925D66500D528E1 /* TimelineGapCollectionViewCell.swift in Sources */,
D6D94955298963A900C59229 /* Colors.swift in Sources */,
D6945C3823AC739F005C403C /* InstanceTimelineViewController.swift in Sources */,
D625E4822588262A0074BB2B /* DraggableTableViewCell.swift in Sources */,
D68E525B24A3D77E0054355A /* TuskerRootViewController.swift in Sources */,
@ -2066,6 +2079,7 @@
D6A00B1D26379FC900316AD4 /* PollOptionsView.swift in Sources */,
D6DF95C12533F5DE0027A9B6 /* RelationshipMO.swift in Sources */,
D6ADB6EE28EA74E8009924AB /* UIView+Configure.swift in Sources */,
D68329EF299540050026EB24 /* MoreTrendsFooterCollectionViewCell.swift in Sources */,
D623A5412635FB3C0095BD04 /* PollOptionView.swift in Sources */,
D61F75B1293BD85300C0B37F /* CreateFilterService.swift in Sources */,
D65C6BF525478A9C00A6E89C /* BackgroundableViewController.swift in Sources */,
@ -2087,6 +2101,7 @@
D6CA8CDE296387310050C433 /* SaveToPhotosActivity.swift in Sources */,
D6B936712829F72900237D0E /* NSManagedObjectContext+Helpers.swift in Sources */,
D68A76EE295369C7001DA1B3 /* AcknowledgementsView.swift in Sources */,
D6C3F5192991F5D60009FCFF /* TimelineJumpButton.swift in Sources */,
D63CC7102911F1E4000E19DE /* UIScrollView+Top.swift in Sources */,
D6F6A557291F4F1600F496A8 /* MuteAccountView.swift in Sources */,
D65B4B8B297879E900DABDFB /* AccountFollowsViewController.swift in Sources */,
@ -2238,6 +2253,7 @@
D6E426B325337C7000C02E1C /* CustomEmojiImageView.swift in Sources */,
D6D4DDD0212518A000E1C4BB /* AppDelegate.swift in Sources */,
D61F75B3293BD89C00C0B37F /* UpdateFilterService.swift in Sources */,
D6C3F5172991C1A00009FCFF /* View+AppListStyle.swift in Sources */,
D65B4B6A297777D900DABDFB /* StatusNotFoundView.swift in Sources */,
D6412B0924B0291E00F5412E /* MyProfileViewController.swift in Sources */,
D601FA5D297B2E6F00A8E8B5 /* ConversationCollectionViewController.swift in Sources */,
@ -2408,7 +2424,7 @@
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 72;
CURRENT_PROJECT_VERSION = 73;
INFOPLIST_FILE = Tusker/Info.plist;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
@ -2473,7 +2489,7 @@
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 72;
CURRENT_PROJECT_VERSION = 73;
INFOPLIST_FILE = OpenInTusker/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
@ -2624,7 +2640,7 @@
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 72;
CURRENT_PROJECT_VERSION = 73;
INFOPLIST_FILE = Tusker/Info.plist;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
@ -2652,7 +2668,7 @@
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 72;
CURRENT_PROJECT_VERSION = 73;
INFOPLIST_FILE = Tusker/Info.plist;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
@ -2757,7 +2773,7 @@
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 72;
CURRENT_PROJECT_VERSION = 73;
INFOPLIST_FILE = OpenInTusker/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
@ -2783,7 +2799,7 @@
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 72;
CURRENT_PROJECT_VERSION = 73;
INFOPLIST_FILE = OpenInTusker/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
"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 {
configureSentry()
swizzleStatusBar()
swizzlePresentationController()
AppShortcutItem.createItems(for: application)
@ -154,4 +155,24 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
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)
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.avatarStyle = try container.decode(AvatarStyle.self, forKey: .avatarStyle)
self.hideCustomEmojiInUsernames = try container.decode(Bool.self, forKey: .hideCustomEmojiInUsernames)
@ -92,6 +93,7 @@ class Preferences: Codable, ObservableObject {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(theme, forKey: .theme)
try container.encode(pureBlackDarkMode, forKey: .pureBlackDarkMode)
try container.encode(accentColor, forKey: .accentColor)
try container.encode(avatarStyle, forKey: .avatarStyle)
try container.encode(hideCustomEmojiInUsernames, forKey: .hideCustomEmojiInUsernames)
@ -140,6 +142,7 @@ class Preferences: Codable, ObservableObject {
// MARK: Appearance
@Published var theme = UIUserInterfaceStyle.unspecified
@Published var pureBlackDarkMode = true
@Published var accentColor = AccentColor.default
@Published var avatarStyle = AvatarStyle.roundRect
@Published var hideCustomEmojiInUsernames = false
@ -202,6 +205,7 @@ class Preferences: Codable, ObservableObject {
private enum CodingKeys: String, CodingKey {
case theme
case pureBlackDarkMode
case accentColor
case avatarStyle
case hideCustomEmojiInUsernames

View File

@ -243,8 +243,13 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDelegate
}
@objc func themePrefChanged() {
window?.overrideUserInterfaceStyle = Preferences.shared.theme
window?.tintColor = Preferences.shared.accentColor.color
guard let window else { return }
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() {

View File

@ -41,7 +41,8 @@ class AccountFollowsListViewController: UIViewController, CollectionViewControll
}
override func loadView() {
var config = UICollectionLayoutListConfiguration(appearance: .grouped)
var config = UICollectionLayoutListConfiguration(appearance: .plain)
config.backgroundColor = .appBackground
config.itemSeparatorHandler = { [unowned self] indexPath, sectionConfig in
guard let item = self.dataSource.itemIdentifier(for: indexPath) else {
return sectionConfig
@ -65,6 +66,16 @@ class AccountFollowsListViewController: UIViewController, CollectionViewControll
let accountCell = UICollectionView.CellRegistration<AccountCollectionViewCell, String> { [unowned self] cell, indexPath, item in
cell.delegate = self
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
cell.indicator.startAnimating()

View File

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

View File

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

View File

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

View File

@ -111,7 +111,7 @@ struct ComposePollView: View {
private var backgroundColor: Color {
// 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 {
@ -191,7 +191,7 @@ struct ComposePollOption: View {
private var textField: some View {
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() {
@ -216,7 +216,7 @@ struct ComposePollOption: View {
.cornerRadius(radiusFraction * size)
Rectangle()
.foregroundColor(Color(UIColor.systemBackground))
.foregroundColor(Color(UIColor.appBackground))
.frame(width: innerSize, height: innerSize)
.cornerRadius(radiusFraction * innerSize)
}

View File

@ -94,6 +94,10 @@ struct ComposeView: View {
var body: some View {
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
.scrollDismissesKeyboardInteractivelyIfAvailable()
@ -124,7 +128,7 @@ struct ComposeView: View {
}
})
.sheet(isPresented: $uiState.isShowingDraftsList) {
DraftsView(currentDraft: draft, mastodonController: mastodonController)
DraftsRepresentable(currentDraft: draft, mastodonController: mastodonController)
}
.actionSheet(isPresented: $uiState.isShowingSaveDraftSheet, content: self.saveAndCloseSheet)
.alert(isPresented: $isShowingPostErrorAlert) {
@ -169,11 +173,13 @@ struct ComposeView: View {
)
.listRowInsets(EdgeInsets(top: 8, leading: 8, bottom: 4, trailing: 8))
.listRowSeparator(.hidden)
.listRowBackground(Color.appBackground)
}
header
.listRowInsets(EdgeInsets(top: draft.inReplyToID == nil ? 8 : 4, leading: 8, bottom: 4, trailing: 8))
.listRowSeparator(.hidden)
.listRowBackground(Color.appBackground)
if uiState.draft.contentWarningEnabled {
ComposeEmojiTextField(
@ -184,6 +190,7 @@ struct ComposeView: View {
)
.listRowInsets(EdgeInsets(top: 4, leading: 8, bottom: 4, trailing: 8))
.listRowSeparator(.hidden)
.listRowBackground(Color.appBackground)
}
MainComposeTextView(
@ -192,17 +199,20 @@ struct ComposeView: View {
)
.listRowInsets(EdgeInsets(top: 4, leading: 8, bottom: 4, trailing: 8))
.listRowSeparator(.hidden)
.listRowBackground(Color.appBackground)
if let poll = draft.poll {
ComposePollView(draft: draft, poll: poll)
.listRowInsets(EdgeInsets(top: 4, leading: 8, bottom: 4, trailing: 8))
.listRowSeparator(.hidden)
.listRowBackground(Color.appBackground)
}
ComposeAttachmentsList(
draft: draft
)
.listRowInsets(EdgeInsets(top: 4, leading: 8, bottom: 8, trailing: 8))
.listRowBackground(Color.appBackground)
}
.animation(.default, value: draft.poll?.options.count)
.scrollDismissesKeyboardInteractivelyIfAvailable()
@ -319,7 +329,7 @@ struct ComposeView: View {
}
}
private extension View {
extension View {
@available(iOS, obsoleted: 16.0)
@ViewBuilder
func scrollDismissesKeyboardInteractivelyIfAvailable() -> some View {

View File

@ -8,6 +8,21 @@
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 {
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
@ -49,8 +64,10 @@ struct DraftsView: View {
.map { visibleDrafts[$0] }
.forEach { draftsManager.remove($0) }
}
.appGroupedListRowBackground()
}
.listStyle(.plain)
.appGroupedListBackground(container: DraftsRepresentable.UIViewControllerType.self)
.navigationTitle(Text("Drafts"))
.navigationBarTitleDisplayMode(.inline)
.toolbar {

View File

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

View File

@ -37,7 +37,7 @@ class ConversationCollectionViewController: UIViewController, CollectionViewCont
override func loadView() {
var config = UICollectionLayoutListConfiguration(appearance: .plain)
config.backgroundColor = .secondarySystemBackground
config.backgroundColor = .appSecondaryBackground
config.leadingSwipeActionsConfigurationProvider = { [unowned self] in
(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
// 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)
view = UICollectionView(frame: .zero, collectionViewLayout: layout)
// 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")
view.backgroundColor = .secondarySystemBackground
view.backgroundColor = .appSecondaryBackground
collapseBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "eye.fill")!, style: .plain, target: self, action: #selector(toggleCollapseButtonPressed))
updateVisibilityBarButtonItem()

View File

@ -141,18 +141,13 @@ class ExpandThreadCollectionViewCell: UICollectionViewListCell {
}
override func updateConfiguration(using state: UICellConfigurationState) {
var config = UIBackgroundConfiguration.listPlainCell()
var config = UIBackgroundConfiguration.listPlainCell().updated(for: state)
if state.isSelected || state.isHighlighted {
var hue: CGFloat = 0
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)
config.backgroundColor = .appSelectedCellBackground
} else {
config.backgroundColor = .secondarySystemBackground
config.backgroundColor = .appSecondaryBackground
}
backgroundConfiguration = config.updated(for: state)
backgroundConfiguration = config
}
}

View File

@ -9,6 +9,20 @@
import SwiftUI
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 {
@EnvironmentObject private var mastodonController: MastodonController
@Environment(\.dismiss) private var dismiss
@ -34,6 +48,8 @@ struct AddHashtagPinnedTimelineView: View {
var body: some View {
NavigationView {
list
.listStyle(.grouped)
.appGroupedListBackground(container: AddHashtagPinnedTimelineRepresentable.UIViewControllerType.self)
.navigationTitle("Add Hashtag")
.navigationBarTitleDisplayMode(.inline)
.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 {
List {
let list = List {
Section {
if viewModel.searchQuery.isEmpty {
forEachTag(savedAndFollowedHashtags)
@ -73,8 +90,17 @@ struct AddHashtagPinnedTimelineView: View {
.listRowBackground(EmptyView())
.listRowSeparator(.hidden)
}
.appGroupedListRowBackground()
}
.listStyle(.grouped)
if #available(iOS 16.0, *) {
list
.scrollContentBackground(.hidden)
.background(Color.appGroupedBackground)
} else {
list
}
}
private func forEachTag(_ tags: [String]) -> some View {

View File

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

View File

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

View File

@ -111,7 +111,13 @@ struct PinnedTimelinesView: View {
Text("Pinned Timelines")
}
.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: {
AddInstancePinnedTimelineView(pinnedTimelines: $pinnedTimelines)

View File

@ -34,15 +34,15 @@ class AddSavedHashtagViewController: UIViewController {
title = NSLocalizedString("Search", comment: "search screen title")
view.backgroundColor = .systemGroupedBackground
var config = UICollectionLayoutListConfiguration(appearance: .grouped)
config.backgroundColor = .appGroupedBackground
config.headerMode = .supplementary
let layout = UICollectionViewCompositionalLayout.list(using: config)
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
collectionView.delegate = self
collectionView.allowsFocus = true
collectionView.backgroundColor = .appGroupedBackground
view.addSubview(collectionView)
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()
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
let layout = UICollectionViewCompositionalLayout.list(using: configuration)
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
@ -100,12 +101,23 @@ class ExploreViewController: UIViewController, UICollectionViewDelegate, Collect
override func viewWillAppear(_ animated: Bool) {
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)
}
override func viewDidAppear(_ animated: Bool) {
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
// does not cause it to automatically become active once it becomes visible
// see FB7814561
@ -130,6 +142,16 @@ class ExploreViewController: UIViewController, UICollectionViewDelegate, Collect
config.image = item.image
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 {
case .addList, .addSavedHashtag, .findInstance:
cell.accessories = []

View File

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

View File

@ -1,9 +1,9 @@
<?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"/>
<dependencies>
<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="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
@ -55,7 +55,6 @@
</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">
<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>
<color key="textColor" systemColor="labelColor"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
@ -102,7 +101,7 @@
</objects>
<resources>
<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 name="systemBackgroundColor">
<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.autoresizingMask = [.flexibleWidth, .flexibleHeight]
collectionView.backgroundColor = .secondarySystemBackground
collectionView.backgroundColor = .appSecondaryBackground
collectionView.register(UINib(nibName: "FeaturedProfileCollectionViewCell", bundle: .main), forCellWithReuseIdentifier: "featuredProfileCell")
collectionView.delegate = self
collectionView.dragDelegate = self

View File

@ -36,6 +36,7 @@ class SuggestedProfileCardCollectionViewCell: UICollectionViewCell {
layer.shadowOffset = .zero
layer.masksToBounds = false
contentView.layer.cornerRadius = 12.5
contentView.backgroundColor = .appGroupedCellBackground
updateLayerColors()
headerImageView.cache = .headers
@ -189,7 +190,7 @@ private struct SuggestionSourceView: View {
var body: some View {
VStack(alignment: .leading) {
HStack {
Image(uiImage: source.image)
Image(uiImage: source.image.withRenderingMode(.alwaysTemplate))
Text(source.title)
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 Pachyderm
import WebURLFoundationExtras
import Combine
class TrendingHashtagsViewController: UIViewController {
@ -17,6 +18,9 @@ class TrendingHashtagsViewController: UIViewController {
private var collectionView: UICollectionView!
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
private var state = State.unloaded
private var confirmLoadMore = PassthroughSubject<Void, Never>()
init(mastodonController: MastodonController) {
self.mastodonController = mastodonController
@ -32,9 +36,21 @@ class TrendingHashtagsViewController: UIViewController {
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)
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
@ -43,14 +59,24 @@ class TrendingHashtagsViewController: UIViewController {
collectionView.allowsFocus = true
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)
}
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
switch item {
case let .tag(hashtag):
return collectionView.dequeueConfiguredReusableCell(using: registration, for: indexPath, item: hashtag)
case .loadingIndicator:
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) {
super.viewWillAppear(animated)
let request = Client.getTrendingHashtags(limit: 10)
Task {
guard let (hashtags, _) = try? await mastodonController.run(request) else {
return
}
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.trendingTags])
snapshot.appendItems(hashtags.map { .tag($0) })
await dataSource.apply(snapshot)
await loadInitial()
}
}
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 {
@ -77,11 +189,45 @@ extension TrendingHashtagsViewController {
case trendingTags
}
enum Item: Hashable {
case loadingIndicator
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 {
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) {
guard let item = dataSource.itemIdentifier(for: indexPath),
case let .tag(hashtag) = item else {

View File

@ -21,6 +21,29 @@ class TrendingLinkCardCollectionViewCell: UICollectionViewCell {
@IBOutlet weak var providerLabel: UILabel!
@IBOutlet weak var activityLabel: UILabel!
@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?
@ -34,8 +57,11 @@ class TrendingLinkCardCollectionViewCell: UICollectionViewCell {
layer.shadowOffset = .zero
layer.masksToBounds = false
contentView.layer.cornerRadius = 12.5
contentView.backgroundColor = .appGroupedCellBackground
updateLayerColors()
verticalSize = .regular
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">
<rect key="frame" x="0.0" y="0.0" width="300" height="225"/>
<constraints>
<constraint firstAttribute="width" secondItem="h3b-Mf-lD6" secondAttribute="height" multiplier="4:3" id="QDY-8a-LYC"/>
<constraint firstAttribute="width" secondItem="h3b-Mf-lD6" secondAttribute="height" multiplier="4:3" placeholder="YES" id="QDY-8a-LYC"/>
</constraints>
</imageView>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="top" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="LpU-m4-guC">
@ -56,9 +56,9 @@
</constraints>
</view>
<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">
<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"/>
<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">

View File

@ -80,6 +80,16 @@ class TrendingLinkTableViewCell: UITableViewCell {
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) {
self.card = card
self.thumbnailView.image = nil

View File

@ -10,19 +10,22 @@ import UIKit
import Pachyderm
import WebURLFoundationExtras
import SafariServices
import Combine
class TrendingLinksViewController: EnhancedTableViewController {
class TrendingLinksViewController: UIViewController, CollectionViewController {
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) {
self.mastodonController = mastodonController
super.init(style: .grouped)
dragEnabled = true
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
@ -34,72 +37,259 @@ class TrendingLinksViewController: EnhancedTableViewController {
title = NSLocalizedString("Trending Links", comment: "trending links screen title")
tableView.register(TrendingLinkTableViewCell.self, forCellReuseIdentifier: "trendingLinkCell")
tableView.estimatedRowHeight = 100
tableView.allowsFocus = true
let layout = UICollectionViewCompositionalLayout { [unowned self] sectionIndex, environment in
switch dataSource.sectionIdentifier(for: sectionIndex) {
case nil:
fatalError()
dataSource = UITableViewDiffableDataSource(tableView: tableView, cellProvider: { tableView, indexPath, item in
let cell = tableView.dequeueReusableCell(withIdentifier: "trendingLinkCell", for: indexPath) as! TrendingLinkTableViewCell
cell.updateUI(card: item.card)
return cell
})
case .loadingIndicator:
var config = UICollectionLayoutListConfiguration(appearance: .grouped)
config.backgroundColor = .appGroupedBackground
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) {
super.viewWillAppear(animated)
let request = Client.getTrendingLinks()
Task {
guard let (links, _) = try? await mastodonController.run(request) else {
return
}
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.links])
snapshot.appendItems(links.map(Item.init))
await dataSource.apply(snapshot)
await loadInitial()
}
}
// MARK: - Table View Delegate
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let item = dataSource.itemIdentifier(for: indexPath),
let url = URL(item.card.url) else {
@MainActor
private func loadInitial() async {
guard case .unloaded = state else {
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? {
guard let item = dataSource.itemIdentifier(for: indexPath),
let url = URL(item.card.url) else {
return nil
@MainActor
private func loadOlder() async {
guard case .loaded = state else {
return
}
return UIContextMenuConfiguration(identifier: nil) {
let vc = SFSafariViewController(url: url)
vc.preferredControlTintColor = Preferences.shared.accentColor.color
return vc
} actionProvider: { _ in
return UIMenu(children: self.actionsForTrendingLink(card: item.card))
state = .loadingOlder
let origSnapshot = dataSource.snapshot()
var snapshot = origSnapshot
if Preferences.shared.disableInfiniteScrolling {
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 {
enum Section {
case loadingIndicator
case links
}
struct Item: Hashable {
let card: Card
enum Item: Hashable {
case loadingIndicator
case link(Card)
case confirmLoadMore(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) {
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 Pachyderm
import SafariServices
import Combine
class TrendsViewController: UIViewController, CollectionViewController {
@ -18,6 +19,8 @@ class TrendsViewController: UIViewController, CollectionViewController {
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
private var loadTask: Task<Void, Never>?
private var trendingStatusesState = TrendingStatusesState.unloaded
private let confirmLoadMoreStatuses = PassthroughSubject<Void, Never>()
private var isShowingTrends = false
private var shouldShowTrends: Bool {
@ -42,9 +45,17 @@ class TrendsViewController: UIViewController, CollectionViewController {
let layout = UICollectionViewCompositionalLayout { [unowned self] sectionIndex, environment in
let sectionIdentifier = self.dataSource.snapshot().sectionIdentifiers[sectionIndex]
switch sectionIdentifier {
case .loadingIndicator:
var listConfig = UICollectionLayoutListConfiguration(appearance: .grouped)
listConfig.backgroundColor = .appGroupedBackground
listConfig.showsSeparators = false
return .list(using: listConfig, layoutEnvironment: environment)
case .trendingHashtags:
var listConfig = UICollectionLayoutListConfiguration(appearance: .grouped)
listConfig.headerMode = .supplementary
listConfig.footerMode = .supplementary
listConfig.backgroundColor = .appGroupedBackground
return .list(using: listConfig, layoutEnvironment: environment)
case .trendingLinks:
@ -56,9 +67,10 @@ class TrendsViewController: UIViewController, CollectionViewController {
let section = NSCollectionLayoutSection(group: group)
section.orthogonalScrollingBehavior = .groupPaging
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
case .profileSuggestions:
@ -70,14 +82,25 @@ class TrendsViewController: UIViewController, CollectionViewController {
let section = NSCollectionLayoutSection(group: group)
section.orthogonalScrollingBehavior = .groupPaging
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
case .trendingStatuses:
var listConfig = UICollectionLayoutListConfiguration(appearance: .grouped)
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)
}
}
@ -85,7 +108,7 @@ class TrendsViewController: UIViewController, CollectionViewController {
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
collectionView.delegate = self
collectionView.dragDelegate = self
collectionView.backgroundColor = .secondarySystemBackground
collectionView.backgroundColor = .appGroupedBackground
collectionView.allowsFocus = true
view.addSubview(collectionView)
@ -96,19 +119,35 @@ class TrendsViewController: UIViewController, CollectionViewController {
private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> {
let sectionHeaderCell = UICollectionView.SupplementaryRegistration<UICollectionViewListCell>(elementKind: UICollectionView.elementKindSectionHeader) { [unowned self] (headerView, collectionView, indexPath) in
let section = self.dataSource.snapshot().sectionIdentifiers[indexPath.section]
let section = self.dataSource.sectionIdentifier(for: indexPath.section)!
var config = UIListContentConfiguration.groupedHeader()
config.text = section.title
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
cell.updateUI(hashtag: hashtag)
}
let trendingLinkCell = UICollectionView.CellRegistration<TrendingLinkCardCollectionViewCell, Card>(cellNib: UINib(nibName: "TrendingLinkCardCollectionViewCell", bundle: .main)) { (cell, indexPath, card) in
cell.updateUI(card: card)
}
let 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
// TODO: filter trends
cell.updateUI(statusID: item.0, state: item.1, filterResult: .allow, precomputedContent: nil)
@ -117,9 +156,16 @@ class TrendsViewController: UIViewController, CollectionViewController {
cell.delegate = self
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
switch item {
case .loadingIndicator:
return collectionView.dequeueConfiguredReusableCell(using: loadingCell, for: indexPath, item: ())
case let .tag(hashtag):
return collectionView.dequeueConfiguredReusableCell(using: trendingHashtagCell, for: indexPath, item: hashtag)
@ -131,11 +177,16 @@ class TrendsViewController: UIViewController, CollectionViewController {
case let .account(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
if elementKind == UICollectionView.elementKindSectionHeader {
return collectionView.dequeueConfiguredReusableSupplementary(using: sectionHeaderCell, for: indexPath)
} else if elementKind == UICollectionView.elementKindSectionFooter {
return collectionView.dequeueConfiguredReusableSupplementary(using: moreCell, for: indexPath)
} else {
return nil
}
@ -169,6 +220,11 @@ class TrendsViewController: UIViewController, CollectionViewController {
}
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.loadingIndicator])
snapshot.appendItems([.loadingIndicator])
await apply(snapshot: snapshot)
snapshot = NSDiffableDataSourceSnapshot()
let hashtagsReq = Client.getTrendingHashtags(limit: 5)
let hashtags = try? await mastodonController.run(hashtagsReq).0
@ -212,9 +268,60 @@ class TrendsViewController: UIViewController, CollectionViewController {
if !Task.isCancelled {
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() {
if isShowingTrends != shouldShowTrends {
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
self.dataSource.apply(snapshot)
self.dataSource.apply(snapshot, animatingDifferences: animatingDifferences)
}.value
}
@ -249,15 +356,26 @@ class TrendsViewController: UIViewController, CollectionViewController {
}
}
extension TrendsViewController {
enum TrendingStatusesState {
case unloaded
case loaded
case loadingOlder
}
}
extension TrendsViewController {
enum Section {
case loadingIndicator
case trendingHashtags
case trendingLinks
case profileSuggestions
case trendingStatuses
var title: String {
var title: String? {
switch self {
case .loadingIndicator:
return nil
case .trendingHashtags:
return "Trending Hashtags"
case .trendingLinks:
@ -270,13 +388,17 @@ extension TrendsViewController {
}
}
enum Item: Equatable, Hashable {
case loadingIndicator
case status(String, CollapseState)
case tag(Hashtag)
case link(Card)
case account(String, Suggestion.Source)
case confirmLoadMoreStatuses(Bool)
static func == (lhs: Item, rhs: Item) -> Bool {
switch (lhs, rhs) {
case (.loadingIndicator, .loadingIndicator):
return true
case let (.status(a, _), .status(b, _)):
return a == b
case let (.tag(a), .tag(b)):
@ -285,6 +407,8 @@ extension TrendsViewController {
return a.url == b.url
case let (.account(a, _), .account(b, _)):
return a == b
case (.confirmLoadMoreStatuses(let a), .confirmLoadMoreStatuses(let b)):
return a == b
default:
return false
}
@ -292,6 +416,8 @@ extension TrendsViewController {
func hash(into hasher: inout Hasher) {
switch self {
case .loadingIndicator:
hasher.combine("loadingIndicator")
case let .status(id, _):
hasher.combine("status")
hasher.combine(id)
@ -304,17 +430,54 @@ extension TrendsViewController {
case let .account(id, _):
hasher.combine("account")
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 {
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) {
guard let item = dataSource.itemIdentifier(for: indexPath) else {
return
}
switch item {
case .loadingIndicator, .confirmLoadMoreStatuses(_):
return
case let .tag(hashtag):
show(HashtagTimelineViewController(for: hashtag, mastodonController: mastodonController), sender: nil)
@ -338,6 +501,9 @@ extension TrendsViewController: UICollectionViewDelegate {
}
switch item {
case .loadingIndicator, .confirmLoadMoreStatuses(_):
return nil
case let .tag(hashtag):
return UIContextMenuConfiguration(identifier: nil) {
HashtagTimelineViewController(for: hashtag, mastodonController: self.mastodonController)
@ -349,12 +515,13 @@ extension TrendsViewController: UICollectionViewDelegate {
guard let url = URL(card.url) else {
return nil
}
let cell = collectionView.cellForItem(at: indexPath)!
return UIContextMenuConfiguration {
let vc = SFSafariViewController(url: url)
vc.preferredControlTintColor = Preferences.shared.accentColor.color
return vc
} actionProvider: { _ in
UIMenu(children: self.actionsForTrendingLink(card: card))
UIMenu(children: self.actionsForTrendingLink(card: card, source: .view(cell)))
}
case let .status(id, state):
@ -422,6 +589,9 @@ extension TrendsViewController: UICollectionViewDragDelegate {
return []
}
switch item {
case .loadingIndicator, .confirmLoadMoreStatuses(_):
return []
case let .tag(hashtag):
guard let url = URL(hashtag.url) else {
return []

View File

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

View File

@ -54,6 +54,7 @@ class EditListAccountsViewController: EnhancedTableViewController {
tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 66
tableView.allowsSelection = false
tableView.backgroundColor = .appGroupedBackground
dataSource = DataSource(tableView: tableView, cellProvider: { (tableView, indexPath, item) -> UITableViewCell? in
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
cell.delegate = self
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
})
dataSource.editListAccountsController = self

View File

@ -45,6 +45,7 @@ class LocalPredicateStatusesViewController: UIViewController, CollectionViewCont
override func loadView() {
var config = UICollectionLayoutListConfiguration(appearance: .plain)
config.backgroundColor = .appBackground
config.leadingSwipeActionsConfigurationProvider = { [unowned self] in
(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.")
}
}
.appGroupedListRowBackground()
Section {
Picker(selection: $duration) {
@ -99,6 +100,7 @@ struct MuteAccountView: View {
Text("The mute will automatically be removed after the selected time.")
}
}
.appGroupedListRowBackground()
Button(action: self.mute) {
if isMuting {
@ -113,7 +115,9 @@ struct MuteAccountView: View {
}
}
.disabled(isMuting)
.appGroupedListRowBackground()
}
.appGroupedListBackground(container: UIHostingController<MuteAccountView>.self)
.alertWithData("Erorr Muting", data: $error, actions: { error in
Button("OK") {}
}, message: { error in

View File

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

View File

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

View File

@ -42,6 +42,7 @@ struct AboutView: View {
}
}
}
.appGroupedListRowBackground()
Section {
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("Issue Tracker", destination: URL(string: "https://git.shadowfacts.net/shadowfacts/Tusker/issues")!)
}
.appGroupedListRowBackground()
}
.listStyle(.insetGrouped)
.appGroupedListBackground(container: PreferencesNavigationController.self)
.sheet(isPresented: $isShowingMailSheet) {
MailSheet(logData: logData)
}

View File

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

View File

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

View File

@ -6,21 +6,19 @@
//
import SwiftUI
import Combine
struct AppearancePrefsView : View {
@ObservedObject var preferences = Preferences.shared
private var theme: Binding<UIUserInterfaceStyle> = Binding(get: {
Preferences.shared.theme
}, set: {
Preferences.shared.theme = $0
NotificationCenter.default.post(name: .themePreferenceChanged, object: nil)
})
private var accentColor: Binding<Preferences.AccentColor> = Binding {
Preferences.shared.accentColor
} set: {
Preferences.shared.accentColor = $0
NotificationCenter.default.post(name: .themePreferenceChanged, object: nil)
private var appearanceChangePublisher: some Publisher<Void, Never> {
preferences.$theme
.map { _ in () }
.merge(with: preferences.$pureBlackDarkMode.map { _ in () },
preferences.$accentColor.map { _ in () }
)
// the prefrence publishers are all willSet, but want to notify after the change, so wait one runloop iteration
.receive(on: DispatchQueue.main)
}
private var useCircularAvatars: Binding<Bool> = Binding(get: {
@ -50,19 +48,27 @@ struct AppearancePrefsView : View {
accountsSection
postsSection
}
.listStyle(InsetGroupedListStyle())
.listStyle(.insetGrouped)
.appGroupedListBackground(container: PreferencesNavigationController.self)
.navigationBarTitle(Text("Appearance"))
}
private var themeSection: some View {
Section {
Picker(selection: theme, label: Text("Theme")) {
Picker(selection: $preferences.theme, label: Text("Theme")) {
Text("Use System Theme").tag(UIUserInterfaceStyle.unspecified)
Text("Light").tag(UIUserInterfaceStyle.light)
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
HStack {
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 {
@ -86,6 +96,7 @@ struct AppearancePrefsView : View {
Text("Hide Custom Emoji in Usernames")
}
}
.appGroupedListRowBackground()
}
private var postsSection: some View {
@ -113,6 +124,7 @@ struct AppearancePrefsView : View {
.navigationTitle("Trailing Swipe Actions")
}
}
.appGroupedListRowBackground()
}
}

View File

@ -18,7 +18,8 @@ struct BehaviorPrefsView: View {
linksSection
contentWarningsSection
}
.listStyle(InsetGroupedListStyle())
.listStyle(.insetGrouped)
.appGroupedListBackground(container: PreferencesNavigationController.self)
.navigationBarTitle(Text("Behavior"))
}
@ -28,6 +29,7 @@ struct BehaviorPrefsView: View {
Text("Require Confirmation Before Reblogging")
}
}
.appGroupedListRowBackground()
}
private var timelineSection: some View {
@ -38,6 +40,7 @@ struct BehaviorPrefsView: View {
} header: {
Text("Timeline")
}
.appGroupedListRowBackground()
}
private var linksSection: some View {
@ -52,6 +55,7 @@ struct BehaviorPrefsView: View {
Text("Always Use Reader Mode in In-App Safari")
}.disabled(!preferences.useInAppSafari)
}
.appGroupedListRowBackground()
}
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")
}
}
.appGroupedListRowBackground()
}
}

View File

@ -19,7 +19,8 @@ struct ComposingPrefsView: View {
replyingSection
writingSection
}
.listStyle(InsetGroupedListStyle())
.listStyle(.insetGrouped)
.appGroupedListBackground(container: PreferencesNavigationController.self)
.navigationBarTitle("Composing")
}
@ -51,6 +52,7 @@ struct ComposingPrefsView: View {
} 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.")
}
.appGroupedListRowBackground()
}
var composingSection: some View {
@ -62,6 +64,7 @@ struct ComposingPrefsView: View {
Text("Require Attachment Descriptions")
}
}
.appGroupedListRowBackground()
}
var replyingSection: some View {
@ -75,6 +78,7 @@ struct ComposingPrefsView: View {
Text("Mention Reblogger")
}
}
.appGroupedListRowBackground()
}
var writingSection: some View {
@ -83,6 +87,7 @@ struct ComposingPrefsView: View {
Text("Show @ and # on Keyboard")
}
}
.appGroupedListRowBackground()
}
}

View File

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

View File

@ -23,8 +23,8 @@ struct OppositeCollapseKeywordsView: View {
ZStack {
// 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
Color(UIColor.secondarySystemBackground)
.edgesIgnoringSafeArea(.bottom)
// Color(UIColor.secondarySystemBackground)
// .edgesIgnoringSafeArea(.bottom)
List {
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)
}
.appGroupedListRowBackground()
}
.animation(.default, value: keywords.map(\.id))
.listStyle(GroupedListStyle())
.listStyle(.grouped)
.appGroupedListBackground(container: PreferencesNavigationController.self)
}
.onAppear(perform: updateAppearance)
.navigationBarTitle(preferences.expandAllContentWarnings ? "Collapse Post CW Keywords" : "Expand Post CW Keywords")

View File

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

View File

@ -42,6 +42,7 @@ class SwipeActionsPrefsViewController: UIViewController, UICollectionViewDelegat
override func loadView() {
let layout = UICollectionViewCompositionalLayout { [unowned self] sectionIndex, environment in
var config = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
config.backgroundColor = .appGroupedBackground
if dataSource.sectionIdentifier(for: sectionIndex) == .selected {
config.headerMode = .supplementary
}
@ -59,6 +60,15 @@ class SwipeActionsPrefsViewController: UIViewController, UICollectionViewDelegat
config.image = UIImage(systemName: item.systemImageName)
cell.contentConfiguration = config
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
return collectionView.dequeueConfiguredReusableCell(using: listCell, for: indexPath, item: itemIdentifier)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -34,25 +34,57 @@ struct ReportAddStatusView: View {
ReportStatusView(status: status, mastodonController: mastodonController)
}
}
.appGroupedListRowBackground()
}
.modifier(ScrollBackgroundModifier())
} else {
ProgressView()
.progressViewStyle(.circular)
.alertWithData("Error Loading Posts", data: $error, actions: { _ in
Button("OK") {}
}, message: { error in
Text(error.localizedDescription)
})
.task { @MainActor in
do {
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
ZStack {
// because the background needs to fill the entire screen
Color.appGroupedBackground
.edgesIgnoringSafeArea(.all)
ProgressView()
.progressViewStyle(.circular)
.alertWithData("Error Loading Posts", data: $error, actions: { _ in
Button("OK") {}
}, message: { error in
Text(error.localizedDescription)
})
.task { @MainActor in
do {
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)
}
}
.appGroupedListRowBackground()
}
.withAppBackgroundIfAvailable()
.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 {
// static var previews: some View {
// ReportSelectRulesView()

View File

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

View File

@ -9,6 +9,7 @@
import UIKit
import Combine
import Pachyderm
import WebURLFoundationExtras
fileprivate let accountCell = "accountCell"
fileprivate let statusCell = "statusCell"
@ -26,17 +27,15 @@ extension SearchResultsViewControllerDelegate {
func selectedSearchResult(status statusID: String) {}
}
class SearchResultsViewController: EnhancedTableViewController {
class SearchResultsViewController: UIViewController, CollectionViewController {
weak var mastodonController: MastodonController!
weak var exploreNavigationController: UINavigationController?
weak var delegate: SearchResultsViewControllerDelegate?
var dataSource: UITableViewDiffableDataSource<Section, Item>!
private var activityIndicator: UIActivityIndicatorView!
private var errorLabel: UILabel!
var collectionView: UICollectionView! { view as? UICollectionView }
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
/// Types of results to search for.
var scope: Scope
@ -50,9 +49,7 @@ class SearchResultsViewController: EnhancedTableViewController {
self.mastodonController = mastodonController
self.scope = scope
super.init(style: .grouped)
dragEnabled = true
super.init(nibName: nil, bundle: nil)
title = NSLocalizedString("Search", comment: "search screen title")
}
@ -61,59 +58,45 @@ class SearchResultsViewController: EnhancedTableViewController {
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() {
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
.debounce(for: .milliseconds(500), scheduler: RunLoop.main)
.map { $0?.trimmingCharacters(in: .whitespacesAndNewlines) }
@ -125,6 +108,66 @@ class SearchResultsViewController: EnhancedTableViewController {
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? {
// 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
@ -149,25 +192,19 @@ class SearchResultsViewController: EnhancedTableViewController {
}
self.currentQuery = query
activityIndicator.isHidden = false
activityIndicator.startAnimating()
errorLabel.isHidden = true
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.loadingIndicator])
snapshot.appendItems([.loadingIndicator])
dataSource.apply(snapshot)
let request = Client.search(query: query, types: scope.resultTypes, resolve: true, limit: 10, following: following)
mastodonController.run(request) { (response) in
switch response {
case let .success(results, _):
guard self.currentQuery == query else { return }
DispatchQueue.main.async {
self.activityIndicator.isHidden = true
self.activityIndicator.stopAnimating()
}
self.showSearchResults(results)
case let .failure(error):
DispatchQueue.main.async {
self.activityIndicator.isHidden = true
self.activityIndicator.stopAnimating()
self.showSearchError(error)
}
}
@ -195,7 +232,6 @@ class SearchResultsViewController: EnhancedTableViewController {
}
}, completion: {
DispatchQueue.main.async {
self.errorLabel.isHidden = true
self.dataSource.apply(snapshot)
}
})
@ -205,8 +241,11 @@ class SearchResultsViewController: EnhancedTableViewController {
let snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
dataSource.apply(snapshot)
errorLabel.isHidden = false
errorLabel.text = error.localizedDescription
let config = ToastConfiguration(from: error, with: "Error Searching", in: self) { [unowned self] toast in
toast.dismissToast(animated: true)
self.performSearch(query: self.currentQuery)
}
showToast(configuration: config, animated: true)
}
@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 {
@ -289,12 +308,15 @@ extension SearchResultsViewController {
extension SearchResultsViewController {
enum Section: CaseIterable {
case loadingIndicator
case accounts
case hashtags
case statuses
var displayName: String {
var displayName: String? {
switch self {
case .loadingIndicator:
return nil
case .accounts:
return NSLocalizedString("People", comment: "accounts search results section")
case .hashtags:
@ -305,12 +327,15 @@ extension SearchResultsViewController {
}
}
enum Item: Hashable {
case loadingIndicator
case account(String)
case hashtag(Hashtag)
case status(String, CollapseState)
func hash(into hasher: inout Hasher) {
switch self {
case .loadingIndicator:
hasher.combine("loadingIndicator")
case let .account(id):
hasher.combine("account")
hasher.combine(id)
@ -322,16 +347,121 @@ extension SearchResultsViewController {
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> {
override func tableView(_ tableView: UITableView, titleForHeaderInSection sectionIndex: Int) -> String? {
let currentSnapshot = snapshot()
for section in Section.allCases where currentSnapshot.indexOfSection(section) == sectionIndex {
return section.displayName
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
switch dataSource.itemIdentifier(for: indexPath) {
case nil, .loadingIndicator:
return
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
}
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) {
self.scope = Scope.allCases[selectedScope]
performSearch(query: searchBar.text?.trimmingCharacters(in: .whitespacesAndNewlines))
let newQuery = 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: StatusTableViewCellDelegate {
func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) {
tableView.beginUpdates()
tableView.endUpdates()
extension SearchResultsViewController: StatusCollectionViewCellDelegate {
func statusCellNeedsReconfigure(_ cell: StatusCollectionViewCell, animated: Bool, completion: (() -> Void)?) {
if let indexPath = collectionView.indexPath(for: cell) {
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
cell.delegate = self
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
cell.delegate = self
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
cell.indicator.startAnimating()

View File

@ -88,7 +88,7 @@ class StatusActionAccountListViewController: UIViewController {
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)
}

View File

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

View File

@ -19,7 +19,7 @@ class TimelineGapCollectionViewCell: UICollectionViewCell {
override var isHighlighted: Bool {
didSet {
backgroundColor = isHighlighted ? .systemFill : .systemGroupedBackground
backgroundColor = isHighlighted ? .appFill : .appGroupedBackground
}
}
@ -38,7 +38,7 @@ class TimelineGapCollectionViewCell: UICollectionViewCell {
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .systemGroupedBackground
backgroundColor = .appGroupedBackground
indicator.isHidden = true
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 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 {
weak var delegate: TimelineViewControllerDelegate?
let timeline: Timeline
weak var mastodonController: MastodonController!
let filterer: Filterer
@ -63,6 +79,7 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
super.viewDidLoad()
var config = UICollectionLayoutListConfiguration(appearance: .plain)
config.backgroundColor = .appBackground
config.leadingSwipeActionsConfigurationProvider = { [unowned self] in
(collectionView.cellForItem(at: $0) as? TimelineStatusCollectionViewCell)?.leadingSwipeActions()
}
@ -99,6 +116,7 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
collectionView.delegate = self
collectionView.dragDelegate = self
collectionView.allowsFocus = true
collectionView.backgroundColor = .appBackground
collectionView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(collectionView)
NSLayoutConstraint.activate([
@ -143,7 +161,9 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
}
.debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)
.sink { [unowned self] _ in
_ = syncPositionIfNecessary(alwaysPrompt: true)
Task {
_ = await syncPositionIfNecessary(alwaysPrompt: true)
}
}
.store(in: &cancellables)
NotificationCenter.default.addObserver(self, selector: #selector(handleStatusDeleted), name: .statusDeleted, object: nil)
@ -225,7 +245,7 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
if case .notLoadedInitial = controller.state {
Task {
if await restoreState() {
await checkPresent(jumpImmediately: false)
await checkPresent(jumpImmediately: false, animateImmediateJump: false)
} else {
await controller.loadInitial()
}
@ -357,28 +377,12 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
@MainActor
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 unloaded = position.statusIDs.filter({ mastodonController.persistentContainer.status(for: $0) == nil })
guard !unloaded.isEmpty else {
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
for id in unloaded {
group.addTask {
@ -399,9 +403,6 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
switch result {
case .success(let status):
statuses.append(status)
let crumb = Breadcrumb(level: .info, category: "TimelineViewController")
crumb.message = "Loaded status \(id)"
SentrySDK.addBreadcrumb(crumb)
case .failure(let error):
let crumb = Breadcrumb(level: .error, category: "TimelineViewController")
crumb.message = "Error loading status"
@ -414,15 +415,6 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
}
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 position.statusIDs != originalPositionStatusIDs {
let crumb = Breadcrumb(level: .info, category: "TimelineViewController")
@ -444,15 +436,6 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
!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
}
@ -563,7 +546,7 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
saveState()
}
private func syncPositionIfNecessary(alwaysPrompt: Bool) -> Bool {
func syncPositionIfNecessary(alwaysPrompt: Bool) async -> Bool {
guard persistsState,
let timelinePosition = mastodonController.persistentContainer.getTimelinePosition(timeline: timeline) else {
return false
@ -591,9 +574,7 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
}
stateRestorationLogger.info("Potential restore with centerStatusID: \(timelinePosition.centerStatusID ?? "<none>")")
if !alwaysPrompt {
Task {
_ = await restoreState()
}
_ = await restoreState()
} else {
var config = ToastConfiguration(title: "Sync Position")
config.edge = .top
@ -615,6 +596,12 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
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)
UIAccessibility.post(notification: .announcement, argument: "Synced Position Updated")
}
@ -652,30 +639,31 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
return
}
self.disappearedAt = nil
if syncPositionIfNecessary(alwaysPrompt: false) {
// no-op
} else {
Task {
await checkPresent(jumpImmediately: false)
Task {
if await syncPositionIfNecessary(alwaysPrompt: false) {
// no-op
} else {
await checkPresent(jumpImmediately: false, animateImmediateJump: false)
}
}
}
func checkPresent(jumpImmediately: Bool) async {
if case .idle = controller.state,
let presentItems = try? await loadInitial(),
!presentItems.isEmpty {
if jumpImmediately {
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.statuses])
snapshot.appendItems(presentItems.map { .status(id: $0, collapseState: .unknown, filterState: .unknown) }, toSection: .statuses)
dataSource.apply(snapshot, animatingDifferences: false) {
self.collectionView.scrollToItem(at: IndexPath(row: 0, section: 0), at: .top, animated: false)
UIAccessibility.post(notification: .screenChanged, argument: self.collectionView.cellForItem(at: IndexPath(row: 0, section: 0)))
}
} else {
insertPresentItemsAndShowJumpToast(presentItems)
func checkPresent(jumpImmediately: Bool, animateImmediateJump: Bool) async {
guard case .idle = controller.state,
let presentItems = try? await loadInitial(),
!presentItems.isEmpty else {
return
}
if jumpImmediately {
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.statuses])
snapshot.appendItems(presentItems.map { .status(id: $0, collapseState: .unknown, filterState: .unknown) }, toSection: .statuses)
dataSource.apply(snapshot, animatingDifferences: animateImmediateJump) {
self.collectionView.scrollToItem(at: IndexPath(row: 0, section: 0), at: .top, animated: animateImmediateJump)
UIAccessibility.post(notification: .screenChanged, argument: self.collectionView.cellForItem(at: IndexPath(row: 0, section: 0)))
}
} else {
insertPresentItemsAndShowJumpToast(presentItems)
}
}
@ -778,6 +766,12 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
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)
}
}

View File

@ -13,12 +13,22 @@ import Combine
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 federatedTitle = NSLocalizedString("Federated", comment: "federated timeline tab title")
private let localTitle = NSLocalizedString("Local", comment: "local timeline tab title")
weak var mastodonController: MastodonController!
private let jumpButton = TimelineJumpButton()
private var cancellables = Set<AnyCancellable>()
init(mastodonController: MastodonController) {
@ -46,30 +56,17 @@ class TimelinesPageViewController: SegmentedPageViewController<TimelinesPageView
customizeItem.accessibilityLabel = "Customize Timelines"
navigationItem.rightBarButtonItem = customizeItem
let jumpToPresentName = 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
jumpToPresentName.addAttribute(.accessibilitySpeechIPANotation, value: "ˈprɛ.zənt", range: NSRange(location: "Jump to ".count, length: "Present".count))
segmentedControl.accessibilityCustomActions = [
UIAccessibilityCustomAction(attributedName: jumpToPresentName, actionHandler: { [unowned self] _ in
guard let vc = currentViewController as? TimelineViewController else {
return false
}
Task {
await vc.checkPresent(jumpImmediately: true)
}
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
}),
]
jumpButton.action = { [unowned self] mode in
switch mode {
case .jump:
await (self.currentViewController as! TimelineViewController).checkPresent(jumpImmediately: true, animateImmediateJump: true)
case .sync:
_ = await (self.currentViewController as! TimelineViewController).syncPositionIfNecessary(alwaysPrompt: false)
}
}
let jumpItem = UIBarButtonItem(customView: jumpButton)
jumpItem.accessibilityAttributedLabel = Self.jumpToPresentTitle
navigationItem.leftBarButtonItem = jumpItem
mastodonController.accountPreferences.publisher(for: \.pinnedTimelinesData)
.map { _ in () }
@ -87,6 +84,23 @@ class TimelinesPageViewController: SegmentedPageViewController<TimelinesPageView
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) {
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.backgroundColor = .systemBackground
blurView.backgroundColor = .appBackground
blurView.layer.cornerRadius = 15
blurView.layer.cornerCurve = .continuous
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 {
return []
}
return [
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
guard let self = self else { return }
let draft = self.mastodonController!.createDraft()

View File

@ -79,7 +79,7 @@ class SegmentedPageViewController<Page: SegmentedPageViewControllerPage>: UIView
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
view.backgroundColor = .appBackground
selectPage(initialPage, animated: false)
@ -94,6 +94,9 @@ class SegmentedPageViewController<Page: SegmentedPageViewControllerPage>: UIView
}
}
func configureViewController(_ viewController: UIViewController) {
}
func selectPage(_ page: Page, animated: Bool) {
guard pages.contains(page) else {
fatalError("invalid page \(page) that is not in SegmentedPageViewController.pages")
@ -116,6 +119,7 @@ class SegmentedPageViewController<Page: SegmentedPageViewControllerPage>: UIView
newController = existing
} else {
newController = pageProvider(page)
configureViewController(newController)
pageControllers[page] = newController
}
@ -130,7 +134,16 @@ class SegmentedPageViewController<Page: SegmentedPageViewControllerPage>: UIView
animated != .none else {
currentViewController?.removeViewAndController()
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
return
}

View File

@ -12,8 +12,6 @@ import Pachyderm
protocol TuskerNavigationDelegate: UIViewController, ToastableViewController {
var apiController: MastodonController! { get }
func conversation(mainStatusID: String, state: CollapseState) -> ConversationViewController
}
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) {
self.selected(status: statusID, state: .unknown)
}
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) {
@ -119,15 +113,11 @@ extension TuskerNavigationDelegate {
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)
vc.animationSourceView = sourceView
vc.transitioningDelegate = self
return vc
}
func showLoadingLargeImage(url: URL, cache: ImageCache, description: String?, animatingFrom sourceView: UIImageView) {
present(loadingLargeImage(url: url, cache: cache, description: description, animatingFrom: sourceView), animated: true)
present(vc, animated: true)
}
func gallery(attachments: [Attachment], sourceViews: [UIImageView?], startIndex: Int) -> GalleryViewController {
@ -183,16 +173,6 @@ extension TuskerNavigationDelegate {
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 {

View File

@ -21,6 +21,16 @@ class AlbumTableViewCell: UITableViewCell {
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) {
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) {
super.init(frame: frame)
backgroundColor = .systemGroupedBackground
backgroundColor = .appGroupedBackground
let label = UILabel()
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:)
// causes the mask to be ignored. See FB7832297
let snapshotContainer = UIView(frame: snapshot.bounds)
snapshotContainer.backgroundColor = .systemBackground
snapshotContainer.backgroundColor = .appBackground
snapshotContainer.addSubview(snapshot)
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) {
super.init(frame: frame)
backgroundColor = .systemBackground
hashtagLabel.font = .preferredFont(forTextStyle: .title2)
hashtagLabel.adjustsFontForContentSizeCategory = true
peopleTodayLabel.font = .preferredFont(forTextStyle: .caption1)
@ -60,6 +58,16 @@ class TrendingHashtagCollectionViewCell: UICollectionViewCell {
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) {
hashtagLabel.text = "#\(hashtag.name)"
historyView.setHistory(hashtag.history)

View File

@ -39,6 +39,16 @@ class InstanceTableViewCell: UITableViewCell {
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) {
self.selectorInstance = instance
self.instance = nil

View File

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

View File

@ -48,6 +48,16 @@ class ActionNotificationGroupTableViewCell: UITableViewCell {
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() {
for case let imageView as UIImageView in actionAvatarStackView.arrangedSubviews {
imageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: imageView)
@ -263,7 +273,7 @@ extension ActionNotificationGroupTableViewCell: SelectableTableViewCell {
default:
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
delegate.show(vc)
}
@ -272,9 +282,6 @@ extension ActionNotificationGroupTableViewCell: SelectableTableViewCell {
extension ActionNotificationGroupTableViewCell: MenuPreviewProvider {
func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? {
return (content: {
guard let delegate = self.delegate else {
return nil
}
let notifications = self.group.notifications
let accountIDs = notifications.map { $0.account.id }
let action: StatusActionAccountListViewController.ActionType
@ -286,7 +293,7 @@ extension ActionNotificationGroupTableViewCell: MenuPreviewProvider {
default:
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
return vc
}, actions: {

View File

@ -43,6 +43,16 @@ class FollowNotificationGroupTableViewCell: UITableViewCell {
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() {
for case let imageView as UIImageView in avatarStackView.arrangedSubviews {
imageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: imageView)
@ -213,7 +223,9 @@ extension FollowNotificationGroupTableViewCell: SelectableTableViewCell {
case 1:
delegate?.selected(account: accountIDs.first!)
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()
}
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() {
avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * 30

View File

@ -38,6 +38,16 @@ class PollFinishedTableViewCell: UITableViewCell {
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) {
guard let statusID = notification.status?.id,
let status = delegate?.apiController.persistentContainer.status(for: statusID),
@ -120,8 +130,7 @@ extension PollFinishedTableViewCell: SelectableTableViewCell {
let status = notification?.status else {
return
}
let vc = delegate.conversation(mainStatusID: status.id, state: .unknown)
delegate.show(vc)
delegate.selected(status: status.id)
}
}
@ -133,7 +142,7 @@ extension PollFinishedTableViewCell: MenuPreviewProvider {
return nil
}
return (content: {
delegate.conversation(mainStatusID: statusID, state: .unknown)
ConversationViewController(for: statusID, state: .unknown, mastodonController: delegate.apiController)
}, actions: {
delegate.actionsForStatus(status, source: .view(self))
})

View File

@ -35,6 +35,16 @@ class StatusUpdatedNotificationTableViewCell: UITableViewCell {
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) {
guard notification.kind == .update,
let status = notification.status else {
@ -109,8 +119,7 @@ extension StatusUpdatedNotificationTableViewCell: SelectableTableViewCell {
let status = notification?.status else {
return
}
let vc = delegate.conversation(mainStatusID: status.id, state: .unknown)
delegate.show(vc)
delegate.selected(status: status.id)
}
}
@ -122,7 +131,7 @@ extension StatusUpdatedNotificationTableViewCell: MenuPreviewProvider {
return nil
}
return (content: {
delegate.conversation(mainStatusID: statusID, state: .unknown)
ConversationViewController(for: statusID, state: .unknown, mastodonController: delegate.apiController)
}, actions: {
delegate.actionsForStatus(status, source: .view(self))
})

View File

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

View File

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

View File

@ -79,7 +79,6 @@
</textView>
<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"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints>
<constraint firstAttribute="height" constant="128" placeholder="YES" id="xbR-M6-H0I"/>
</constraints>

View File

@ -317,6 +317,12 @@ class ConversationMainStatusCollectionViewCell: UICollectionViewListCell, Status
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
func updateUI(statusID: String, state: CollapseState) {
@ -410,7 +416,7 @@ class ConversationMainStatusCollectionViewCell: UICollectionViewListCell, Status
guard let delegate else {
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
vc.showInacurateCountWarning = true
delegate.show(vc)
@ -420,7 +426,7 @@ class ConversationMainStatusCollectionViewCell: UICollectionViewListCell, Status
guard let delegate else {
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
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
override var isAccessibilityElement: Bool {

View File

@ -93,6 +93,16 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
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() {
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 dismissOnScroll = true
var dismissAutomaticallyAfter: TimeInterval? = nil
var onAppear: ((UIViewPropertyAnimator?) -> Void)?
var onDismiss: ((UIViewPropertyAnimator?) -> Void)?
init(title: String) {
self.title = title

View File

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

View File

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

View File

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