Compare commits
24 Commits
afed69e43e
...
4731801893
Author | SHA1 | Date |
---|---|---|
Shadowfacts | 4731801893 | |
Shadowfacts | 4293b51c31 | |
Shadowfacts | ecadb83c6d | |
Shadowfacts | 205bdffebd | |
Shadowfacts | ae7ca9c91c | |
Shadowfacts | 841119949b | |
Shadowfacts | b63f663947 | |
Shadowfacts | 00a23b525f | |
Shadowfacts | ea85b11945 | |
Shadowfacts | d8c7eb5cf5 | |
Shadowfacts | 8bc185ecf9 | |
Shadowfacts | 1832e64ad7 | |
Shadowfacts | 87bc1f5f75 | |
Shadowfacts | 6e2f6bb8e9 | |
Shadowfacts | 74d8adfffe | |
Shadowfacts | 99127b617b | |
Shadowfacts | 65ea72c07f | |
Shadowfacts | 04ca932a01 | |
Shadowfacts | 4ea2dff8f1 | |
Shadowfacts | 9f0176350c | |
Shadowfacts | dac1e1fe3f | |
Shadowfacts | 471d3459a6 | |
Shadowfacts | 512eec09a8 | |
Shadowfacts | 20c4c4bb2f |
12
CHANGELOG.md
12
CHANGELOG.md
|
@ -1,5 +1,17 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2023.4 (73)
|
||||||
|
Features/Improvements:
|
||||||
|
- Add preference for non-pure-black dark mode
|
||||||
|
- Add Jump to Present button to timelines
|
||||||
|
- Improve status collapse animation in search results screen
|
||||||
|
- Add more trending links/hashtags/profiles buttons to Trends screen
|
||||||
|
- Add infinite scrolling to trending links/hashtags screens
|
||||||
|
- Add Share action to trending link context menu
|
||||||
|
|
||||||
|
Bugfixes:
|
||||||
|
- Fix icon in suggested profile popover not adjusting to dark mode
|
||||||
|
|
||||||
## 2023.4 (72)
|
## 2023.4 (72)
|
||||||
Features/Improvements:
|
Features/Improvements:
|
||||||
- Consolidate Trends into a single screen
|
- Consolidate Trends into a single screen
|
||||||
|
|
|
@ -417,32 +417,46 @@ public class Client {
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Instance
|
// MARK: - Instance
|
||||||
public static func getTrendingHashtags(limit: Int? = nil) -> Request<[Hashtag]> {
|
public static func getTrendingHashtagsDeprecated(limit: Int? = nil, offset: Int? = nil) -> Request<[Hashtag]> {
|
||||||
let parameters: [Parameter]
|
var parameters: [Parameter] = []
|
||||||
if let limit = limit {
|
if let limit {
|
||||||
parameters = ["limit" => limit]
|
parameters.append("limit" => limit)
|
||||||
} else {
|
}
|
||||||
parameters = []
|
if let offset {
|
||||||
|
parameters.append("offset" => offset)
|
||||||
}
|
}
|
||||||
return Request<[Hashtag]>(method: .get, path: "/api/v1/trends", queryParameters: parameters)
|
return Request<[Hashtag]>(method: .get, path: "/api/v1/trends", queryParameters: parameters)
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func getTrendingStatuses(limit: Int? = nil) -> Request<[Status]> {
|
public static func getTrendingHashtags(limit: Int? = nil, offset: Int? = nil) -> Request<[Hashtag]> {
|
||||||
let parameters: [Parameter]
|
var parameters: [Parameter] = []
|
||||||
if let limit = limit {
|
if let limit {
|
||||||
parameters = ["limit" => limit]
|
parameters.append("limit" => limit)
|
||||||
} else {
|
}
|
||||||
parameters = []
|
if let offset {
|
||||||
|
parameters.append("offset" => offset)
|
||||||
|
}
|
||||||
|
return Request<[Hashtag]>(method: .get, path: "/api/v1/trends/tags", queryParameters: parameters)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func getTrendingStatuses(limit: Int? = nil, offset: Int? = nil) -> Request<[Status]> {
|
||||||
|
var parameters: [Parameter] = []
|
||||||
|
if let limit {
|
||||||
|
parameters.append("limit" => limit)
|
||||||
|
}
|
||||||
|
if let offset {
|
||||||
|
parameters.append("offset" => offset)
|
||||||
}
|
}
|
||||||
return Request(method: .get, path: "/api/v1/trends/statuses", queryParameters: parameters)
|
return Request(method: .get, path: "/api/v1/trends/statuses", queryParameters: parameters)
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func getTrendingLinks(limit: Int? = nil) -> Request<[Card]> {
|
public static func getTrendingLinks(limit: Int? = nil, offset: Int? = nil) -> Request<[Card]> {
|
||||||
let parameters: [Parameter]
|
var parameters: [Parameter] = []
|
||||||
if let limit = limit {
|
if let limit {
|
||||||
parameters = ["limit" => limit]
|
parameters.append("limit" => limit)
|
||||||
} else {
|
}
|
||||||
parameters = []
|
if let offset {
|
||||||
|
parameters.append("offset" => offset)
|
||||||
}
|
}
|
||||||
return Request(method: .get, path: "/api/v1/trends/links", queryParameters: parameters)
|
return Request(method: .get, path: "/api/v1/trends/links", queryParameters: parameters)
|
||||||
}
|
}
|
||||||
|
|
|
@ -171,7 +171,7 @@ extension Account: CustomDebugStringConvertible {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Account {
|
extension Account {
|
||||||
public struct Field: Codable {
|
public struct Field: Codable, Equatable {
|
||||||
public let name: String
|
public let name: String
|
||||||
public let value: String
|
public let value: String
|
||||||
public let verifiedAt: Date?
|
public let verifiedAt: Date?
|
||||||
|
|
|
@ -41,8 +41,6 @@
|
||||||
D6114E1127F899B30080E273 /* TrendingLinksViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6114E1027F899B30080E273 /* TrendingLinksViewController.swift */; };
|
D6114E1127F899B30080E273 /* TrendingLinksViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6114E1027F899B30080E273 /* TrendingLinksViewController.swift */; };
|
||||||
D6114E1327F89B440080E273 /* TrendingLinkTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6114E1227F89B440080E273 /* TrendingLinkTableViewCell.swift */; };
|
D6114E1327F89B440080E273 /* TrendingLinkTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6114E1227F89B440080E273 /* TrendingLinkTableViewCell.swift */; };
|
||||||
D6114E1727F8BB210080E273 /* VersionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6114E1627F8BB210080E273 /* VersionTests.swift */; };
|
D6114E1727F8BB210080E273 /* VersionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6114E1627F8BB210080E273 /* VersionTests.swift */; };
|
||||||
D611C2CF232DC61100C86A49 /* HashtagTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D611C2CD232DC61100C86A49 /* HashtagTableViewCell.swift */; };
|
|
||||||
D611C2D0232DC61100C86A49 /* HashtagTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D611C2CE232DC61100C86A49 /* HashtagTableViewCell.xib */; };
|
|
||||||
D61A45E628DC0F2F002BE511 /* ConfirmLoadMoreCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61A45E528DC0F2F002BE511 /* ConfirmLoadMoreCollectionViewCell.swift */; };
|
D61A45E628DC0F2F002BE511 /* ConfirmLoadMoreCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61A45E528DC0F2F002BE511 /* ConfirmLoadMoreCollectionViewCell.swift */; };
|
||||||
D61A45E828DF477D002BE511 /* LoadingCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61A45E728DF477D002BE511 /* LoadingCollectionViewCell.swift */; };
|
D61A45E828DF477D002BE511 /* LoadingCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61A45E728DF477D002BE511 /* LoadingCollectionViewCell.swift */; };
|
||||||
D61A45EA28DF51EE002BE511 /* TimelineLikeCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61A45E928DF51EE002BE511 /* TimelineLikeCollectionViewController.swift */; };
|
D61A45EA28DF51EE002BE511 /* TimelineLikeCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61A45E928DF51EE002BE511 /* TimelineLikeCollectionViewController.swift */; };
|
||||||
|
@ -202,6 +200,7 @@
|
||||||
D681E4D7246E32290053414F /* StatusActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D681E4D6246E32290053414F /* StatusActivityItemSource.swift */; };
|
D681E4D7246E32290053414F /* StatusActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D681E4D6246E32290053414F /* StatusActivityItemSource.swift */; };
|
||||||
D681E4D9246E346E0053414F /* AccountActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D681E4D8246E346E0053414F /* AccountActivityItemSource.swift */; };
|
D681E4D9246E346E0053414F /* AccountActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D681E4D8246E346E0053414F /* AccountActivityItemSource.swift */; };
|
||||||
D68232F72464F4FD00325FB8 /* ComposeDrawingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68232F62464F4FD00325FB8 /* ComposeDrawingViewController.swift */; };
|
D68232F72464F4FD00325FB8 /* ComposeDrawingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68232F62464F4FD00325FB8 /* ComposeDrawingViewController.swift */; };
|
||||||
|
D68329EF299540050026EB24 /* MoreTrendsFooterCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68329EE299540050026EB24 /* MoreTrendsFooterCollectionViewCell.swift */; };
|
||||||
D686BBE324FBF8110068E6AA /* WrappedProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D686BBE224FBF8110068E6AA /* WrappedProgressView.swift */; };
|
D686BBE324FBF8110068E6AA /* WrappedProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D686BBE224FBF8110068E6AA /* WrappedProgressView.swift */; };
|
||||||
D6895DC228D65274006341DA /* CustomAlertController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6895DC128D65274006341DA /* CustomAlertController.swift */; };
|
D6895DC228D65274006341DA /* CustomAlertController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6895DC128D65274006341DA /* CustomAlertController.swift */; };
|
||||||
D6895DC428D65342006341DA /* ConfirmReblogStatusPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6895DC328D65342006341DA /* ConfirmReblogStatusPreviewView.swift */; };
|
D6895DC428D65342006341DA /* ConfirmReblogStatusPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6895DC328D65342006341DA /* ConfirmReblogStatusPreviewView.swift */; };
|
||||||
|
@ -299,6 +298,8 @@
|
||||||
D6C3F4F7298ED7F70009FCFF /* FavoritesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C3F4F6298ED7F70009FCFF /* FavoritesViewController.swift */; };
|
D6C3F4F7298ED7F70009FCFF /* FavoritesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C3F4F6298ED7F70009FCFF /* FavoritesViewController.swift */; };
|
||||||
D6C3F4F9298EDBF20009FCFF /* ConversationTree.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C3F4F8298EDBF20009FCFF /* ConversationTree.swift */; };
|
D6C3F4F9298EDBF20009FCFF /* ConversationTree.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C3F4F8298EDBF20009FCFF /* ConversationTree.swift */; };
|
||||||
D6C3F4FB299035650009FCFF /* TrendsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C3F4FA299035650009FCFF /* TrendsViewController.swift */; };
|
D6C3F4FB299035650009FCFF /* TrendsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C3F4FA299035650009FCFF /* TrendsViewController.swift */; };
|
||||||
|
D6C3F5172991C1A00009FCFF /* View+AppListStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C3F5162991C1A00009FCFF /* View+AppListStyle.swift */; };
|
||||||
|
D6C3F5192991F5D60009FCFF /* TimelineJumpButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C3F5182991F5D60009FCFF /* TimelineJumpButton.swift */; };
|
||||||
D6C693EF216192C2007D6A6D /* TuskerNavigationDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */; };
|
D6C693EF216192C2007D6A6D /* TuskerNavigationDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */; };
|
||||||
D6C693FC2162FE6F007D6A6D /* LoadingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693FB2162FE6F007D6A6D /* LoadingViewController.swift */; };
|
D6C693FC2162FE6F007D6A6D /* LoadingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693FB2162FE6F007D6A6D /* LoadingViewController.swift */; };
|
||||||
D6C693FE2162FEEA007D6A6D /* UIViewController+Children.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693FD2162FEEA007D6A6D /* UIViewController+Children.swift */; };
|
D6C693FE2162FEEA007D6A6D /* UIViewController+Children.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693FD2162FEEA007D6A6D /* UIViewController+Children.swift */; };
|
||||||
|
@ -325,6 +326,8 @@
|
||||||
D6D4DDF0212518A200E1C4BB /* TuskerUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D4DDEF212518A200E1C4BB /* TuskerUITests.swift */; };
|
D6D4DDF0212518A200E1C4BB /* TuskerUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D4DDEF212518A200E1C4BB /* TuskerUITests.swift */; };
|
||||||
D6D706A029466649000827ED /* ScrollingSegmentedControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D7069F29466649000827ED /* ScrollingSegmentedControl.swift */; };
|
D6D706A029466649000827ED /* ScrollingSegmentedControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D7069F29466649000827ED /* ScrollingSegmentedControl.swift */; };
|
||||||
D6D706A72948D4D0000827ED /* TimlineState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D706A62948D4D0000827ED /* TimlineState.swift */; };
|
D6D706A72948D4D0000827ED /* TimlineState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D706A62948D4D0000827ED /* TimlineState.swift */; };
|
||||||
|
D6D94955298963A900C59229 /* Colors.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D94954298963A900C59229 /* Colors.swift */; };
|
||||||
|
D6D9498D298CBB4000C59229 /* TrendingStatusCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D9498C298CBB4000C59229 /* TrendingStatusCollectionViewCell.swift */; };
|
||||||
D6D9498F298EB79400C59229 /* CopyableLable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D9498E298EB79400C59229 /* CopyableLable.swift */; };
|
D6D9498F298EB79400C59229 /* CopyableLable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D9498E298EB79400C59229 /* CopyableLable.swift */; };
|
||||||
D6DD2A3F273C1F4900386A6C /* ComposeAttachmentImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD2A3E273C1F4900386A6C /* ComposeAttachmentImage.swift */; };
|
D6DD2A3F273C1F4900386A6C /* ComposeAttachmentImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD2A3E273C1F4900386A6C /* ComposeAttachmentImage.swift */; };
|
||||||
D6DD2A45273D6C5700386A6C /* GIFImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD2A44273D6C5700386A6C /* GIFImageView.swift */; };
|
D6DD2A45273D6C5700386A6C /* GIFImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD2A44273D6C5700386A6C /* GIFImageView.swift */; };
|
||||||
|
@ -332,6 +335,7 @@
|
||||||
D6DD353F22F502EC00A9563A /* Preferences+Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD353E22F502EC00A9563A /* Preferences+Notification.swift */; };
|
D6DD353F22F502EC00A9563A /* Preferences+Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD353E22F502EC00A9563A /* Preferences+Notification.swift */; };
|
||||||
D6DD8FFD298495A8002AD3FD /* LogoutService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD8FFC298495A8002AD3FD /* LogoutService.swift */; };
|
D6DD8FFD298495A8002AD3FD /* LogoutService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD8FFC298495A8002AD3FD /* LogoutService.swift */; };
|
||||||
D6DD8FFF2984D327002AD3FD /* BookmarksViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD8FFE2984D327002AD3FD /* BookmarksViewController.swift */; };
|
D6DD8FFF2984D327002AD3FD /* BookmarksViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD8FFE2984D327002AD3FD /* BookmarksViewController.swift */; };
|
||||||
|
D6DD996B2998611A0015C962 /* SuggestedProfilesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD996A2998611A0015C962 /* SuggestedProfilesViewController.swift */; };
|
||||||
D6DF95C12533F5DE0027A9B6 /* RelationshipMO.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DF95C02533F5DE0027A9B6 /* RelationshipMO.swift */; };
|
D6DF95C12533F5DE0027A9B6 /* RelationshipMO.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DF95C02533F5DE0027A9B6 /* RelationshipMO.swift */; };
|
||||||
D6DFC69E242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DFC69D242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift */; };
|
D6DFC69E242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DFC69D242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift */; };
|
||||||
D6DFC6A0242C4CCC00ACC392 /* Weak.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DFC69F242C4CCC00ACC392 /* Weak.swift */; };
|
D6DFC6A0242C4CCC00ACC392 /* Weak.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DFC69F242C4CCC00ACC392 /* Weak.swift */; };
|
||||||
|
@ -451,8 +455,6 @@
|
||||||
D6114E1027F899B30080E273 /* TrendingLinksViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingLinksViewController.swift; sourceTree = "<group>"; };
|
D6114E1027F899B30080E273 /* TrendingLinksViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingLinksViewController.swift; sourceTree = "<group>"; };
|
||||||
D6114E1227F89B440080E273 /* TrendingLinkTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingLinkTableViewCell.swift; sourceTree = "<group>"; };
|
D6114E1227F89B440080E273 /* TrendingLinkTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingLinkTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
D6114E1627F8BB210080E273 /* VersionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionTests.swift; sourceTree = "<group>"; };
|
D6114E1627F8BB210080E273 /* VersionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionTests.swift; sourceTree = "<group>"; };
|
||||||
D611C2CD232DC61100C86A49 /* HashtagTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagTableViewCell.swift; sourceTree = "<group>"; };
|
|
||||||
D611C2CE232DC61100C86A49 /* HashtagTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = HashtagTableViewCell.xib; sourceTree = "<group>"; };
|
|
||||||
D61A45E528DC0F2F002BE511 /* ConfirmLoadMoreCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmLoadMoreCollectionViewCell.swift; sourceTree = "<group>"; };
|
D61A45E528DC0F2F002BE511 /* ConfirmLoadMoreCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmLoadMoreCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||||
D61A45E728DF477D002BE511 /* LoadingCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingCollectionViewCell.swift; sourceTree = "<group>"; };
|
D61A45E728DF477D002BE511 /* LoadingCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||||
D61A45E928DF51EE002BE511 /* TimelineLikeCollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineLikeCollectionViewController.swift; sourceTree = "<group>"; };
|
D61A45E928DF51EE002BE511 /* TimelineLikeCollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineLikeCollectionViewController.swift; sourceTree = "<group>"; };
|
||||||
|
@ -612,6 +614,7 @@
|
||||||
D681E4D6246E32290053414F /* StatusActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusActivityItemSource.swift; sourceTree = "<group>"; };
|
D681E4D6246E32290053414F /* StatusActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusActivityItemSource.swift; sourceTree = "<group>"; };
|
||||||
D681E4D8246E346E0053414F /* AccountActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountActivityItemSource.swift; sourceTree = "<group>"; };
|
D681E4D8246E346E0053414F /* AccountActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountActivityItemSource.swift; sourceTree = "<group>"; };
|
||||||
D68232F62464F4FD00325FB8 /* ComposeDrawingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeDrawingViewController.swift; sourceTree = "<group>"; };
|
D68232F62464F4FD00325FB8 /* ComposeDrawingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeDrawingViewController.swift; sourceTree = "<group>"; };
|
||||||
|
D68329EE299540050026EB24 /* MoreTrendsFooterCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoreTrendsFooterCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||||
D686BBE224FBF8110068E6AA /* WrappedProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WrappedProgressView.swift; sourceTree = "<group>"; };
|
D686BBE224FBF8110068E6AA /* WrappedProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WrappedProgressView.swift; sourceTree = "<group>"; };
|
||||||
D6895DC128D65274006341DA /* CustomAlertController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomAlertController.swift; sourceTree = "<group>"; };
|
D6895DC128D65274006341DA /* CustomAlertController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomAlertController.swift; sourceTree = "<group>"; };
|
||||||
D6895DC328D65342006341DA /* ConfirmReblogStatusPreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmReblogStatusPreviewView.swift; sourceTree = "<group>"; };
|
D6895DC328D65342006341DA /* ConfirmReblogStatusPreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmReblogStatusPreviewView.swift; sourceTree = "<group>"; };
|
||||||
|
@ -710,6 +713,8 @@
|
||||||
D6C3F4F6298ED7F70009FCFF /* FavoritesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesViewController.swift; sourceTree = "<group>"; };
|
D6C3F4F6298ED7F70009FCFF /* FavoritesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesViewController.swift; sourceTree = "<group>"; };
|
||||||
D6C3F4F8298EDBF20009FCFF /* ConversationTree.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationTree.swift; sourceTree = "<group>"; };
|
D6C3F4F8298EDBF20009FCFF /* ConversationTree.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationTree.swift; sourceTree = "<group>"; };
|
||||||
D6C3F4FA299035650009FCFF /* TrendsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendsViewController.swift; sourceTree = "<group>"; };
|
D6C3F4FA299035650009FCFF /* TrendsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendsViewController.swift; sourceTree = "<group>"; };
|
||||||
|
D6C3F5162991C1A00009FCFF /* View+AppListStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+AppListStyle.swift"; sourceTree = "<group>"; };
|
||||||
|
D6C3F5182991F5D60009FCFF /* TimelineJumpButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineJumpButton.swift; sourceTree = "<group>"; };
|
||||||
D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TuskerNavigationDelegate.swift; sourceTree = "<group>"; };
|
D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TuskerNavigationDelegate.swift; sourceTree = "<group>"; };
|
||||||
D6C693FB2162FE6F007D6A6D /* LoadingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingViewController.swift; sourceTree = "<group>"; };
|
D6C693FB2162FE6F007D6A6D /* LoadingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingViewController.swift; sourceTree = "<group>"; };
|
||||||
D6C693FD2162FEEA007D6A6D /* UIViewController+Children.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Children.swift"; sourceTree = "<group>"; };
|
D6C693FD2162FEEA007D6A6D /* UIViewController+Children.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Children.swift"; sourceTree = "<group>"; };
|
||||||
|
@ -743,6 +748,8 @@
|
||||||
D6D7069F29466649000827ED /* ScrollingSegmentedControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollingSegmentedControl.swift; sourceTree = "<group>"; };
|
D6D7069F29466649000827ED /* ScrollingSegmentedControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollingSegmentedControl.swift; sourceTree = "<group>"; };
|
||||||
D6D706A62948D4D0000827ED /* TimlineState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimlineState.swift; sourceTree = "<group>"; };
|
D6D706A62948D4D0000827ED /* TimlineState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimlineState.swift; sourceTree = "<group>"; };
|
||||||
D6D706A829498C82000827ED /* Tusker.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Tusker.xcconfig; sourceTree = "<group>"; };
|
D6D706A829498C82000827ED /* Tusker.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Tusker.xcconfig; sourceTree = "<group>"; };
|
||||||
|
D6D94954298963A900C59229 /* Colors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Colors.swift; sourceTree = "<group>"; };
|
||||||
|
D6D9498C298CBB4000C59229 /* TrendingStatusCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingStatusCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||||
D6D9498E298EB79400C59229 /* CopyableLable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyableLable.swift; sourceTree = "<group>"; };
|
D6D9498E298EB79400C59229 /* CopyableLable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyableLable.swift; sourceTree = "<group>"; };
|
||||||
D6DD2A3E273C1F4900386A6C /* ComposeAttachmentImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeAttachmentImage.swift; sourceTree = "<group>"; };
|
D6DD2A3E273C1F4900386A6C /* ComposeAttachmentImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeAttachmentImage.swift; sourceTree = "<group>"; };
|
||||||
D6DD2A44273D6C5700386A6C /* GIFImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GIFImageView.swift; sourceTree = "<group>"; };
|
D6DD2A44273D6C5700386A6C /* GIFImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GIFImageView.swift; sourceTree = "<group>"; };
|
||||||
|
@ -750,6 +757,7 @@
|
||||||
D6DD353E22F502EC00A9563A /* Preferences+Notification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Preferences+Notification.swift"; sourceTree = "<group>"; };
|
D6DD353E22F502EC00A9563A /* Preferences+Notification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Preferences+Notification.swift"; sourceTree = "<group>"; };
|
||||||
D6DD8FFC298495A8002AD3FD /* LogoutService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogoutService.swift; sourceTree = "<group>"; };
|
D6DD8FFC298495A8002AD3FD /* LogoutService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogoutService.swift; sourceTree = "<group>"; };
|
||||||
D6DD8FFE2984D327002AD3FD /* BookmarksViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksViewController.swift; sourceTree = "<group>"; };
|
D6DD8FFE2984D327002AD3FD /* BookmarksViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksViewController.swift; sourceTree = "<group>"; };
|
||||||
|
D6DD996A2998611A0015C962 /* SuggestedProfilesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestedProfilesViewController.swift; sourceTree = "<group>"; };
|
||||||
D6DF95C02533F5DE0027A9B6 /* RelationshipMO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelationshipMO.swift; sourceTree = "<group>"; };
|
D6DF95C02533F5DE0027A9B6 /* RelationshipMO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelationshipMO.swift; sourceTree = "<group>"; };
|
||||||
D6DFC69D242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackpadScrollGestureRecognizer.swift; sourceTree = "<group>"; };
|
D6DFC69D242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackpadScrollGestureRecognizer.swift; sourceTree = "<group>"; };
|
||||||
D6DFC69F242C4CCC00ACC392 /* Weak.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Weak.swift; sourceTree = "<group>"; };
|
D6DFC69F242C4CCC00ACC392 /* Weak.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Weak.swift; sourceTree = "<group>"; };
|
||||||
|
@ -853,8 +861,6 @@
|
||||||
D611C2CC232DC5FC00C86A49 /* Hashtag Cell */ = {
|
D611C2CC232DC5FC00C86A49 /* Hashtag Cell */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
D611C2CD232DC61100C86A49 /* HashtagTableViewCell.swift */,
|
|
||||||
D611C2CE232DC61100C86A49 /* HashtagTableViewCell.xib */,
|
|
||||||
D6E77D08286D25FA00D8B732 /* TrendingHashtagCollectionViewCell.swift */,
|
D6E77D08286D25FA00D8B732 /* TrendingHashtagCollectionViewCell.swift */,
|
||||||
);
|
);
|
||||||
path = "Hashtag Cell";
|
path = "Hashtag Cell";
|
||||||
|
@ -943,6 +949,8 @@
|
||||||
D693A72D25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift */,
|
D693A72D25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift */,
|
||||||
D693A72E25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.xib */,
|
D693A72E25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.xib */,
|
||||||
D6C3F4FA299035650009FCFF /* TrendsViewController.swift */,
|
D6C3F4FA299035650009FCFF /* TrendsViewController.swift */,
|
||||||
|
D68329EE299540050026EB24 /* MoreTrendsFooterCollectionViewCell.swift */,
|
||||||
|
D6DD996A2998611A0015C962 /* SuggestedProfilesViewController.swift */,
|
||||||
);
|
);
|
||||||
path = Explore;
|
path = Explore;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1050,6 +1058,7 @@
|
||||||
D66C900A28DAB7FD00217BF2 /* TimelineViewController.swift */,
|
D66C900A28DAB7FD00217BF2 /* TimelineViewController.swift */,
|
||||||
D6ADB6E728E8C878009924AB /* PublicTimelineDescriptionCollectionViewCell.swift */,
|
D6ADB6E728E8C878009924AB /* PublicTimelineDescriptionCollectionViewCell.swift */,
|
||||||
D6D12B2E2925D66500D528E1 /* TimelineGapCollectionViewCell.swift */,
|
D6D12B2E2925D66500D528E1 /* TimelineGapCollectionViewCell.swift */,
|
||||||
|
D6C3F5182991F5D60009FCFF /* TimelineJumpButton.swift */,
|
||||||
);
|
);
|
||||||
path = Timeline;
|
path = Timeline;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1181,6 +1190,7 @@
|
||||||
D6ADB6E928E91C30009924AB /* TimelineStatusCollectionViewCell.swift */,
|
D6ADB6E928E91C30009924AB /* TimelineStatusCollectionViewCell.swift */,
|
||||||
D601FA60297B539E00A8E8B5 /* ConversationMainStatusCollectionViewCell.swift */,
|
D601FA60297B539E00A8E8B5 /* ConversationMainStatusCollectionViewCell.swift */,
|
||||||
D6ADB6EB28EA73CB009924AB /* StatusContentContainer.swift */,
|
D6ADB6EB28EA73CB009924AB /* StatusContentContainer.swift */,
|
||||||
|
D6D9498C298CBB4000C59229 /* TrendingStatusCollectionViewCell.swift */,
|
||||||
);
|
);
|
||||||
path = Status;
|
path = Status;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1274,6 +1284,7 @@
|
||||||
D6DD353E22F502EC00A9563A /* Preferences+Notification.swift */,
|
D6DD353E22F502EC00A9563A /* Preferences+Notification.swift */,
|
||||||
D6BC9DB4232D4CE3002CA326 /* NotificationsMode.swift */,
|
D6BC9DB4232D4CE3002CA326 /* NotificationsMode.swift */,
|
||||||
D61F75872932DB6000C0B37F /* StatusSwipeAction.swift */,
|
D61F75872932DB6000C0B37F /* StatusSwipeAction.swift */,
|
||||||
|
D6D94954298963A900C59229 /* Colors.swift */,
|
||||||
);
|
);
|
||||||
path = Preferences;
|
path = Preferences;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1303,6 +1314,7 @@
|
||||||
D63CC70F2911F1E4000E19DE /* UIScrollView+Top.swift */,
|
D63CC70F2911F1E4000E19DE /* UIScrollView+Top.swift */,
|
||||||
D61F758F29353B4300C0B37F /* FileManager+Size.swift */,
|
D61F758F29353B4300C0B37F /* FileManager+Size.swift */,
|
||||||
D61F75AC293AF39000C0B37F /* Filter+Helpers.swift */,
|
D61F75AC293AF39000C0B37F /* Filter+Helpers.swift */,
|
||||||
|
D6C3F5162991C1A00009FCFF /* View+AppListStyle.swift */,
|
||||||
);
|
);
|
||||||
path = Extensions;
|
path = Extensions;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1843,7 +1855,6 @@
|
||||||
isa = PBXResourcesBuildPhase;
|
isa = PBXResourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
D611C2D0232DC61100C86A49 /* HashtagTableViewCell.xib in Resources */,
|
|
||||||
D6E77D0D286E6B7300D8B732 /* TrendingLinkCardCollectionViewCell.xib in Resources */,
|
D6E77D0D286E6B7300D8B732 /* TrendingLinkCardCollectionViewCell.xib in Resources */,
|
||||||
D693A73025CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.xib in Resources */,
|
D693A73025CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.xib in Resources */,
|
||||||
D61AC1D9232EA42D00C54D2D /* InstanceTableViewCell.xib in Resources */,
|
D61AC1D9232EA42D00C54D2D /* InstanceTableViewCell.xib in Resources */,
|
||||||
|
@ -1960,6 +1971,7 @@
|
||||||
D6093FB725BE0CF3004811E6 /* TrendHistoryView.swift in Sources */,
|
D6093FB725BE0CF3004811E6 /* TrendHistoryView.swift in Sources */,
|
||||||
D6EBF01723C55E0D00AE061B /* UISceneSession+MastodonController.swift in Sources */,
|
D6EBF01723C55E0D00AE061B /* UISceneSession+MastodonController.swift in Sources */,
|
||||||
D61F759229365C6C00C0B37F /* CollectionViewController.swift in Sources */,
|
D61F759229365C6C00C0B37F /* CollectionViewController.swift in Sources */,
|
||||||
|
D6DD996B2998611A0015C962 /* SuggestedProfilesViewController.swift in Sources */,
|
||||||
D6DD8FFD298495A8002AD3FD /* LogoutService.swift in Sources */,
|
D6DD8FFD298495A8002AD3FD /* LogoutService.swift in Sources */,
|
||||||
04DACE8C212CB14B009840C4 /* MainTabBarViewController.swift in Sources */,
|
04DACE8C212CB14B009840C4 /* MainTabBarViewController.swift in Sources */,
|
||||||
D68E525D24A3E8F00054355A /* InlineTrendsViewController.swift in Sources */,
|
D68E525D24A3E8F00054355A /* InlineTrendsViewController.swift in Sources */,
|
||||||
|
@ -1993,7 +2005,6 @@
|
||||||
0411610022B442870030A9B7 /* LoadingLargeImageViewController.swift in Sources */,
|
0411610022B442870030A9B7 /* LoadingLargeImageViewController.swift in Sources */,
|
||||||
D6A4DCE525537C7A00D9DE31 /* FastSwitchingAccountView.swift in Sources */,
|
D6A4DCE525537C7A00D9DE31 /* FastSwitchingAccountView.swift in Sources */,
|
||||||
D6A4DCCC2553667800D9DE31 /* FastAccountSwitcherViewController.swift in Sources */,
|
D6A4DCCC2553667800D9DE31 /* FastAccountSwitcherViewController.swift in Sources */,
|
||||||
D611C2CF232DC61100C86A49 /* HashtagTableViewCell.swift in Sources */,
|
|
||||||
D627944F23A9C99800D38C68 /* EditListAccountsViewController.swift in Sources */,
|
D627944F23A9C99800D38C68 /* EditListAccountsViewController.swift in Sources */,
|
||||||
D6945C3423AC6431005C403C /* AddSavedHashtagViewController.swift in Sources */,
|
D6945C3423AC6431005C403C /* AddSavedHashtagViewController.swift in Sources */,
|
||||||
D6F6A552291F098700F496A8 /* RenameListService.swift in Sources */,
|
D6F6A552291F098700F496A8 /* RenameListService.swift in Sources */,
|
||||||
|
@ -2002,6 +2013,7 @@
|
||||||
D6AC956723C4347E008C9946 /* MainSceneDelegate.swift in Sources */,
|
D6AC956723C4347E008C9946 /* MainSceneDelegate.swift in Sources */,
|
||||||
D6311C5025B3765B00B27539 /* ImageDataCache.swift in Sources */,
|
D6311C5025B3765B00B27539 /* ImageDataCache.swift in Sources */,
|
||||||
D6BC9DD7232D7811002CA326 /* TimelinesPageViewController.swift in Sources */,
|
D6BC9DD7232D7811002CA326 /* TimelinesPageViewController.swift in Sources */,
|
||||||
|
D6D9498D298CBB4000C59229 /* TrendingStatusCollectionViewCell.swift in Sources */,
|
||||||
D60E2F2E244248BF005F8713 /* MastodonCachePersistentStore.swift in Sources */,
|
D60E2F2E244248BF005F8713 /* MastodonCachePersistentStore.swift in Sources */,
|
||||||
D620483623D38075008A63EF /* ContentTextView.swift in Sources */,
|
D620483623D38075008A63EF /* ContentTextView.swift in Sources */,
|
||||||
D651C5B42915B00400236EF6 /* ProfileFieldsView.swift in Sources */,
|
D651C5B42915B00400236EF6 /* ProfileFieldsView.swift in Sources */,
|
||||||
|
@ -2011,6 +2023,7 @@
|
||||||
D6A3BC802321B7E600FD64D5 /* FollowNotificationGroupTableViewCell.swift in Sources */,
|
D6A3BC802321B7E600FD64D5 /* FollowNotificationGroupTableViewCell.swift in Sources */,
|
||||||
D627944D23A9A03D00D38C68 /* ListTimelineViewController.swift in Sources */,
|
D627944D23A9A03D00D38C68 /* ListTimelineViewController.swift in Sources */,
|
||||||
D6D12B2F2925D66500D528E1 /* TimelineGapCollectionViewCell.swift in Sources */,
|
D6D12B2F2925D66500D528E1 /* TimelineGapCollectionViewCell.swift in Sources */,
|
||||||
|
D6D94955298963A900C59229 /* Colors.swift in Sources */,
|
||||||
D6945C3823AC739F005C403C /* InstanceTimelineViewController.swift in Sources */,
|
D6945C3823AC739F005C403C /* InstanceTimelineViewController.swift in Sources */,
|
||||||
D625E4822588262A0074BB2B /* DraggableTableViewCell.swift in Sources */,
|
D625E4822588262A0074BB2B /* DraggableTableViewCell.swift in Sources */,
|
||||||
D68E525B24A3D77E0054355A /* TuskerRootViewController.swift in Sources */,
|
D68E525B24A3D77E0054355A /* TuskerRootViewController.swift in Sources */,
|
||||||
|
@ -2066,6 +2079,7 @@
|
||||||
D6A00B1D26379FC900316AD4 /* PollOptionsView.swift in Sources */,
|
D6A00B1D26379FC900316AD4 /* PollOptionsView.swift in Sources */,
|
||||||
D6DF95C12533F5DE0027A9B6 /* RelationshipMO.swift in Sources */,
|
D6DF95C12533F5DE0027A9B6 /* RelationshipMO.swift in Sources */,
|
||||||
D6ADB6EE28EA74E8009924AB /* UIView+Configure.swift in Sources */,
|
D6ADB6EE28EA74E8009924AB /* UIView+Configure.swift in Sources */,
|
||||||
|
D68329EF299540050026EB24 /* MoreTrendsFooterCollectionViewCell.swift in Sources */,
|
||||||
D623A5412635FB3C0095BD04 /* PollOptionView.swift in Sources */,
|
D623A5412635FB3C0095BD04 /* PollOptionView.swift in Sources */,
|
||||||
D61F75B1293BD85300C0B37F /* CreateFilterService.swift in Sources */,
|
D61F75B1293BD85300C0B37F /* CreateFilterService.swift in Sources */,
|
||||||
D65C6BF525478A9C00A6E89C /* BackgroundableViewController.swift in Sources */,
|
D65C6BF525478A9C00A6E89C /* BackgroundableViewController.swift in Sources */,
|
||||||
|
@ -2087,6 +2101,7 @@
|
||||||
D6CA8CDE296387310050C433 /* SaveToPhotosActivity.swift in Sources */,
|
D6CA8CDE296387310050C433 /* SaveToPhotosActivity.swift in Sources */,
|
||||||
D6B936712829F72900237D0E /* NSManagedObjectContext+Helpers.swift in Sources */,
|
D6B936712829F72900237D0E /* NSManagedObjectContext+Helpers.swift in Sources */,
|
||||||
D68A76EE295369C7001DA1B3 /* AcknowledgementsView.swift in Sources */,
|
D68A76EE295369C7001DA1B3 /* AcknowledgementsView.swift in Sources */,
|
||||||
|
D6C3F5192991F5D60009FCFF /* TimelineJumpButton.swift in Sources */,
|
||||||
D63CC7102911F1E4000E19DE /* UIScrollView+Top.swift in Sources */,
|
D63CC7102911F1E4000E19DE /* UIScrollView+Top.swift in Sources */,
|
||||||
D6F6A557291F4F1600F496A8 /* MuteAccountView.swift in Sources */,
|
D6F6A557291F4F1600F496A8 /* MuteAccountView.swift in Sources */,
|
||||||
D65B4B8B297879E900DABDFB /* AccountFollowsViewController.swift in Sources */,
|
D65B4B8B297879E900DABDFB /* AccountFollowsViewController.swift in Sources */,
|
||||||
|
@ -2238,6 +2253,7 @@
|
||||||
D6E426B325337C7000C02E1C /* CustomEmojiImageView.swift in Sources */,
|
D6E426B325337C7000C02E1C /* CustomEmojiImageView.swift in Sources */,
|
||||||
D6D4DDD0212518A000E1C4BB /* AppDelegate.swift in Sources */,
|
D6D4DDD0212518A000E1C4BB /* AppDelegate.swift in Sources */,
|
||||||
D61F75B3293BD89C00C0B37F /* UpdateFilterService.swift in Sources */,
|
D61F75B3293BD89C00C0B37F /* UpdateFilterService.swift in Sources */,
|
||||||
|
D6C3F5172991C1A00009FCFF /* View+AppListStyle.swift in Sources */,
|
||||||
D65B4B6A297777D900DABDFB /* StatusNotFoundView.swift in Sources */,
|
D65B4B6A297777D900DABDFB /* StatusNotFoundView.swift in Sources */,
|
||||||
D6412B0924B0291E00F5412E /* MyProfileViewController.swift in Sources */,
|
D6412B0924B0291E00F5412E /* MyProfileViewController.swift in Sources */,
|
||||||
D601FA5D297B2E6F00A8E8B5 /* ConversationCollectionViewController.swift in Sources */,
|
D601FA5D297B2E6F00A8E8B5 /* ConversationCollectionViewController.swift in Sources */,
|
||||||
|
@ -2408,7 +2424,7 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 72;
|
CURRENT_PROJECT_VERSION = 73;
|
||||||
INFOPLIST_FILE = Tusker/Info.plist;
|
INFOPLIST_FILE = Tusker/Info.plist;
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
|
@ -2473,7 +2489,7 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
|
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 72;
|
CURRENT_PROJECT_VERSION = 73;
|
||||||
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||||
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
|
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
|
||||||
|
@ -2624,7 +2640,7 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 72;
|
CURRENT_PROJECT_VERSION = 73;
|
||||||
INFOPLIST_FILE = Tusker/Info.plist;
|
INFOPLIST_FILE = Tusker/Info.plist;
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
|
@ -2652,7 +2668,7 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 72;
|
CURRENT_PROJECT_VERSION = 73;
|
||||||
INFOPLIST_FILE = Tusker/Info.plist;
|
INFOPLIST_FILE = Tusker/Info.plist;
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
|
@ -2757,7 +2773,7 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
|
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 72;
|
CURRENT_PROJECT_VERSION = 73;
|
||||||
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||||
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
|
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
|
||||||
|
@ -2783,7 +2799,7 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
|
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 72;
|
CURRENT_PROJECT_VERSION = 73;
|
||||||
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||||
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
|
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
|
||||||
|
|
|
@ -20,6 +20,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||||
configureSentry()
|
configureSentry()
|
||||||
swizzleStatusBar()
|
swizzleStatusBar()
|
||||||
|
swizzlePresentationController()
|
||||||
|
|
||||||
AppShortcutItem.createItems(for: application)
|
AppShortcutItem.createItems(for: application)
|
||||||
|
|
||||||
|
@ -154,4 +155,24 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||||
Logging.general.error("Unable to swizzle status bar manager")
|
Logging.general.error("Unable to swizzle status bar manager")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func swizzlePresentationController() {
|
||||||
|
var originalIMP: IMP?
|
||||||
|
let imp = imp_implementationWithBlock({ (self: UIPresentationController) in
|
||||||
|
let new = UITraitCollection(pureBlackDarkMode: self.presentingViewController.traitCollection.pureBlackDarkMode)
|
||||||
|
if let existing = self.overrideTraitCollection {
|
||||||
|
self.overrideTraitCollection = UITraitCollection(traitsFrom: [existing, new])
|
||||||
|
} else {
|
||||||
|
self.overrideTraitCollection = new
|
||||||
|
}
|
||||||
|
let original = unsafeBitCast(originalIMP!, to: (@convention(c) (UIPresentationController) -> Void).self)
|
||||||
|
original(self)
|
||||||
|
} as (@convention(block) (UIPresentationController) -> Void))
|
||||||
|
let sel = Selector(["Necessary", "If", "Traits", "update", "_"].reversed().joined())
|
||||||
|
originalIMP = class_replaceMethod(UIPresentationController.self, sel, imp, "v@:")
|
||||||
|
if originalIMP == nil {
|
||||||
|
Logging.general.error("Unable to swizzle presentation controller")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -38,6 +38,7 @@ class Preferences: Codable, ObservableObject {
|
||||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
|
||||||
self.theme = try container.decode(UIUserInterfaceStyle.self, forKey: .theme)
|
self.theme = try container.decode(UIUserInterfaceStyle.self, forKey: .theme)
|
||||||
|
self.pureBlackDarkMode = try container.decodeIfPresent(Bool.self, forKey: .pureBlackDarkMode) ?? true
|
||||||
self.accentColor = try container.decodeIfPresent(AccentColor.self, forKey: .accentColor) ?? .default
|
self.accentColor = try container.decodeIfPresent(AccentColor.self, forKey: .accentColor) ?? .default
|
||||||
self.avatarStyle = try container.decode(AvatarStyle.self, forKey: .avatarStyle)
|
self.avatarStyle = try container.decode(AvatarStyle.self, forKey: .avatarStyle)
|
||||||
self.hideCustomEmojiInUsernames = try container.decode(Bool.self, forKey: .hideCustomEmojiInUsernames)
|
self.hideCustomEmojiInUsernames = try container.decode(Bool.self, forKey: .hideCustomEmojiInUsernames)
|
||||||
|
@ -92,6 +93,7 @@ class Preferences: Codable, ObservableObject {
|
||||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||||
|
|
||||||
try container.encode(theme, forKey: .theme)
|
try container.encode(theme, forKey: .theme)
|
||||||
|
try container.encode(pureBlackDarkMode, forKey: .pureBlackDarkMode)
|
||||||
try container.encode(accentColor, forKey: .accentColor)
|
try container.encode(accentColor, forKey: .accentColor)
|
||||||
try container.encode(avatarStyle, forKey: .avatarStyle)
|
try container.encode(avatarStyle, forKey: .avatarStyle)
|
||||||
try container.encode(hideCustomEmojiInUsernames, forKey: .hideCustomEmojiInUsernames)
|
try container.encode(hideCustomEmojiInUsernames, forKey: .hideCustomEmojiInUsernames)
|
||||||
|
@ -140,6 +142,7 @@ class Preferences: Codable, ObservableObject {
|
||||||
|
|
||||||
// MARK: Appearance
|
// MARK: Appearance
|
||||||
@Published var theme = UIUserInterfaceStyle.unspecified
|
@Published var theme = UIUserInterfaceStyle.unspecified
|
||||||
|
@Published var pureBlackDarkMode = true
|
||||||
@Published var accentColor = AccentColor.default
|
@Published var accentColor = AccentColor.default
|
||||||
@Published var avatarStyle = AvatarStyle.roundRect
|
@Published var avatarStyle = AvatarStyle.roundRect
|
||||||
@Published var hideCustomEmojiInUsernames = false
|
@Published var hideCustomEmojiInUsernames = false
|
||||||
|
@ -202,6 +205,7 @@ class Preferences: Codable, ObservableObject {
|
||||||
|
|
||||||
private enum CodingKeys: String, CodingKey {
|
private enum CodingKeys: String, CodingKey {
|
||||||
case theme
|
case theme
|
||||||
|
case pureBlackDarkMode
|
||||||
case accentColor
|
case accentColor
|
||||||
case avatarStyle
|
case avatarStyle
|
||||||
case hideCustomEmojiInUsernames
|
case hideCustomEmojiInUsernames
|
||||||
|
|
|
@ -243,8 +243,13 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDelegate
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func themePrefChanged() {
|
@objc func themePrefChanged() {
|
||||||
window?.overrideUserInterfaceStyle = Preferences.shared.theme
|
guard let window else { return }
|
||||||
window?.tintColor = Preferences.shared.accentColor.color
|
window.overrideUserInterfaceStyle = Preferences.shared.theme
|
||||||
|
window.tintColor = Preferences.shared.accentColor.color
|
||||||
|
let key = ["Controller", "Presentation", "root", "_"].reversed().joined()
|
||||||
|
if let rootPresentationController = window.value(forKey: key) as? UIPresentationController {
|
||||||
|
rootPresentationController.overrideTraitCollection = UITraitCollection(pureBlackDarkMode: Preferences.shared.pureBlackDarkMode)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func showAddAccount() {
|
func showAddAccount() {
|
||||||
|
|
|
@ -41,7 +41,8 @@ class AccountFollowsListViewController: UIViewController, CollectionViewControll
|
||||||
}
|
}
|
||||||
|
|
||||||
override func loadView() {
|
override func loadView() {
|
||||||
var config = UICollectionLayoutListConfiguration(appearance: .grouped)
|
var config = UICollectionLayoutListConfiguration(appearance: .plain)
|
||||||
|
config.backgroundColor = .appBackground
|
||||||
config.itemSeparatorHandler = { [unowned self] indexPath, sectionConfig in
|
config.itemSeparatorHandler = { [unowned self] indexPath, sectionConfig in
|
||||||
guard let item = self.dataSource.itemIdentifier(for: indexPath) else {
|
guard let item = self.dataSource.itemIdentifier(for: indexPath) else {
|
||||||
return sectionConfig
|
return sectionConfig
|
||||||
|
@ -65,6 +66,16 @@ class AccountFollowsListViewController: UIViewController, CollectionViewControll
|
||||||
let accountCell = UICollectionView.CellRegistration<AccountCollectionViewCell, String> { [unowned self] cell, indexPath, item in
|
let accountCell = UICollectionView.CellRegistration<AccountCollectionViewCell, String> { [unowned self] cell, indexPath, item in
|
||||||
cell.delegate = self
|
cell.delegate = self
|
||||||
cell.updateUI(accountID: item)
|
cell.updateUI(accountID: item)
|
||||||
|
|
||||||
|
cell.configurationUpdateHandler = { cell, state in
|
||||||
|
var config = UIBackgroundConfiguration.listPlainCell().updated(for: state)
|
||||||
|
if state.isHighlighted || state.isSelected {
|
||||||
|
config.backgroundColor = .appSelectedCellBackground
|
||||||
|
} else {
|
||||||
|
config.backgroundColor = .appBackground
|
||||||
|
}
|
||||||
|
cell.backgroundConfiguration = config
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let loadingCell = UICollectionView.CellRegistration<LoadingCollectionViewCell, Void> { cell, indexPath, item in
|
let loadingCell = UICollectionView.CellRegistration<LoadingCollectionViewCell, Void> { cell, indexPath, item in
|
||||||
cell.indicator.startAnimating()
|
cell.indicator.startAnimating()
|
||||||
|
|
|
@ -31,7 +31,8 @@ class AccountListViewController: UIViewController, CollectionViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
override func loadView() {
|
override func loadView() {
|
||||||
let config = UICollectionLayoutListConfiguration(appearance: .grouped)
|
var config = UICollectionLayoutListConfiguration(appearance: .grouped)
|
||||||
|
config.backgroundColor = .appGroupedBackground
|
||||||
let layout = UICollectionViewCompositionalLayout.list(using: config)
|
let layout = UICollectionViewCompositionalLayout.list(using: config)
|
||||||
view = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
view = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
||||||
collectionView.delegate = self
|
collectionView.delegate = self
|
||||||
|
|
|
@ -72,7 +72,8 @@ class AssetCollectionViewController: UIViewController, UICollectionViewDelegate
|
||||||
// bottom ignores safe area because we want cells to underflow bottom of the screen on notched iPhones
|
// bottom ignores safe area because we want cells to underflow bottom of the screen on notched iPhones
|
||||||
view.bottomAnchor.constraint(equalTo: collectionView.bottomAnchor),
|
view.bottomAnchor.constraint(equalTo: collectionView.bottomAnchor),
|
||||||
])
|
])
|
||||||
view.backgroundColor = .systemBackground
|
view.backgroundColor = .appBackground
|
||||||
|
collectionView.backgroundColor = .appBackground
|
||||||
|
|
||||||
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(donePressed))
|
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(donePressed))
|
||||||
|
|
||||||
|
|
|
@ -34,6 +34,7 @@ class AssetCollectionsListViewController: UITableViewController {
|
||||||
tableView.register(UINib(nibName: "AlbumTableViewCell", bundle: .main), forCellReuseIdentifier: "albumCell")
|
tableView.register(UINib(nibName: "AlbumTableViewCell", bundle: .main), forCellReuseIdentifier: "albumCell")
|
||||||
|
|
||||||
tableView.allowsFocus = true
|
tableView.allowsFocus = true
|
||||||
|
tableView.backgroundColor = .appGroupedBackground
|
||||||
|
|
||||||
dataSource = DataSource(tableView: tableView, cellProvider: { (tableView, indexPath, item) -> UITableViewCell? in
|
dataSource = DataSource(tableView: tableView, cellProvider: { (tableView, indexPath, item) -> UITableViewCell? in
|
||||||
switch item {
|
switch item {
|
||||||
|
|
|
@ -112,7 +112,7 @@ struct ComposeAttachmentsList: View {
|
||||||
self.isShowingAssetPickerPopover = false
|
self.isShowingAssetPickerPopover = false
|
||||||
}
|
}
|
||||||
// on iPadOS 16, this is necessary to show the dark color in the popover arrow
|
// on iPadOS 16, this is necessary to show the dark color in the popover arrow
|
||||||
.background(Color(.systemBackground))
|
.background(Color(.appBackground))
|
||||||
.environment(\.colorScheme, .dark)
|
.environment(\.colorScheme, .dark)
|
||||||
.edgesIgnoringSafeArea(.bottom)
|
.edgesIgnoringSafeArea(.bottom)
|
||||||
.withSheetDetentsIfAvailable()
|
.withSheetDetentsIfAvailable()
|
||||||
|
|
|
@ -111,7 +111,7 @@ struct ComposePollView: View {
|
||||||
|
|
||||||
private var backgroundColor: Color {
|
private var backgroundColor: Color {
|
||||||
// in light mode, .secondarySystemBackground has a blue-ish hue, which we don't want
|
// in light mode, .secondarySystemBackground has a blue-ish hue, which we don't want
|
||||||
colorScheme == .dark ? Color(UIColor.secondarySystemBackground) : Color(white: 0.95)
|
colorScheme == .dark ? Color.appFill : Color(white: 0.95)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var buttonBackgroundColor: Color {
|
private var buttonBackgroundColor: Color {
|
||||||
|
@ -191,7 +191,7 @@ struct ComposePollOption: View {
|
||||||
|
|
||||||
private var textField: some View {
|
private var textField: some View {
|
||||||
var field = ComposeEmojiTextField(text: $option.text, placeholder: "Option \(optionIndex + 1)", maxLength: mastodonController.instance?.pollsConfiguration?.maxCharactersPerOption)
|
var field = ComposeEmojiTextField(text: $option.text, placeholder: "Option \(optionIndex + 1)", maxLength: mastodonController.instance?.pollsConfiguration?.maxCharactersPerOption)
|
||||||
return field.backgroundColor(.systemBackground)
|
return field.backgroundColor(.appBackground)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func removeOption() {
|
private func removeOption() {
|
||||||
|
@ -216,7 +216,7 @@ struct ComposePollOption: View {
|
||||||
.cornerRadius(radiusFraction * size)
|
.cornerRadius(radiusFraction * size)
|
||||||
|
|
||||||
Rectangle()
|
Rectangle()
|
||||||
.foregroundColor(Color(UIColor.systemBackground))
|
.foregroundColor(Color(UIColor.appBackground))
|
||||||
.frame(width: innerSize, height: innerSize)
|
.frame(width: innerSize, height: innerSize)
|
||||||
.cornerRadius(radiusFraction * innerSize)
|
.cornerRadius(radiusFraction * innerSize)
|
||||||
}
|
}
|
||||||
|
|
|
@ -94,6 +94,10 @@ struct ComposeView: View {
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack(alignment: .top) {
|
ZStack(alignment: .top) {
|
||||||
|
// just using .background doesn't work; for some reason it gets inset immediately after the software keyboard is dismissed
|
||||||
|
Color.appBackground
|
||||||
|
.edgesIgnoringSafeArea(.all)
|
||||||
|
|
||||||
mainList
|
mainList
|
||||||
.scrollDismissesKeyboardInteractivelyIfAvailable()
|
.scrollDismissesKeyboardInteractivelyIfAvailable()
|
||||||
|
|
||||||
|
@ -124,7 +128,7 @@ struct ComposeView: View {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.sheet(isPresented: $uiState.isShowingDraftsList) {
|
.sheet(isPresented: $uiState.isShowingDraftsList) {
|
||||||
DraftsView(currentDraft: draft, mastodonController: mastodonController)
|
DraftsRepresentable(currentDraft: draft, mastodonController: mastodonController)
|
||||||
}
|
}
|
||||||
.actionSheet(isPresented: $uiState.isShowingSaveDraftSheet, content: self.saveAndCloseSheet)
|
.actionSheet(isPresented: $uiState.isShowingSaveDraftSheet, content: self.saveAndCloseSheet)
|
||||||
.alert(isPresented: $isShowingPostErrorAlert) {
|
.alert(isPresented: $isShowingPostErrorAlert) {
|
||||||
|
@ -169,11 +173,13 @@ struct ComposeView: View {
|
||||||
)
|
)
|
||||||
.listRowInsets(EdgeInsets(top: 8, leading: 8, bottom: 4, trailing: 8))
|
.listRowInsets(EdgeInsets(top: 8, leading: 8, bottom: 4, trailing: 8))
|
||||||
.listRowSeparator(.hidden)
|
.listRowSeparator(.hidden)
|
||||||
|
.listRowBackground(Color.appBackground)
|
||||||
}
|
}
|
||||||
|
|
||||||
header
|
header
|
||||||
.listRowInsets(EdgeInsets(top: draft.inReplyToID == nil ? 8 : 4, leading: 8, bottom: 4, trailing: 8))
|
.listRowInsets(EdgeInsets(top: draft.inReplyToID == nil ? 8 : 4, leading: 8, bottom: 4, trailing: 8))
|
||||||
.listRowSeparator(.hidden)
|
.listRowSeparator(.hidden)
|
||||||
|
.listRowBackground(Color.appBackground)
|
||||||
|
|
||||||
if uiState.draft.contentWarningEnabled {
|
if uiState.draft.contentWarningEnabled {
|
||||||
ComposeEmojiTextField(
|
ComposeEmojiTextField(
|
||||||
|
@ -184,6 +190,7 @@ struct ComposeView: View {
|
||||||
)
|
)
|
||||||
.listRowInsets(EdgeInsets(top: 4, leading: 8, bottom: 4, trailing: 8))
|
.listRowInsets(EdgeInsets(top: 4, leading: 8, bottom: 4, trailing: 8))
|
||||||
.listRowSeparator(.hidden)
|
.listRowSeparator(.hidden)
|
||||||
|
.listRowBackground(Color.appBackground)
|
||||||
}
|
}
|
||||||
|
|
||||||
MainComposeTextView(
|
MainComposeTextView(
|
||||||
|
@ -192,17 +199,20 @@ struct ComposeView: View {
|
||||||
)
|
)
|
||||||
.listRowInsets(EdgeInsets(top: 4, leading: 8, bottom: 4, trailing: 8))
|
.listRowInsets(EdgeInsets(top: 4, leading: 8, bottom: 4, trailing: 8))
|
||||||
.listRowSeparator(.hidden)
|
.listRowSeparator(.hidden)
|
||||||
|
.listRowBackground(Color.appBackground)
|
||||||
|
|
||||||
if let poll = draft.poll {
|
if let poll = draft.poll {
|
||||||
ComposePollView(draft: draft, poll: poll)
|
ComposePollView(draft: draft, poll: poll)
|
||||||
.listRowInsets(EdgeInsets(top: 4, leading: 8, bottom: 4, trailing: 8))
|
.listRowInsets(EdgeInsets(top: 4, leading: 8, bottom: 4, trailing: 8))
|
||||||
.listRowSeparator(.hidden)
|
.listRowSeparator(.hidden)
|
||||||
|
.listRowBackground(Color.appBackground)
|
||||||
}
|
}
|
||||||
|
|
||||||
ComposeAttachmentsList(
|
ComposeAttachmentsList(
|
||||||
draft: draft
|
draft: draft
|
||||||
)
|
)
|
||||||
.listRowInsets(EdgeInsets(top: 4, leading: 8, bottom: 8, trailing: 8))
|
.listRowInsets(EdgeInsets(top: 4, leading: 8, bottom: 8, trailing: 8))
|
||||||
|
.listRowBackground(Color.appBackground)
|
||||||
}
|
}
|
||||||
.animation(.default, value: draft.poll?.options.count)
|
.animation(.default, value: draft.poll?.options.count)
|
||||||
.scrollDismissesKeyboardInteractivelyIfAvailable()
|
.scrollDismissesKeyboardInteractivelyIfAvailable()
|
||||||
|
@ -319,7 +329,7 @@ struct ComposeView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension View {
|
extension View {
|
||||||
@available(iOS, obsoleted: 16.0)
|
@available(iOS, obsoleted: 16.0)
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
func scrollDismissesKeyboardInteractivelyIfAvailable() -> some View {
|
func scrollDismissesKeyboardInteractivelyIfAvailable() -> some View {
|
||||||
|
|
|
@ -8,6 +8,21 @@
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
@available(iOS, obsoleted: 16.0)
|
||||||
|
struct DraftsRepresentable: UIViewControllerRepresentable {
|
||||||
|
typealias UIViewControllerType = UIHostingController<DraftsView>
|
||||||
|
|
||||||
|
let currentDraft: Draft
|
||||||
|
let mastodonController: MastodonController
|
||||||
|
|
||||||
|
func makeUIViewController(context: Context) -> UIHostingController<DraftsView> {
|
||||||
|
return UIHostingController(rootView: DraftsView(currentDraft: currentDraft, mastodonController: mastodonController))
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateUIViewController(_ uiViewController: UIHostingController<DraftsView>, context: Context) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct DraftsView: View {
|
struct DraftsView: View {
|
||||||
let currentDraft: Draft
|
let currentDraft: Draft
|
||||||
// don't pass this in via the environment b/c it crashes on macOS (at least, in Designed for iPad mode) since the environment doesn't get propagated through the modal popup window or something
|
// don't pass this in via the environment b/c it crashes on macOS (at least, in Designed for iPad mode) since the environment doesn't get propagated through the modal popup window or something
|
||||||
|
@ -49,8 +64,10 @@ struct DraftsView: View {
|
||||||
.map { visibleDrafts[$0] }
|
.map { visibleDrafts[$0] }
|
||||||
.forEach { draftsManager.remove($0) }
|
.forEach { draftsManager.remove($0) }
|
||||||
}
|
}
|
||||||
|
.appGroupedListRowBackground()
|
||||||
}
|
}
|
||||||
.listStyle(.plain)
|
.listStyle(.plain)
|
||||||
|
.appGroupedListBackground(container: DraftsRepresentable.UIViewControllerType.self)
|
||||||
.navigationTitle(Text("Drafts"))
|
.navigationTitle(Text("Drafts"))
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.toolbar {
|
.toolbar {
|
||||||
|
|
|
@ -38,10 +38,11 @@ struct MainComposeTextView: View {
|
||||||
@Binding var becomeFirstResponder: Bool
|
@Binding var becomeFirstResponder: Bool
|
||||||
@State private var hasFirstAppeared = false
|
@State private var hasFirstAppeared = false
|
||||||
@ScaledMetric private var fontSize = 20
|
@ScaledMetric private var fontSize = 20
|
||||||
|
@Environment(\.colorScheme) private var colorScheme
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack(alignment: .topLeading) {
|
ZStack(alignment: .topLeading) {
|
||||||
Color(UIColor.secondarySystemBackground)
|
colorScheme == .dark ? Color.appFill : Color(uiColor: .secondarySystemBackground)
|
||||||
|
|
||||||
if draft.text.isEmpty {
|
if draft.text.isEmpty {
|
||||||
placeholder
|
placeholder
|
||||||
|
|
|
@ -37,7 +37,7 @@ class ConversationCollectionViewController: UIViewController, CollectionViewCont
|
||||||
|
|
||||||
override func loadView() {
|
override func loadView() {
|
||||||
var config = UICollectionLayoutListConfiguration(appearance: .plain)
|
var config = UICollectionLayoutListConfiguration(appearance: .plain)
|
||||||
config.backgroundColor = .secondarySystemBackground
|
config.backgroundColor = .appSecondaryBackground
|
||||||
config.leadingSwipeActionsConfigurationProvider = { [unowned self] in
|
config.leadingSwipeActionsConfigurationProvider = { [unowned self] in
|
||||||
(collectionView.cellForItem(at: $0) as? TimelineStatusCollectionViewCell)?.leadingSwipeActions()
|
(collectionView.cellForItem(at: $0) as? TimelineStatusCollectionViewCell)?.leadingSwipeActions()
|
||||||
}
|
}
|
||||||
|
@ -59,7 +59,7 @@ class ConversationCollectionViewController: UIViewController, CollectionViewCont
|
||||||
}
|
}
|
||||||
// we're not using contenetInsetsReference = .readableContent here because it always insets the cells even if
|
// we're not using contenetInsetsReference = .readableContent here because it always insets the cells even if
|
||||||
// the collection view's actual width is narrow enough to fit in the readable width, resulting in a bit of the
|
// the collection view's actual width is narrow enough to fit in the readable width, resulting in a bit of the
|
||||||
// background color always peaking through the edges
|
// background color always peeking through the edges
|
||||||
let layout = UICollectionViewCompositionalLayout.list(using: config)
|
let layout = UICollectionViewCompositionalLayout.list(using: config)
|
||||||
view = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
view = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
||||||
// something about the autoresizing mask breaks resizing the vc
|
// something about the autoresizing mask breaks resizing the vc
|
||||||
|
|
|
@ -94,7 +94,7 @@ class ConversationViewController: UIViewController {
|
||||||
|
|
||||||
title = NSLocalizedString("Conversation", comment: "conversation screen title")
|
title = NSLocalizedString("Conversation", comment: "conversation screen title")
|
||||||
|
|
||||||
view.backgroundColor = .secondarySystemBackground
|
view.backgroundColor = .appSecondaryBackground
|
||||||
|
|
||||||
collapseBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "eye.fill")!, style: .plain, target: self, action: #selector(toggleCollapseButtonPressed))
|
collapseBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "eye.fill")!, style: .plain, target: self, action: #selector(toggleCollapseButtonPressed))
|
||||||
updateVisibilityBarButtonItem()
|
updateVisibilityBarButtonItem()
|
||||||
|
|
|
@ -141,18 +141,13 @@ class ExpandThreadCollectionViewCell: UICollectionViewListCell {
|
||||||
}
|
}
|
||||||
|
|
||||||
override func updateConfiguration(using state: UICellConfigurationState) {
|
override func updateConfiguration(using state: UICellConfigurationState) {
|
||||||
var config = UIBackgroundConfiguration.listPlainCell()
|
var config = UIBackgroundConfiguration.listPlainCell().updated(for: state)
|
||||||
if state.isSelected || state.isHighlighted {
|
if state.isSelected || state.isHighlighted {
|
||||||
var hue: CGFloat = 0
|
config.backgroundColor = .appSelectedCellBackground
|
||||||
var saturation: CGFloat = 0
|
|
||||||
var brightness: CGFloat = 0
|
|
||||||
UIColor.secondarySystemBackground.getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: nil)
|
|
||||||
let sign: CGFloat = traitCollection.userInterfaceStyle == .dark ? 1 : -1
|
|
||||||
config.backgroundColor = UIColor(hue: hue, saturation: saturation, brightness: max(0, brightness + sign * 0.1), alpha: 1)
|
|
||||||
} else {
|
} else {
|
||||||
config.backgroundColor = .secondarySystemBackground
|
config.backgroundColor = .appSecondaryBackground
|
||||||
}
|
}
|
||||||
backgroundConfiguration = config.updated(for: state)
|
backgroundConfiguration = config
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,20 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
|
|
||||||
|
@available(iOS, obsoleted: 16.0)
|
||||||
|
struct AddHashtagPinnedTimelineRepresentable: UIViewControllerRepresentable {
|
||||||
|
typealias UIViewControllerType = UIHostingController<AddHashtagPinnedTimelineView>
|
||||||
|
|
||||||
|
@Binding var pinnedTimelines: [PinnedTimeline]
|
||||||
|
|
||||||
|
func makeUIViewController(context: Context) -> UIHostingController<AddHashtagPinnedTimelineView> {
|
||||||
|
return UIHostingController(rootView: AddHashtagPinnedTimelineView(pinnedTimelines: $pinnedTimelines))
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateUIViewController(_ uiViewController: UIHostingController<AddHashtagPinnedTimelineView>, context: Context) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct AddHashtagPinnedTimelineView: View {
|
struct AddHashtagPinnedTimelineView: View {
|
||||||
@EnvironmentObject private var mastodonController: MastodonController
|
@EnvironmentObject private var mastodonController: MastodonController
|
||||||
@Environment(\.dismiss) private var dismiss
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
@ -34,6 +48,8 @@ struct AddHashtagPinnedTimelineView: View {
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationView {
|
NavigationView {
|
||||||
list
|
list
|
||||||
|
.listStyle(.grouped)
|
||||||
|
.appGroupedListBackground(container: AddHashtagPinnedTimelineRepresentable.UIViewControllerType.self)
|
||||||
.navigationTitle("Add Hashtag")
|
.navigationTitle("Add Hashtag")
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.searchable(text: $viewModel.searchQuery, placement: .navigationBarDrawer(displayMode: .always), prompt: Text("Search for hashtags"))
|
.searchable(text: $viewModel.searchQuery, placement: .navigationBarDrawer(displayMode: .always), prompt: Text("Search for hashtags"))
|
||||||
|
@ -57,8 +73,9 @@ struct AddHashtagPinnedTimelineView: View {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
private var list: some View {
|
private var list: some View {
|
||||||
List {
|
let list = List {
|
||||||
Section {
|
Section {
|
||||||
if viewModel.searchQuery.isEmpty {
|
if viewModel.searchQuery.isEmpty {
|
||||||
forEachTag(savedAndFollowedHashtags)
|
forEachTag(savedAndFollowedHashtags)
|
||||||
|
@ -73,8 +90,17 @@ struct AddHashtagPinnedTimelineView: View {
|
||||||
.listRowBackground(EmptyView())
|
.listRowBackground(EmptyView())
|
||||||
.listRowSeparator(.hidden)
|
.listRowSeparator(.hidden)
|
||||||
}
|
}
|
||||||
|
.appGroupedListRowBackground()
|
||||||
}
|
}
|
||||||
.listStyle(.grouped)
|
.listStyle(.grouped)
|
||||||
|
|
||||||
|
if #available(iOS 16.0, *) {
|
||||||
|
list
|
||||||
|
.scrollContentBackground(.hidden)
|
||||||
|
.background(Color.appGroupedBackground)
|
||||||
|
} else {
|
||||||
|
list
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func forEachTag(_ tags: [String]) -> some View {
|
private func forEachTag(_ tags: [String]) -> some View {
|
||||||
|
|
|
@ -51,6 +51,7 @@ struct CustomizeTimelinesList: View {
|
||||||
private var navigationBody: some View {
|
private var navigationBody: some View {
|
||||||
List {
|
List {
|
||||||
PinnedTimelinesView(accountPreferences: mastodonController.accountPreferences)
|
PinnedTimelinesView(accountPreferences: mastodonController.accountPreferences)
|
||||||
|
.appGroupedListRowBackground()
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
Toggle(isOn: $preferences.hideReblogsInTimelines) {
|
Toggle(isOn: $preferences.hideReblogsInTimelines) {
|
||||||
|
@ -62,6 +63,7 @@ struct CustomizeTimelinesList: View {
|
||||||
} header: {
|
} header: {
|
||||||
Text("Home Timeline")
|
Text("Home Timeline")
|
||||||
}
|
}
|
||||||
|
.appGroupedListRowBackground()
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
filtersForEach(unexpiredFilters)
|
filtersForEach(unexpiredFilters)
|
||||||
|
@ -75,6 +77,7 @@ struct CustomizeTimelinesList: View {
|
||||||
} header: {
|
} header: {
|
||||||
Text("Active Filters")
|
Text("Active Filters")
|
||||||
}
|
}
|
||||||
|
.appGroupedListRowBackground()
|
||||||
|
|
||||||
if !expiredFilters.isEmpty {
|
if !expiredFilters.isEmpty {
|
||||||
Section {
|
Section {
|
||||||
|
@ -82,8 +85,11 @@ struct CustomizeTimelinesList: View {
|
||||||
} header: {
|
} header: {
|
||||||
Text("Expired Filters")
|
Text("Expired Filters")
|
||||||
}
|
}
|
||||||
|
.appGroupedListRowBackground()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.listStyle(.insetGrouped)
|
||||||
|
.appGroupedListBackground(container: UIHostingController<CustomizeTimelinesList>.self)
|
||||||
.navigationTitle(Text("Customize Timelines"))
|
.navigationTitle(Text("Customize Timelines"))
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.toolbar {
|
.toolbar {
|
||||||
|
|
|
@ -72,6 +72,7 @@ struct EditFilterView: View {
|
||||||
filter.title = newValue
|
filter.title = newValue
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
.appGroupedListRowBackground()
|
||||||
}
|
}
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
|
@ -96,6 +97,7 @@ struct EditFilterView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.appGroupedListRowBackground()
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
if mastodonController.instanceFeatures.filtersV2 {
|
if mastodonController.instanceFeatures.filtersV2 {
|
||||||
|
@ -120,6 +122,7 @@ struct EditFilterView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.appGroupedListRowBackground()
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
ForEach(FilterV1.Context.allCases, id: \.rawValue) { context in
|
ForEach(FilterV1.Context.allCases, id: \.rawValue) { context in
|
||||||
|
@ -141,7 +144,10 @@ struct EditFilterView: View {
|
||||||
} header: {
|
} header: {
|
||||||
Text("Contexts")
|
Text("Contexts")
|
||||||
}
|
}
|
||||||
|
.appGroupedListRowBackground()
|
||||||
}
|
}
|
||||||
|
.appGroupedListBackground(container: UIHostingController<CustomizeTimelinesList>.self)
|
||||||
|
.scrollDismissesKeyboardInteractivelyIfAvailable()
|
||||||
.navigationTitle(create ? "Add Filter" : "Edit Filter")
|
.navigationTitle(create ? "Add Filter" : "Edit Filter")
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.toolbar {
|
.toolbar {
|
||||||
|
|
|
@ -111,7 +111,13 @@ struct PinnedTimelinesView: View {
|
||||||
Text("Pinned Timelines")
|
Text("Pinned Timelines")
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $isShowingAddHashtagSheet, content: {
|
.sheet(isPresented: $isShowingAddHashtagSheet, content: {
|
||||||
|
if #available(iOS 16.0, *) {
|
||||||
AddHashtagPinnedTimelineView(pinnedTimelines: $pinnedTimelines)
|
AddHashtagPinnedTimelineView(pinnedTimelines: $pinnedTimelines)
|
||||||
|
.edgesIgnoringSafeArea(.bottom)
|
||||||
|
} else {
|
||||||
|
AddHashtagPinnedTimelineRepresentable(pinnedTimelines: $pinnedTimelines)
|
||||||
|
.edgesIgnoringSafeArea(.bottom)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.sheet(isPresented: $isShowingAddInstanceSheet, content: {
|
.sheet(isPresented: $isShowingAddInstanceSheet, content: {
|
||||||
AddInstancePinnedTimelineView(pinnedTimelines: $pinnedTimelines)
|
AddInstancePinnedTimelineView(pinnedTimelines: $pinnedTimelines)
|
||||||
|
|
|
@ -34,15 +34,15 @@ class AddSavedHashtagViewController: UIViewController {
|
||||||
|
|
||||||
title = NSLocalizedString("Search", comment: "search screen title")
|
title = NSLocalizedString("Search", comment: "search screen title")
|
||||||
|
|
||||||
view.backgroundColor = .systemGroupedBackground
|
|
||||||
|
|
||||||
var config = UICollectionLayoutListConfiguration(appearance: .grouped)
|
var config = UICollectionLayoutListConfiguration(appearance: .grouped)
|
||||||
|
config.backgroundColor = .appGroupedBackground
|
||||||
config.headerMode = .supplementary
|
config.headerMode = .supplementary
|
||||||
let layout = UICollectionViewCompositionalLayout.list(using: config)
|
let layout = UICollectionViewCompositionalLayout.list(using: config)
|
||||||
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
|
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
|
||||||
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||||
collectionView.delegate = self
|
collectionView.delegate = self
|
||||||
collectionView.allowsFocus = true
|
collectionView.allowsFocus = true
|
||||||
|
collectionView.backgroundColor = .appGroupedBackground
|
||||||
view.addSubview(collectionView)
|
view.addSubview(collectionView)
|
||||||
|
|
||||||
let sectionHeaderCell = UICollectionView.SupplementaryRegistration<UICollectionViewListCell>(elementKind: UICollectionView.elementKindSectionHeader) { (headerView, collectionView, indexPath) in
|
let sectionHeaderCell = UICollectionView.SupplementaryRegistration<UICollectionViewListCell>(elementKind: UICollectionView.elementKindSectionHeader) { (headerView, collectionView, indexPath) in
|
||||||
|
|
|
@ -43,7 +43,8 @@ class ExploreViewController: UIViewController, UICollectionViewDelegate, Collect
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
var configuration = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
|
var configuration = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
|
||||||
configuration.trailingSwipeActionsConfigurationProvider = self.trailingSwipeActionsForCell(at:)
|
configuration.backgroundColor = .appGroupedBackground
|
||||||
|
configuration.trailingSwipeActionsConfigurationProvider = { [unowned self] in self.trailingSwipeActionsForCell(at: $0) }
|
||||||
configuration.headerMode = .supplementary
|
configuration.headerMode = .supplementary
|
||||||
let layout = UICollectionViewCompositionalLayout.list(using: configuration)
|
let layout = UICollectionViewCompositionalLayout.list(using: configuration)
|
||||||
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
|
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
|
||||||
|
@ -100,12 +101,23 @@ class ExploreViewController: UIViewController, UICollectionViewDelegate, Collect
|
||||||
override func viewWillAppear(_ animated: Bool) {
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
super.viewWillAppear(animated)
|
super.viewWillAppear(animated)
|
||||||
|
|
||||||
|
// UISearchController exists outside of the normal VC hierarchy,
|
||||||
|
// so we manually propagate this down to the results controller
|
||||||
|
// so that it can deselect on appear
|
||||||
|
if searchController.isActive {
|
||||||
|
resultsController.viewWillAppear(animated)
|
||||||
|
}
|
||||||
|
|
||||||
clearSelectionOnAppear(animated: animated)
|
clearSelectionOnAppear(animated: animated)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewDidAppear(_ animated: Bool) {
|
override func viewDidAppear(_ animated: Bool) {
|
||||||
super.viewDidAppear(animated)
|
super.viewDidAppear(animated)
|
||||||
|
|
||||||
|
if searchController.isActive {
|
||||||
|
resultsController.viewDidAppear(animated)
|
||||||
|
}
|
||||||
|
|
||||||
// this is a workaround for the issue that setting isActive on a search controller that is not visible
|
// this is a workaround for the issue that setting isActive on a search controller that is not visible
|
||||||
// does not cause it to automatically become active once it becomes visible
|
// does not cause it to automatically become active once it becomes visible
|
||||||
// see FB7814561
|
// see FB7814561
|
||||||
|
@ -130,6 +142,16 @@ class ExploreViewController: UIViewController, UICollectionViewDelegate, Collect
|
||||||
config.image = item.image
|
config.image = item.image
|
||||||
cell.contentConfiguration = config
|
cell.contentConfiguration = config
|
||||||
|
|
||||||
|
cell.configurationUpdateHandler = { cell, state in
|
||||||
|
var config = UIBackgroundConfiguration.listGroupedCell()
|
||||||
|
if state.isHighlighted || state.isSelected {
|
||||||
|
config.backgroundColor = .appSelectedCellBackground
|
||||||
|
} else {
|
||||||
|
config.backgroundColor = .appGroupedCellBackground
|
||||||
|
}
|
||||||
|
cell.backgroundConfiguration = config
|
||||||
|
}
|
||||||
|
|
||||||
switch item {
|
switch item {
|
||||||
case .addList, .addSavedHashtag, .findInstance:
|
case .addList, .addSavedHashtag, .findInstance:
|
||||||
cell.accessories = []
|
cell.accessories = []
|
||||||
|
|
|
@ -39,6 +39,7 @@ class FeaturedProfileCollectionViewCell: UICollectionViewCell {
|
||||||
noteTextView.textContainerInset = UIEdgeInsets(top: 16, left: 4, bottom: 16, right: 4)
|
noteTextView.textContainerInset = UIEdgeInsets(top: 16, left: 4, bottom: 16, right: 4)
|
||||||
|
|
||||||
backgroundColor = .clear
|
backgroundColor = .clear
|
||||||
|
clippingView.backgroundColor = .appBackground
|
||||||
clippingView.layer.cornerRadius = 5
|
clippingView.layer.cornerRadius = 5
|
||||||
clippingView.layer.borderWidth = 1
|
clippingView.layer.borderWidth = 1
|
||||||
clippingView.layer.masksToBounds = true
|
clippingView.layer.masksToBounds = true
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="19529" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="21507" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<deployment identifier="iOS"/>
|
<deployment identifier="iOS"/>
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="19519"/>
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21505"/>
|
||||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
|
@ -55,7 +55,6 @@
|
||||||
</label>
|
</label>
|
||||||
<textView clipsSubviews="YES" multipleTouchEnabled="YES" userInteractionEnabled="NO" contentMode="scaleToFill" verticalCompressionResistancePriority="749" scrollEnabled="NO" editable="NO" textAlignment="natural" selectable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="bvj-F0-ggC" customClass="StatusContentTextView" customModule="Tusker" customModuleProvider="target">
|
<textView clipsSubviews="YES" multipleTouchEnabled="YES" userInteractionEnabled="NO" contentMode="scaleToFill" verticalCompressionResistancePriority="749" scrollEnabled="NO" editable="NO" textAlignment="natural" selectable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="bvj-F0-ggC" customClass="StatusContentTextView" customModule="Tusker" customModuleProvider="target">
|
||||||
<rect key="frame" x="8" y="102" width="384" height="98"/>
|
<rect key="frame" x="8" y="102" width="384" height="98"/>
|
||||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
|
||||||
<string key="text">Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda.</string>
|
<string key="text">Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda.</string>
|
||||||
<color key="textColor" systemColor="labelColor"/>
|
<color key="textColor" systemColor="labelColor"/>
|
||||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||||
|
@ -102,7 +101,7 @@
|
||||||
</objects>
|
</objects>
|
||||||
<resources>
|
<resources>
|
||||||
<systemColor name="labelColor">
|
<systemColor name="labelColor">
|
||||||
<color white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
</systemColor>
|
</systemColor>
|
||||||
<systemColor name="systemBackgroundColor">
|
<systemColor name="systemBackgroundColor">
|
||||||
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -64,7 +64,7 @@ class ProfileDirectoryViewController: UIViewController {
|
||||||
|
|
||||||
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
|
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
|
||||||
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||||
collectionView.backgroundColor = .secondarySystemBackground
|
collectionView.backgroundColor = .appSecondaryBackground
|
||||||
collectionView.register(UINib(nibName: "FeaturedProfileCollectionViewCell", bundle: .main), forCellWithReuseIdentifier: "featuredProfileCell")
|
collectionView.register(UINib(nibName: "FeaturedProfileCollectionViewCell", bundle: .main), forCellWithReuseIdentifier: "featuredProfileCell")
|
||||||
collectionView.delegate = self
|
collectionView.delegate = self
|
||||||
collectionView.dragDelegate = self
|
collectionView.dragDelegate = self
|
||||||
|
|
|
@ -36,6 +36,7 @@ class SuggestedProfileCardCollectionViewCell: UICollectionViewCell {
|
||||||
layer.shadowOffset = .zero
|
layer.shadowOffset = .zero
|
||||||
layer.masksToBounds = false
|
layer.masksToBounds = false
|
||||||
contentView.layer.cornerRadius = 12.5
|
contentView.layer.cornerRadius = 12.5
|
||||||
|
contentView.backgroundColor = .appGroupedCellBackground
|
||||||
updateLayerColors()
|
updateLayerColors()
|
||||||
|
|
||||||
headerImageView.cache = .headers
|
headerImageView.cache = .headers
|
||||||
|
@ -189,7 +190,7 @@ private struct SuggestionSourceView: View {
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
HStack {
|
HStack {
|
||||||
Image(uiImage: source.image)
|
Image(uiImage: source.image.withRenderingMode(.alwaysTemplate))
|
||||||
Text(source.title)
|
Text(source.title)
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
}
|
|
@ -9,6 +9,7 @@
|
||||||
import UIKit
|
import UIKit
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
import WebURLFoundationExtras
|
import WebURLFoundationExtras
|
||||||
|
import Combine
|
||||||
|
|
||||||
class TrendingHashtagsViewController: UIViewController {
|
class TrendingHashtagsViewController: UIViewController {
|
||||||
|
|
||||||
|
@ -17,6 +18,9 @@ class TrendingHashtagsViewController: UIViewController {
|
||||||
private var collectionView: UICollectionView!
|
private var collectionView: UICollectionView!
|
||||||
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
||||||
|
|
||||||
|
private var state = State.unloaded
|
||||||
|
private var confirmLoadMore = PassthroughSubject<Void, Never>()
|
||||||
|
|
||||||
init(mastodonController: MastodonController) {
|
init(mastodonController: MastodonController) {
|
||||||
self.mastodonController = mastodonController
|
self.mastodonController = mastodonController
|
||||||
|
|
||||||
|
@ -32,9 +36,21 @@ class TrendingHashtagsViewController: UIViewController {
|
||||||
|
|
||||||
title = NSLocalizedString("Trending Hashtags", comment: "trending hashtags screen title")
|
title = NSLocalizedString("Trending Hashtags", comment: "trending hashtags screen title")
|
||||||
|
|
||||||
view.backgroundColor = .systemGroupedBackground
|
view.backgroundColor = .appGroupedBackground
|
||||||
|
|
||||||
let config = UICollectionLayoutListConfiguration(appearance: .grouped)
|
var config = UICollectionLayoutListConfiguration(appearance: .grouped)
|
||||||
|
config.backgroundColor = .appGroupedBackground
|
||||||
|
config.itemSeparatorHandler = { [unowned self] indexPath, sectionConfig in
|
||||||
|
guard let item = dataSource.itemIdentifier(for: indexPath) else {
|
||||||
|
return sectionConfig
|
||||||
|
}
|
||||||
|
var config = sectionConfig
|
||||||
|
if item.hideSeparators {
|
||||||
|
config.topSeparatorVisibility = .hidden
|
||||||
|
config.bottomSeparatorVisibility = .hidden
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
}
|
||||||
let layout = UICollectionViewCompositionalLayout.list(using: config)
|
let layout = UICollectionViewCompositionalLayout.list(using: config)
|
||||||
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
|
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
|
||||||
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||||
|
@ -43,14 +59,24 @@ class TrendingHashtagsViewController: UIViewController {
|
||||||
collectionView.allowsFocus = true
|
collectionView.allowsFocus = true
|
||||||
view.addSubview(collectionView)
|
view.addSubview(collectionView)
|
||||||
|
|
||||||
let registration = UICollectionView.CellRegistration<TrendingHashtagCollectionViewCell, Hashtag> { cell, indexPath, hashtag in
|
let loadingCell = UICollectionView.CellRegistration<LoadingCollectionViewCell, Void> { cell, indexPath, itemIdentifier in
|
||||||
|
cell.indicator.startAnimating()
|
||||||
|
}
|
||||||
|
let hashtagCell = UICollectionView.CellRegistration<TrendingHashtagCollectionViewCell, Hashtag> { cell, indexPath, hashtag in
|
||||||
cell.updateUI(hashtag: hashtag)
|
cell.updateUI(hashtag: hashtag)
|
||||||
}
|
}
|
||||||
|
let confirmLoadMoreCell = UICollectionView.CellRegistration<ConfirmLoadMoreCollectionViewCell, Bool> { [unowned self] cell, indexPath, isLoading in
|
||||||
|
cell.confirmLoadMore = self.confirmLoadMore
|
||||||
|
cell.isLoading = isLoading
|
||||||
|
}
|
||||||
dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) { (collectionView, indexPath, item) in
|
dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) { (collectionView, indexPath, item) in
|
||||||
switch item {
|
switch item {
|
||||||
case let .tag(hashtag):
|
case .loadingIndicator:
|
||||||
return collectionView.dequeueConfiguredReusableCell(using: registration, for: indexPath, item: hashtag)
|
return collectionView.dequeueConfiguredReusableCell(using: loadingCell, for: indexPath, item: ())
|
||||||
|
case .tag(let hashtag):
|
||||||
|
return collectionView.dequeueConfiguredReusableCell(using: hashtagCell, for: indexPath, item: hashtag)
|
||||||
|
case .confirmLoadMore(let loading):
|
||||||
|
return collectionView.dequeueConfiguredReusableCell(using: confirmLoadMoreCell, for: indexPath, item: loading)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -58,18 +84,104 @@ class TrendingHashtagsViewController: UIViewController {
|
||||||
override func viewWillAppear(_ animated: Bool) {
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
super.viewWillAppear(animated)
|
super.viewWillAppear(animated)
|
||||||
|
|
||||||
let request = Client.getTrendingHashtags(limit: 10)
|
|
||||||
Task {
|
Task {
|
||||||
guard let (hashtags, _) = try? await mastodonController.run(request) else {
|
await loadInitial()
|
||||||
return
|
|
||||||
}
|
|
||||||
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
|
||||||
snapshot.appendSections([.trendingTags])
|
|
||||||
snapshot.appendItems(hashtags.map { .tag($0) })
|
|
||||||
await dataSource.apply(snapshot)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func request(offset: Int?) -> Request<[Hashtag]> {
|
||||||
|
if mastodonController.instanceFeatures.hasMastodonVersion(3, 5, 0) {
|
||||||
|
return Client.getTrendingHashtags(offset: offset)
|
||||||
|
} else {
|
||||||
|
return Client.getTrendingHashtagsDeprecated(offset: offset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
private func loadInitial() async {
|
||||||
|
guard case .unloaded = state else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
state = .loadingInitial
|
||||||
|
|
||||||
|
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||||
|
snapshot.appendSections([.trendingTags])
|
||||||
|
snapshot.appendItems([.loadingIndicator])
|
||||||
|
await dataSource.apply(snapshot)
|
||||||
|
|
||||||
|
do {
|
||||||
|
let request = self.request(offset: nil)
|
||||||
|
let (hashtags, _) = try await mastodonController.run(request)
|
||||||
|
snapshot.deleteItems([.loadingIndicator])
|
||||||
|
snapshot.appendItems(hashtags.map { .tag($0) })
|
||||||
|
state = .loaded
|
||||||
|
await dataSource.apply(snapshot)
|
||||||
|
} catch {
|
||||||
|
snapshot.deleteItems([.loadingIndicator])
|
||||||
|
await dataSource.apply(snapshot)
|
||||||
|
state = .unloaded
|
||||||
|
|
||||||
|
let config = ToastConfiguration(from: error, with: "Error Loading Trending Tags", in: self) { [weak self] toast in
|
||||||
|
toast.dismissToast(animated: true)
|
||||||
|
await self?.loadInitial()
|
||||||
|
}
|
||||||
|
self.showToast(configuration: config, animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
private func loadOlder() async {
|
||||||
|
guard case .loaded = state else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
state = .loadingOlder
|
||||||
|
|
||||||
|
let origSnapshot = dataSource.snapshot()
|
||||||
|
var snapshot = origSnapshot
|
||||||
|
if Preferences.shared.disableInfiniteScrolling {
|
||||||
|
snapshot.appendItems([.confirmLoadMore(false)])
|
||||||
|
await dataSource.apply(snapshot)
|
||||||
|
|
||||||
|
for await _ in confirmLoadMore.values {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
snapshot.deleteItems([.confirmLoadMore(false)])
|
||||||
|
snapshot.appendItems([.confirmLoadMore(true)])
|
||||||
|
await dataSource.apply(snapshot, animatingDifferences: false)
|
||||||
|
} else {
|
||||||
|
snapshot.appendItems([.loadingIndicator])
|
||||||
|
await dataSource.apply(snapshot)
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
let request = self.request(offset: snapshot.itemIdentifiers.count - 1)
|
||||||
|
let (hashtags, _) = try await mastodonController.run(request)
|
||||||
|
var snapshot = origSnapshot
|
||||||
|
snapshot.appendItems(hashtags.map { .tag($0) })
|
||||||
|
await dataSource.apply(snapshot)
|
||||||
|
} catch {
|
||||||
|
await dataSource.apply(origSnapshot)
|
||||||
|
|
||||||
|
let config = ToastConfiguration(from: error, with: "Error Loading More Tags", in: self) { [weak self] toast in
|
||||||
|
toast.dismissToast(animated: true)
|
||||||
|
await self?.loadOlder()
|
||||||
|
}
|
||||||
|
self.showToast(configuration: config, animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
state = .loaded
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension TrendingHashtagsViewController {
|
||||||
|
enum State {
|
||||||
|
case unloaded
|
||||||
|
case loadingInitial
|
||||||
|
case loaded
|
||||||
|
case loadingOlder
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension TrendingHashtagsViewController {
|
extension TrendingHashtagsViewController {
|
||||||
|
@ -77,11 +189,45 @@ extension TrendingHashtagsViewController {
|
||||||
case trendingTags
|
case trendingTags
|
||||||
}
|
}
|
||||||
enum Item: Hashable {
|
enum Item: Hashable {
|
||||||
|
case loadingIndicator
|
||||||
case tag(Hashtag)
|
case tag(Hashtag)
|
||||||
|
case confirmLoadMore(Bool)
|
||||||
|
|
||||||
|
var hideSeparators: Bool {
|
||||||
|
switch self {
|
||||||
|
case .loadingIndicator:
|
||||||
|
return true
|
||||||
|
case .tag(_):
|
||||||
|
return false
|
||||||
|
case .confirmLoadMore(_):
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var shouldSelect: Bool {
|
||||||
|
if case .tag(_) = self {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension TrendingHashtagsViewController: UICollectionViewDelegate {
|
extension TrendingHashtagsViewController: UICollectionViewDelegate {
|
||||||
|
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
|
||||||
|
if indexPath.section == collectionView.numberOfSections - 1,
|
||||||
|
indexPath.row == collectionView.numberOfItems(inSection: indexPath.section) - 1 {
|
||||||
|
Task {
|
||||||
|
await self.loadOlder()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool {
|
||||||
|
return dataSource.itemIdentifier(for: indexPath)?.shouldSelect ?? false
|
||||||
|
}
|
||||||
|
|
||||||
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||||
guard let item = dataSource.itemIdentifier(for: indexPath),
|
guard let item = dataSource.itemIdentifier(for: indexPath),
|
||||||
case let .tag(hashtag) = item else {
|
case let .tag(hashtag) = item else {
|
||||||
|
|
|
@ -21,6 +21,29 @@ class TrendingLinkCardCollectionViewCell: UICollectionViewCell {
|
||||||
@IBOutlet weak var providerLabel: UILabel!
|
@IBOutlet weak var providerLabel: UILabel!
|
||||||
@IBOutlet weak var activityLabel: UILabel!
|
@IBOutlet weak var activityLabel: UILabel!
|
||||||
@IBOutlet weak var historyView: TrendHistoryView!
|
@IBOutlet weak var historyView: TrendHistoryView!
|
||||||
|
private var thumbnailAspectRatioConstraint: NSLayoutConstraint?
|
||||||
|
|
||||||
|
var verticalSize: VerticalSize! {
|
||||||
|
didSet {
|
||||||
|
guard oldValue != verticalSize else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch verticalSize {
|
||||||
|
case nil:
|
||||||
|
fatalError()
|
||||||
|
case .regular:
|
||||||
|
thumbnailAspectRatioConstraint?.isActive = false
|
||||||
|
thumbnailAspectRatioConstraint = thumbnailView.widthAnchor.constraint(equalTo: thumbnailView.heightAnchor, multiplier: 4/3)
|
||||||
|
thumbnailAspectRatioConstraint!.isActive = true
|
||||||
|
descriptionLabel.numberOfLines = 3
|
||||||
|
case .compact:
|
||||||
|
thumbnailAspectRatioConstraint?.isActive = false
|
||||||
|
thumbnailAspectRatioConstraint = thumbnailView.widthAnchor.constraint(equalTo: thumbnailView.heightAnchor, multiplier: 2/1)
|
||||||
|
thumbnailAspectRatioConstraint!.isActive = true
|
||||||
|
descriptionLabel.numberOfLines = 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private var hoverGestureAnimator: UIViewPropertyAnimator?
|
private var hoverGestureAnimator: UIViewPropertyAnimator?
|
||||||
|
|
||||||
|
@ -34,8 +57,11 @@ class TrendingLinkCardCollectionViewCell: UICollectionViewCell {
|
||||||
layer.shadowOffset = .zero
|
layer.shadowOffset = .zero
|
||||||
layer.masksToBounds = false
|
layer.masksToBounds = false
|
||||||
contentView.layer.cornerRadius = 12.5
|
contentView.layer.cornerRadius = 12.5
|
||||||
|
contentView.backgroundColor = .appGroupedCellBackground
|
||||||
updateLayerColors()
|
updateLayerColors()
|
||||||
|
|
||||||
|
verticalSize = .regular
|
||||||
|
|
||||||
addGestureRecognizer(UIHoverGestureRecognizer(target: self, action: #selector(hoverRecognized)))
|
addGestureRecognizer(UIHoverGestureRecognizer(target: self, action: #selector(hoverRecognized)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -136,3 +162,9 @@ class TrendingLinkCardCollectionViewCell: UICollectionViewCell {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension TrendingLinkCardCollectionViewCell {
|
||||||
|
enum VerticalSize {
|
||||||
|
case regular, compact
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -21,7 +21,7 @@
|
||||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="h3b-Mf-lD6" customClass="CachedImageView" customModule="Tusker" customModuleProvider="target">
|
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="h3b-Mf-lD6" customClass="CachedImageView" customModule="Tusker" customModuleProvider="target">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="300" height="225"/>
|
<rect key="frame" x="0.0" y="0.0" width="300" height="225"/>
|
||||||
<constraints>
|
<constraints>
|
||||||
<constraint firstAttribute="width" secondItem="h3b-Mf-lD6" secondAttribute="height" multiplier="4:3" id="QDY-8a-LYC"/>
|
<constraint firstAttribute="width" secondItem="h3b-Mf-lD6" secondAttribute="height" multiplier="4:3" placeholder="YES" id="QDY-8a-LYC"/>
|
||||||
</constraints>
|
</constraints>
|
||||||
</imageView>
|
</imageView>
|
||||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="top" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="LpU-m4-guC">
|
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="top" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="LpU-m4-guC">
|
||||||
|
@ -56,9 +56,9 @@
|
||||||
</constraints>
|
</constraints>
|
||||||
</view>
|
</view>
|
||||||
<visualEffectView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="cWo-9n-z42">
|
<visualEffectView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="cWo-9n-z42">
|
||||||
<rect key="frame" x="0.0" y="196.33333333333334" width="300" height="28.666666666666657"/>
|
<rect key="frame" x="0.0" y="196.66666666666666" width="300" height="28.333333333333343"/>
|
||||||
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="ktv-3s-cp9">
|
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="ktv-3s-cp9">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="300" height="28.666666666666657"/>
|
<rect key="frame" x="0.0" y="0.0" width="300" height="28.333333333333343"/>
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsLetterSpacingToFitWidth="YES" showsExpansionTextWhenTruncated="YES" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Ho3-cU-IGi">
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsLetterSpacingToFitWidth="YES" showsExpansionTextWhenTruncated="YES" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Ho3-cU-IGi">
|
||||||
|
|
|
@ -80,6 +80,16 @@ class TrendingLinkTableViewCell: UITableViewCell {
|
||||||
thumbnailView.layer.cornerRadius = 0.05 * thumbnailView.bounds.width
|
thumbnailView.layer.cornerRadius = 0.05 * thumbnailView.bounds.width
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func updateConfiguration(using state: UICellConfigurationState) {
|
||||||
|
var config = UIBackgroundConfiguration.listGroupedCell().updated(for: state)
|
||||||
|
if state.isHighlighted || state.isSelected {
|
||||||
|
config.backgroundColor = .appSelectedCellBackground
|
||||||
|
} else {
|
||||||
|
config.backgroundColor = .appGroupedCellBackground
|
||||||
|
}
|
||||||
|
backgroundConfiguration = config
|
||||||
|
}
|
||||||
|
|
||||||
func updateUI(card: Card) {
|
func updateUI(card: Card) {
|
||||||
self.card = card
|
self.card = card
|
||||||
self.thumbnailView.image = nil
|
self.thumbnailView.image = nil
|
||||||
|
|
|
@ -10,19 +10,22 @@ import UIKit
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
import WebURLFoundationExtras
|
import WebURLFoundationExtras
|
||||||
import SafariServices
|
import SafariServices
|
||||||
|
import Combine
|
||||||
|
|
||||||
class TrendingLinksViewController: EnhancedTableViewController {
|
class TrendingLinksViewController: UIViewController, CollectionViewController {
|
||||||
|
|
||||||
weak var mastodonController: MastodonController!
|
weak var mastodonController: MastodonController!
|
||||||
|
|
||||||
private var dataSource: UITableViewDiffableDataSource<Section, Item>!
|
var collectionView: UICollectionView!
|
||||||
|
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
||||||
|
|
||||||
|
private var state = State.unloaded
|
||||||
|
private let confirmLoadMore = PassthroughSubject<Void, Never>()
|
||||||
|
|
||||||
init(mastodonController: MastodonController) {
|
init(mastodonController: MastodonController) {
|
||||||
self.mastodonController = mastodonController
|
self.mastodonController = mastodonController
|
||||||
|
|
||||||
super.init(style: .grouped)
|
super.init(nibName: nil, bundle: nil)
|
||||||
|
|
||||||
dragEnabled = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
|
@ -34,73 +37,260 @@ class TrendingLinksViewController: EnhancedTableViewController {
|
||||||
|
|
||||||
title = NSLocalizedString("Trending Links", comment: "trending links screen title")
|
title = NSLocalizedString("Trending Links", comment: "trending links screen title")
|
||||||
|
|
||||||
tableView.register(TrendingLinkTableViewCell.self, forCellReuseIdentifier: "trendingLinkCell")
|
let layout = UICollectionViewCompositionalLayout { [unowned self] sectionIndex, environment in
|
||||||
tableView.estimatedRowHeight = 100
|
switch dataSource.sectionIdentifier(for: sectionIndex) {
|
||||||
tableView.allowsFocus = true
|
case nil:
|
||||||
|
fatalError()
|
||||||
|
|
||||||
dataSource = UITableViewDiffableDataSource(tableView: tableView, cellProvider: { tableView, indexPath, item in
|
case .loadingIndicator:
|
||||||
let cell = tableView.dequeueReusableCell(withIdentifier: "trendingLinkCell", for: indexPath) as! TrendingLinkTableViewCell
|
var config = UICollectionLayoutListConfiguration(appearance: .grouped)
|
||||||
cell.updateUI(card: item.card)
|
config.backgroundColor = .appGroupedBackground
|
||||||
return cell
|
config.showsSeparators = false
|
||||||
})
|
return .list(using: config, layoutEnvironment: environment)
|
||||||
|
|
||||||
|
case .links:
|
||||||
|
let size = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(280))
|
||||||
|
let item = NSCollectionLayoutItem(layoutSize: size)
|
||||||
|
let item2 = NSCollectionLayoutItem(layoutSize: size)
|
||||||
|
let group = NSCollectionLayoutGroup.vertical(layoutSize: size, subitems: [item, item2])
|
||||||
|
group.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16)
|
||||||
|
group.interItemSpacing = .fixed(16)
|
||||||
|
let section = NSCollectionLayoutSection(group: group)
|
||||||
|
section.interGroupSpacing = 16
|
||||||
|
section.contentInsets = NSDirectionalEdgeInsets(top: 16, leading: 0, bottom: 16, trailing: 0)
|
||||||
|
return section
|
||||||
|
}
|
||||||
|
}
|
||||||
|
collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
||||||
|
collectionView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
collectionView.delegate = self
|
||||||
|
collectionView.dragDelegate = self
|
||||||
|
collectionView.backgroundColor = .appGroupedBackground
|
||||||
|
collectionView.allowsFocus = true
|
||||||
|
view.addSubview(collectionView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||||
|
collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||||
|
collectionView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||||
|
collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
|
dataSource = createDataSource()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> {
|
||||||
|
let loadingCell = UICollectionView.CellRegistration<LoadingCollectionViewCell, Void> { cell, indexPath, itemIdentifier in
|
||||||
|
cell.indicator.startAnimating()
|
||||||
|
}
|
||||||
|
let linkCell = UICollectionView.CellRegistration<TrendingLinkCardCollectionViewCell, Card>(cellNib: UINib(nibName: "TrendingLinkCardCollectionViewCell", bundle: .main)) { cell, indexPath, item in
|
||||||
|
cell.verticalSize = .compact
|
||||||
|
cell.updateUI(card: item)
|
||||||
|
}
|
||||||
|
let confirmLoadMoreCell = UICollectionView.CellRegistration<ConfirmLoadMoreCollectionViewCell, Bool> { cell, indexPath, isLoading in
|
||||||
|
cell.confirmLoadMore = self.confirmLoadMore
|
||||||
|
cell.isLoading = isLoading
|
||||||
|
}
|
||||||
|
return UICollectionViewDiffableDataSource(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in
|
||||||
|
switch itemIdentifier {
|
||||||
|
case .loadingIndicator:
|
||||||
|
return collectionView.dequeueConfiguredReusableCell(using: loadingCell, for: indexPath, item: ())
|
||||||
|
case .link(let card):
|
||||||
|
return collectionView.dequeueConfiguredReusableCell(using: linkCell, for: indexPath, item: card)
|
||||||
|
case .confirmLoadMore(let loading):
|
||||||
|
return collectionView.dequeueConfiguredReusableCell(using: confirmLoadMoreCell, for: indexPath, item: loading)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewWillAppear(_ animated: Bool) {
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
super.viewWillAppear(animated)
|
super.viewWillAppear(animated)
|
||||||
|
|
||||||
let request = Client.getTrendingLinks()
|
|
||||||
Task {
|
Task {
|
||||||
guard let (links, _) = try? await mastodonController.run(request) else {
|
await loadInitial()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
private func loadInitial() async {
|
||||||
|
guard case .unloaded = state else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
state = .loading
|
||||||
|
|
||||||
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
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.appendSections([.links])
|
||||||
snapshot.appendItems(links.map(Item.init))
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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.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)
|
await dataSource.apply(snapshot)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Table View Delegate
|
do {
|
||||||
|
let request = Client.getTrendingLinks(offset: origSnapshot.itemIdentifiers.count)
|
||||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
let (links, _) = try await mastodonController.run(request)
|
||||||
guard let item = dataSource.itemIdentifier(for: indexPath),
|
var snapshot = origSnapshot
|
||||||
let url = URL(item.card.url) else {
|
snapshot.appendItems(links.map { .link($0) }, toSection: .links)
|
||||||
return
|
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()
|
||||||
}
|
}
|
||||||
selected(url: url)
|
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
|
|
||||||
}
|
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension TrendingLinksViewController {
|
||||||
|
enum State {
|
||||||
|
case unloaded
|
||||||
|
case loading
|
||||||
|
case loaded
|
||||||
|
case loadingOlder
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
extension TrendingLinksViewController {
|
extension TrendingLinksViewController {
|
||||||
enum Section {
|
enum Section {
|
||||||
|
case loadingIndicator
|
||||||
case links
|
case links
|
||||||
}
|
}
|
||||||
struct Item: Hashable {
|
enum Item: Hashable {
|
||||||
let card: Card
|
case loadingIndicator
|
||||||
|
case link(Card)
|
||||||
|
case confirmLoadMore(Bool)
|
||||||
|
|
||||||
static func ==(lhs: Item, rhs: Item) -> Bool {
|
static func ==(lhs: Item, rhs: Item) -> Bool {
|
||||||
return lhs.card.url == rhs.card.url
|
switch (lhs, rhs) {
|
||||||
|
case (.loadingIndicator, .loadingIndicator):
|
||||||
|
return true
|
||||||
|
case (.link(let a), .link(let b)):
|
||||||
|
return a.url == b.url
|
||||||
|
case (.confirmLoadMore(let a), .confirmLoadMore(let b)):
|
||||||
|
return a == b
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func hash(into hasher: inout Hasher) {
|
func hash(into hasher: inout Hasher) {
|
||||||
|
switch self {
|
||||||
|
case .loadingIndicator:
|
||||||
|
hasher.combine(0)
|
||||||
|
case .link(let card):
|
||||||
|
hasher.combine(1)
|
||||||
hasher.combine(card.url)
|
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))]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension TrendingLinksViewController: TuskerNavigationDelegate {
|
extension TrendingLinksViewController: TuskerNavigationDelegate {
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
import UIKit
|
import UIKit
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
import SafariServices
|
import SafariServices
|
||||||
|
import Combine
|
||||||
|
|
||||||
class TrendsViewController: UIViewController, CollectionViewController {
|
class TrendsViewController: UIViewController, CollectionViewController {
|
||||||
|
|
||||||
|
@ -18,6 +19,8 @@ class TrendsViewController: UIViewController, CollectionViewController {
|
||||||
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
||||||
|
|
||||||
private var loadTask: Task<Void, Never>?
|
private var loadTask: Task<Void, Never>?
|
||||||
|
private var trendingStatusesState = TrendingStatusesState.unloaded
|
||||||
|
private let confirmLoadMoreStatuses = PassthroughSubject<Void, Never>()
|
||||||
|
|
||||||
private var isShowingTrends = false
|
private var isShowingTrends = false
|
||||||
private var shouldShowTrends: Bool {
|
private var shouldShowTrends: Bool {
|
||||||
|
@ -42,9 +45,17 @@ class TrendsViewController: UIViewController, CollectionViewController {
|
||||||
let layout = UICollectionViewCompositionalLayout { [unowned self] sectionIndex, environment in
|
let layout = UICollectionViewCompositionalLayout { [unowned self] sectionIndex, environment in
|
||||||
let sectionIdentifier = self.dataSource.snapshot().sectionIdentifiers[sectionIndex]
|
let sectionIdentifier = self.dataSource.snapshot().sectionIdentifiers[sectionIndex]
|
||||||
switch sectionIdentifier {
|
switch sectionIdentifier {
|
||||||
|
case .loadingIndicator:
|
||||||
|
var listConfig = UICollectionLayoutListConfiguration(appearance: .grouped)
|
||||||
|
listConfig.backgroundColor = .appGroupedBackground
|
||||||
|
listConfig.showsSeparators = false
|
||||||
|
return .list(using: listConfig, layoutEnvironment: environment)
|
||||||
|
|
||||||
case .trendingHashtags:
|
case .trendingHashtags:
|
||||||
var listConfig = UICollectionLayoutListConfiguration(appearance: .grouped)
|
var listConfig = UICollectionLayoutListConfiguration(appearance: .grouped)
|
||||||
listConfig.headerMode = .supplementary
|
listConfig.headerMode = .supplementary
|
||||||
|
listConfig.footerMode = .supplementary
|
||||||
|
listConfig.backgroundColor = .appGroupedBackground
|
||||||
return .list(using: listConfig, layoutEnvironment: environment)
|
return .list(using: listConfig, layoutEnvironment: environment)
|
||||||
|
|
||||||
case .trendingLinks:
|
case .trendingLinks:
|
||||||
|
@ -56,9 +67,10 @@ class TrendsViewController: UIViewController, CollectionViewController {
|
||||||
let section = NSCollectionLayoutSection(group: group)
|
let section = NSCollectionLayoutSection(group: group)
|
||||||
section.orthogonalScrollingBehavior = .groupPaging
|
section.orthogonalScrollingBehavior = .groupPaging
|
||||||
section.boundarySupplementaryItems = [
|
section.boundarySupplementaryItems = [
|
||||||
NSCollectionLayoutBoundarySupplementaryItem(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(12)), elementKind: UICollectionView.elementKindSectionHeader, alignment: .topLeading)
|
NSCollectionLayoutBoundarySupplementaryItem(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(12)), elementKind: UICollectionView.elementKindSectionHeader, alignment: .topLeading),
|
||||||
|
NSCollectionLayoutBoundarySupplementaryItem(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(30)), elementKind: UICollectionView.elementKindSectionFooter, alignment: .bottomLeading),
|
||||||
]
|
]
|
||||||
section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 0, bottom: 16, trailing: 0)
|
section.contentInsets = .zero
|
||||||
return section
|
return section
|
||||||
|
|
||||||
case .profileSuggestions:
|
case .profileSuggestions:
|
||||||
|
@ -70,14 +82,25 @@ class TrendsViewController: UIViewController, CollectionViewController {
|
||||||
let section = NSCollectionLayoutSection(group: group)
|
let section = NSCollectionLayoutSection(group: group)
|
||||||
section.orthogonalScrollingBehavior = .groupPaging
|
section.orthogonalScrollingBehavior = .groupPaging
|
||||||
section.boundarySupplementaryItems = [
|
section.boundarySupplementaryItems = [
|
||||||
NSCollectionLayoutBoundarySupplementaryItem(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(12)), elementKind: UICollectionView.elementKindSectionHeader, alignment: .topLeading)
|
NSCollectionLayoutBoundarySupplementaryItem(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(12)), elementKind: UICollectionView.elementKindSectionHeader, alignment: .topLeading),
|
||||||
|
NSCollectionLayoutBoundarySupplementaryItem(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(30)), elementKind: UICollectionView.elementKindSectionFooter, alignment: .bottomLeading),
|
||||||
]
|
]
|
||||||
section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 0, bottom: 16, trailing: 0)
|
section.contentInsets = .zero
|
||||||
return section
|
return section
|
||||||
|
|
||||||
case .trendingStatuses:
|
case .trendingStatuses:
|
||||||
var listConfig = UICollectionLayoutListConfiguration(appearance: .grouped)
|
var listConfig = UICollectionLayoutListConfiguration(appearance: .grouped)
|
||||||
listConfig.headerMode = .supplementary
|
listConfig.headerMode = .supplementary
|
||||||
|
listConfig.backgroundColor = .appGroupedBackground
|
||||||
|
listConfig.itemSeparatorHandler = { [unowned self] indexPath, sectionConfig in
|
||||||
|
var config = sectionConfig
|
||||||
|
if let item = dataSource.itemIdentifier(for: indexPath),
|
||||||
|
item.hideListSeparators {
|
||||||
|
config.topSeparatorVisibility = .hidden
|
||||||
|
config.bottomSeparatorVisibility = .hidden
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
}
|
||||||
return .list(using: listConfig, layoutEnvironment: environment)
|
return .list(using: listConfig, layoutEnvironment: environment)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -85,7 +108,7 @@ class TrendsViewController: UIViewController, CollectionViewController {
|
||||||
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||||
collectionView.delegate = self
|
collectionView.delegate = self
|
||||||
collectionView.dragDelegate = self
|
collectionView.dragDelegate = self
|
||||||
collectionView.backgroundColor = .secondarySystemBackground
|
collectionView.backgroundColor = .appGroupedBackground
|
||||||
collectionView.allowsFocus = true
|
collectionView.allowsFocus = true
|
||||||
view.addSubview(collectionView)
|
view.addSubview(collectionView)
|
||||||
|
|
||||||
|
@ -96,19 +119,35 @@ class TrendsViewController: UIViewController, CollectionViewController {
|
||||||
|
|
||||||
private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> {
|
private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> {
|
||||||
let sectionHeaderCell = UICollectionView.SupplementaryRegistration<UICollectionViewListCell>(elementKind: UICollectionView.elementKindSectionHeader) { [unowned self] (headerView, collectionView, indexPath) in
|
let sectionHeaderCell = UICollectionView.SupplementaryRegistration<UICollectionViewListCell>(elementKind: UICollectionView.elementKindSectionHeader) { [unowned self] (headerView, collectionView, indexPath) in
|
||||||
let section = self.dataSource.snapshot().sectionIdentifiers[indexPath.section]
|
let section = self.dataSource.sectionIdentifier(for: indexPath.section)!
|
||||||
var config = UIListContentConfiguration.groupedHeader()
|
var config = UIListContentConfiguration.groupedHeader()
|
||||||
config.text = section.title
|
config.text = section.title
|
||||||
headerView.contentConfiguration = config
|
headerView.contentConfiguration = config
|
||||||
}
|
}
|
||||||
|
let moreCell = UICollectionView.SupplementaryRegistration<MoreTrendsFooterCollectionViewCell>(elementKind: UICollectionView.elementKindSectionFooter) { [unowned self] supplementaryView, elementKind, indexPath in
|
||||||
|
supplementaryView.delegate = self
|
||||||
|
switch self.dataSource.sectionIdentifier(for: indexPath.section) {
|
||||||
|
case nil, .loadingIndicator, .trendingStatuses:
|
||||||
|
fatalError()
|
||||||
|
case .trendingHashtags:
|
||||||
|
supplementaryView.updateUI(.hashtags)
|
||||||
|
case .trendingLinks:
|
||||||
|
supplementaryView.updateUI(.links)
|
||||||
|
case .profileSuggestions:
|
||||||
|
supplementaryView.updateUI(.profileSuggestions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let loadingCell = UICollectionView.CellRegistration<LoadingCollectionViewCell, Void> { cell, indexPath, itemIdentifier in
|
||||||
|
cell.indicator.startAnimating()
|
||||||
|
}
|
||||||
let trendingHashtagCell = UICollectionView.CellRegistration<TrendingHashtagCollectionViewCell, Hashtag> { (cell, indexPath, hashtag) in
|
let trendingHashtagCell = UICollectionView.CellRegistration<TrendingHashtagCollectionViewCell, Hashtag> { (cell, indexPath, hashtag) in
|
||||||
cell.updateUI(hashtag: hashtag)
|
cell.updateUI(hashtag: hashtag)
|
||||||
}
|
}
|
||||||
let trendingLinkCell = UICollectionView.CellRegistration<TrendingLinkCardCollectionViewCell, Card>(cellNib: UINib(nibName: "TrendingLinkCardCollectionViewCell", bundle: .main)) { (cell, indexPath, card) in
|
let trendingLinkCell = UICollectionView.CellRegistration<TrendingLinkCardCollectionViewCell, Card>(cellNib: UINib(nibName: "TrendingLinkCardCollectionViewCell", bundle: .main)) { (cell, indexPath, card) in
|
||||||
cell.updateUI(card: card)
|
cell.updateUI(card: card)
|
||||||
}
|
}
|
||||||
let statusCell = UICollectionView.CellRegistration<TimelineStatusCollectionViewCell, (String, CollapseState)> { [unowned self] cell, indexPath, item in
|
let statusCell = UICollectionView.CellRegistration<TrendingStatusCollectionViewCell, (String, CollapseState)> { [unowned self] cell, indexPath, item in
|
||||||
cell.delegate = self
|
cell.delegate = self
|
||||||
// TODO: filter trends
|
// TODO: filter trends
|
||||||
cell.updateUI(statusID: item.0, state: item.1, filterResult: .allow, precomputedContent: nil)
|
cell.updateUI(statusID: item.0, state: item.1, filterResult: .allow, precomputedContent: nil)
|
||||||
|
@ -117,9 +156,16 @@ class TrendsViewController: UIViewController, CollectionViewController {
|
||||||
cell.delegate = self
|
cell.delegate = self
|
||||||
cell.updateUI(accountID: item.0, source: item.1)
|
cell.updateUI(accountID: item.0, source: item.1)
|
||||||
}
|
}
|
||||||
|
let confirmLoadMoreCell = UICollectionView.CellRegistration<ConfirmLoadMoreCollectionViewCell, Bool> { [unowned self] cell, indexPath, isLoading in
|
||||||
|
cell.confirmLoadMore = self.confirmLoadMoreStatuses
|
||||||
|
cell.isLoading = isLoading
|
||||||
|
}
|
||||||
|
|
||||||
let dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) { collectionView, indexPath, item in
|
let dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) { collectionView, indexPath, item in
|
||||||
switch item {
|
switch item {
|
||||||
|
case .loadingIndicator:
|
||||||
|
return collectionView.dequeueConfiguredReusableCell(using: loadingCell, for: indexPath, item: ())
|
||||||
|
|
||||||
case let .tag(hashtag):
|
case let .tag(hashtag):
|
||||||
return collectionView.dequeueConfiguredReusableCell(using: trendingHashtagCell, for: indexPath, item: hashtag)
|
return collectionView.dequeueConfiguredReusableCell(using: trendingHashtagCell, for: indexPath, item: hashtag)
|
||||||
|
|
||||||
|
@ -131,11 +177,16 @@ class TrendsViewController: UIViewController, CollectionViewController {
|
||||||
|
|
||||||
case let .account(id, source):
|
case let .account(id, source):
|
||||||
return collectionView.dequeueConfiguredReusableCell(using: accountCell, for: indexPath, item: (id, source))
|
return collectionView.dequeueConfiguredReusableCell(using: accountCell, for: indexPath, item: (id, source))
|
||||||
|
|
||||||
|
case let .confirmLoadMoreStatuses(loading):
|
||||||
|
return collectionView.dequeueConfiguredReusableCell(using: confirmLoadMoreCell, for: indexPath, item: loading)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
dataSource.supplementaryViewProvider = { (collectionView, elementKind, indexPath) in
|
dataSource.supplementaryViewProvider = { (collectionView, elementKind, indexPath) in
|
||||||
if elementKind == UICollectionView.elementKindSectionHeader {
|
if elementKind == UICollectionView.elementKindSectionHeader {
|
||||||
return collectionView.dequeueConfiguredReusableSupplementary(using: sectionHeaderCell, for: indexPath)
|
return collectionView.dequeueConfiguredReusableSupplementary(using: sectionHeaderCell, for: indexPath)
|
||||||
|
} else if elementKind == UICollectionView.elementKindSectionFooter {
|
||||||
|
return collectionView.dequeueConfiguredReusableSupplementary(using: moreCell, for: indexPath)
|
||||||
} else {
|
} else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -169,6 +220,11 @@ class TrendsViewController: UIViewController, CollectionViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||||
|
snapshot.appendSections([.loadingIndicator])
|
||||||
|
snapshot.appendItems([.loadingIndicator])
|
||||||
|
await apply(snapshot: snapshot)
|
||||||
|
|
||||||
|
snapshot = NSDiffableDataSourceSnapshot()
|
||||||
|
|
||||||
let hashtagsReq = Client.getTrendingHashtags(limit: 5)
|
let hashtagsReq = Client.getTrendingHashtags(limit: 5)
|
||||||
let hashtags = try? await mastodonController.run(hashtagsReq).0
|
let hashtags = try? await mastodonController.run(hashtagsReq).0
|
||||||
|
@ -212,8 +268,59 @@ class TrendsViewController: UIViewController, CollectionViewController {
|
||||||
|
|
||||||
if !Task.isCancelled {
|
if !Task.isCancelled {
|
||||||
await apply(snapshot: snapshot)
|
await apply(snapshot: snapshot)
|
||||||
|
if snapshot.sectionIdentifiers.contains(.trendingStatuses) {
|
||||||
|
self.trendingStatusesState = .loaded
|
||||||
|
} else {
|
||||||
|
self.trendingStatusesState = .unloaded
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
private func loadOlderStatuses() async {
|
||||||
|
guard case .loaded = trendingStatusesState else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
trendingStatusesState = .loadingOlder
|
||||||
|
|
||||||
|
let origSnapshot = dataSource.snapshot()
|
||||||
|
var snapshot = origSnapshot
|
||||||
|
if Preferences.shared.disableInfiniteScrolling {
|
||||||
|
snapshot.appendItems([.confirmLoadMoreStatuses(false)], toSection: .trendingStatuses)
|
||||||
|
await apply(snapshot: snapshot)
|
||||||
|
|
||||||
|
for await _ in confirmLoadMoreStatuses.values {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
snapshot.deleteItems([.confirmLoadMoreStatuses(false)])
|
||||||
|
snapshot.appendItems([.confirmLoadMoreStatuses(true)], toSection: .trendingStatuses)
|
||||||
|
await apply(snapshot: snapshot, animatingDifferences: false)
|
||||||
|
} else {
|
||||||
|
snapshot.appendItems([.loadingIndicator], toSection: .trendingStatuses)
|
||||||
|
await apply(snapshot: snapshot)
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
let request = Client.getTrendingStatuses(offset: origSnapshot.itemIdentifiers(inSection: .trendingStatuses).count)
|
||||||
|
let (statuses, _) = try await mastodonController.run(request)
|
||||||
|
|
||||||
|
await mastodonController.persistentContainer.addAll(statuses: statuses)
|
||||||
|
|
||||||
|
var snapshot = origSnapshot
|
||||||
|
snapshot.appendItems(statuses.map { .status($0.id, .unknown) }, toSection: .trendingStatuses)
|
||||||
|
await apply(snapshot: snapshot)
|
||||||
|
} catch {
|
||||||
|
await apply(snapshot: origSnapshot)
|
||||||
|
let config = ToastConfiguration(from: error, with: "Error Loading More Trending Statuses", in: self) { [weak self] toast in
|
||||||
|
toast.dismissToast(animated: true)
|
||||||
|
await self?.loadOlderStatuses()
|
||||||
|
}
|
||||||
|
showToast(configuration: config, animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
trendingStatusesState = .loaded
|
||||||
|
}
|
||||||
|
|
||||||
@objc private func preferencesChanged() {
|
@objc private func preferencesChanged() {
|
||||||
if isShowingTrends != shouldShowTrends {
|
if isShowingTrends != shouldShowTrends {
|
||||||
|
@ -224,9 +331,9 @@ class TrendsViewController: UIViewController, CollectionViewController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func apply(snapshot: NSDiffableDataSourceSnapshot<Section, Item>) async {
|
private func apply(snapshot: NSDiffableDataSourceSnapshot<Section, Item>, animatingDifferences: Bool = true) async {
|
||||||
await Task { @MainActor in
|
await Task { @MainActor in
|
||||||
self.dataSource.apply(snapshot)
|
self.dataSource.apply(snapshot, animatingDifferences: animatingDifferences)
|
||||||
}.value
|
}.value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -249,15 +356,26 @@ class TrendsViewController: UIViewController, CollectionViewController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension TrendsViewController {
|
||||||
|
enum TrendingStatusesState {
|
||||||
|
case unloaded
|
||||||
|
case loaded
|
||||||
|
case loadingOlder
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
extension TrendsViewController {
|
extension TrendsViewController {
|
||||||
enum Section {
|
enum Section {
|
||||||
|
case loadingIndicator
|
||||||
case trendingHashtags
|
case trendingHashtags
|
||||||
case trendingLinks
|
case trendingLinks
|
||||||
case profileSuggestions
|
case profileSuggestions
|
||||||
case trendingStatuses
|
case trendingStatuses
|
||||||
|
|
||||||
var title: String {
|
var title: String? {
|
||||||
switch self {
|
switch self {
|
||||||
|
case .loadingIndicator:
|
||||||
|
return nil
|
||||||
case .trendingHashtags:
|
case .trendingHashtags:
|
||||||
return "Trending Hashtags"
|
return "Trending Hashtags"
|
||||||
case .trendingLinks:
|
case .trendingLinks:
|
||||||
|
@ -270,13 +388,17 @@ extension TrendsViewController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
enum Item: Equatable, Hashable {
|
enum Item: Equatable, Hashable {
|
||||||
|
case loadingIndicator
|
||||||
case status(String, CollapseState)
|
case status(String, CollapseState)
|
||||||
case tag(Hashtag)
|
case tag(Hashtag)
|
||||||
case link(Card)
|
case link(Card)
|
||||||
case account(String, Suggestion.Source)
|
case account(String, Suggestion.Source)
|
||||||
|
case confirmLoadMoreStatuses(Bool)
|
||||||
|
|
||||||
static func == (lhs: Item, rhs: Item) -> Bool {
|
static func == (lhs: Item, rhs: Item) -> Bool {
|
||||||
switch (lhs, rhs) {
|
switch (lhs, rhs) {
|
||||||
|
case (.loadingIndicator, .loadingIndicator):
|
||||||
|
return true
|
||||||
case let (.status(a, _), .status(b, _)):
|
case let (.status(a, _), .status(b, _)):
|
||||||
return a == b
|
return a == b
|
||||||
case let (.tag(a), .tag(b)):
|
case let (.tag(a), .tag(b)):
|
||||||
|
@ -285,6 +407,8 @@ extension TrendsViewController {
|
||||||
return a.url == b.url
|
return a.url == b.url
|
||||||
case let (.account(a, _), .account(b, _)):
|
case let (.account(a, _), .account(b, _)):
|
||||||
return a == b
|
return a == b
|
||||||
|
case (.confirmLoadMoreStatuses(let a), .confirmLoadMoreStatuses(let b)):
|
||||||
|
return a == b
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -292,6 +416,8 @@ extension TrendsViewController {
|
||||||
|
|
||||||
func hash(into hasher: inout Hasher) {
|
func hash(into hasher: inout Hasher) {
|
||||||
switch self {
|
switch self {
|
||||||
|
case .loadingIndicator:
|
||||||
|
hasher.combine("loadingIndicator")
|
||||||
case let .status(id, _):
|
case let .status(id, _):
|
||||||
hasher.combine("status")
|
hasher.combine("status")
|
||||||
hasher.combine(id)
|
hasher.combine(id)
|
||||||
|
@ -304,17 +430,54 @@ extension TrendsViewController {
|
||||||
case let .account(id, _):
|
case let .account(id, _):
|
||||||
hasher.combine("account")
|
hasher.combine("account")
|
||||||
hasher.combine(id)
|
hasher.combine(id)
|
||||||
|
case let .confirmLoadMoreStatuses(loading):
|
||||||
|
hasher.combine("confirmLoadMoreStatuses")
|
||||||
|
hasher.combine(loading)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var shouldSelect: Bool {
|
||||||
|
switch self {
|
||||||
|
case .loadingIndicator, .confirmLoadMoreStatuses(_):
|
||||||
|
return false
|
||||||
|
default:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var hideListSeparators: Bool {
|
||||||
|
switch self {
|
||||||
|
case .loadingIndicator, .confirmLoadMoreStatuses(_):
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension TrendsViewController: UICollectionViewDelegate {
|
extension TrendsViewController: UICollectionViewDelegate {
|
||||||
|
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
|
||||||
|
if case .trendingStatuses = dataSource.sectionIdentifier(for: indexPath.section),
|
||||||
|
indexPath.row == collectionView.numberOfItems(inSection: indexPath.section) - 1 {
|
||||||
|
Task {
|
||||||
|
await self.loadOlderStatuses()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool {
|
||||||
|
return dataSource.itemIdentifier(for: indexPath)?.shouldSelect ?? false
|
||||||
|
}
|
||||||
|
|
||||||
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||||
guard let item = dataSource.itemIdentifier(for: indexPath) else {
|
guard let item = dataSource.itemIdentifier(for: indexPath) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
switch item {
|
switch item {
|
||||||
|
case .loadingIndicator, .confirmLoadMoreStatuses(_):
|
||||||
|
return
|
||||||
|
|
||||||
case let .tag(hashtag):
|
case let .tag(hashtag):
|
||||||
show(HashtagTimelineViewController(for: hashtag, mastodonController: mastodonController), sender: nil)
|
show(HashtagTimelineViewController(for: hashtag, mastodonController: mastodonController), sender: nil)
|
||||||
|
|
||||||
|
@ -338,6 +501,9 @@ extension TrendsViewController: UICollectionViewDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
switch item {
|
switch item {
|
||||||
|
case .loadingIndicator, .confirmLoadMoreStatuses(_):
|
||||||
|
return nil
|
||||||
|
|
||||||
case let .tag(hashtag):
|
case let .tag(hashtag):
|
||||||
return UIContextMenuConfiguration(identifier: nil) {
|
return UIContextMenuConfiguration(identifier: nil) {
|
||||||
HashtagTimelineViewController(for: hashtag, mastodonController: self.mastodonController)
|
HashtagTimelineViewController(for: hashtag, mastodonController: self.mastodonController)
|
||||||
|
@ -349,12 +515,13 @@ extension TrendsViewController: UICollectionViewDelegate {
|
||||||
guard let url = URL(card.url) else {
|
guard let url = URL(card.url) else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
let cell = collectionView.cellForItem(at: indexPath)!
|
||||||
return UIContextMenuConfiguration {
|
return UIContextMenuConfiguration {
|
||||||
let vc = SFSafariViewController(url: url)
|
let vc = SFSafariViewController(url: url)
|
||||||
vc.preferredControlTintColor = Preferences.shared.accentColor.color
|
vc.preferredControlTintColor = Preferences.shared.accentColor.color
|
||||||
return vc
|
return vc
|
||||||
} actionProvider: { _ in
|
} actionProvider: { _ in
|
||||||
UIMenu(children: self.actionsForTrendingLink(card: card))
|
UIMenu(children: self.actionsForTrendingLink(card: card, source: .view(cell)))
|
||||||
}
|
}
|
||||||
|
|
||||||
case let .status(id, state):
|
case let .status(id, state):
|
||||||
|
@ -422,6 +589,9 @@ extension TrendsViewController: UICollectionViewDragDelegate {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
switch item {
|
switch item {
|
||||||
|
case .loadingIndicator, .confirmLoadMoreStatuses(_):
|
||||||
|
return []
|
||||||
|
|
||||||
case let .tag(hashtag):
|
case let .tag(hashtag):
|
||||||
guard let url = URL(hashtag.url) else {
|
guard let url = URL(hashtag.url) else {
|
||||||
return []
|
return []
|
||||||
|
|
|
@ -50,7 +50,7 @@ class FindInstanceViewController: InstanceSelectorTableViewController {
|
||||||
extension FindInstanceViewController: InstanceSelectorTableViewControllerDelegate {
|
extension FindInstanceViewController: InstanceSelectorTableViewControllerDelegate {
|
||||||
func didSelectInstance(url: URL) {
|
func didSelectInstance(url: URL) {
|
||||||
let instanceTimelineController = InstanceTimelineViewController(for: url, parentMastodonController: parentMastodonController!)
|
let instanceTimelineController = InstanceTimelineViewController(for: url, parentMastodonController: parentMastodonController!)
|
||||||
instanceTimelineController.delegate = instanceTimelineDelegate
|
instanceTimelineController.instanceTimelineDelegate = instanceTimelineDelegate
|
||||||
instanceTimelineController.browsingEnabled = false
|
instanceTimelineController.browsingEnabled = false
|
||||||
show(instanceTimelineController, sender: self)
|
show(instanceTimelineController, sender: self)
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,6 +54,7 @@ class EditListAccountsViewController: EnhancedTableViewController {
|
||||||
tableView.rowHeight = UITableView.automaticDimension
|
tableView.rowHeight = UITableView.automaticDimension
|
||||||
tableView.estimatedRowHeight = 66
|
tableView.estimatedRowHeight = 66
|
||||||
tableView.allowsSelection = false
|
tableView.allowsSelection = false
|
||||||
|
tableView.backgroundColor = .appGroupedBackground
|
||||||
|
|
||||||
dataSource = DataSource(tableView: tableView, cellProvider: { (tableView, indexPath, item) -> UITableViewCell? in
|
dataSource = DataSource(tableView: tableView, cellProvider: { (tableView, indexPath, item) -> UITableViewCell? in
|
||||||
guard case let .account(id) = item else { fatalError() }
|
guard case let .account(id) = item else { fatalError() }
|
||||||
|
@ -61,6 +62,15 @@ class EditListAccountsViewController: EnhancedTableViewController {
|
||||||
let cell = tableView.dequeueReusableCell(withIdentifier: "accountCell", for: indexPath) as! AccountTableViewCell
|
let cell = tableView.dequeueReusableCell(withIdentifier: "accountCell", for: indexPath) as! AccountTableViewCell
|
||||||
cell.delegate = self
|
cell.delegate = self
|
||||||
cell.updateUI(accountID: id)
|
cell.updateUI(accountID: id)
|
||||||
|
cell.configurationUpdateHandler = { cell, state in
|
||||||
|
var config = UIBackgroundConfiguration.listGroupedCell().updated(for: state)
|
||||||
|
if state.isHighlighted || state.isSelected {
|
||||||
|
config.backgroundColor = .appSelectedCellBackground
|
||||||
|
} else {
|
||||||
|
config.backgroundColor = .appGroupedCellBackground
|
||||||
|
}
|
||||||
|
cell.backgroundConfiguration = config
|
||||||
|
}
|
||||||
return cell
|
return cell
|
||||||
})
|
})
|
||||||
dataSource.editListAccountsController = self
|
dataSource.editListAccountsController = self
|
||||||
|
|
|
@ -45,6 +45,7 @@ class LocalPredicateStatusesViewController: UIViewController, CollectionViewCont
|
||||||
|
|
||||||
override func loadView() {
|
override func loadView() {
|
||||||
var config = UICollectionLayoutListConfiguration(appearance: .plain)
|
var config = UICollectionLayoutListConfiguration(appearance: .plain)
|
||||||
|
config.backgroundColor = .appBackground
|
||||||
config.leadingSwipeActionsConfigurationProvider = { [unowned self] in
|
config.leadingSwipeActionsConfigurationProvider = { [unowned self] in
|
||||||
(collectionView.cellForItem(at: $0) as? TimelineStatusCollectionViewCell)?.leadingSwipeActions()
|
(collectionView.cellForItem(at: $0) as? TimelineStatusCollectionViewCell)?.leadingSwipeActions()
|
||||||
}
|
}
|
||||||
|
|
|
@ -85,6 +85,7 @@ struct MuteAccountView: View {
|
||||||
Text("This user's posts will be hidden from your timeline. You can still receive notifications from them.")
|
Text("This user's posts will be hidden from your timeline. You can still receive notifications from them.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.appGroupedListRowBackground()
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
Picker(selection: $duration) {
|
Picker(selection: $duration) {
|
||||||
|
@ -99,6 +100,7 @@ struct MuteAccountView: View {
|
||||||
Text("The mute will automatically be removed after the selected time.")
|
Text("The mute will automatically be removed after the selected time.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.appGroupedListRowBackground()
|
||||||
|
|
||||||
Button(action: self.mute) {
|
Button(action: self.mute) {
|
||||||
if isMuting {
|
if isMuting {
|
||||||
|
@ -113,7 +115,9 @@ struct MuteAccountView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.disabled(isMuting)
|
.disabled(isMuting)
|
||||||
|
.appGroupedListRowBackground()
|
||||||
}
|
}
|
||||||
|
.appGroupedListBackground(container: UIHostingController<MuteAccountView>.self)
|
||||||
.alertWithData("Erorr Muting", data: $error, actions: { error in
|
.alertWithData("Erorr Muting", data: $error, actions: { error in
|
||||||
Button("OK") {}
|
Button("OK") {}
|
||||||
}, message: { error in
|
}, message: { error in
|
||||||
|
|
|
@ -58,6 +58,7 @@ class NotificationsTableViewController: DiffableTimelineLikeTableViewController<
|
||||||
|
|
||||||
tableView.cellLayoutMarginsFollowReadableWidth = UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac
|
tableView.cellLayoutMarginsFollowReadableWidth = UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac
|
||||||
tableView.allowsFocus = true
|
tableView.allowsFocus = true
|
||||||
|
tableView.backgroundColor = .appBackground
|
||||||
|
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(handleStatusDeleted), name: .statusDeleted, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(handleStatusDeleted), name: .statusDeleted, object: nil)
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,6 +63,7 @@ class InstanceSelectorTableViewController: UITableViewController {
|
||||||
appearance.configureWithDefaultBackground()
|
appearance.configureWithDefaultBackground()
|
||||||
navigationItem.scrollEdgeAppearance = appearance
|
navigationItem.scrollEdgeAppearance = appearance
|
||||||
|
|
||||||
|
tableView.backgroundColor = .appGroupedBackground
|
||||||
tableView.keyboardDismissMode = .interactive
|
tableView.keyboardDismissMode = .interactive
|
||||||
tableView.register(UINib(nibName: "InstanceTableViewCell", bundle: .main), forCellReuseIdentifier: instanceCell)
|
tableView.register(UINib(nibName: "InstanceTableViewCell", bundle: .main), forCellReuseIdentifier: instanceCell)
|
||||||
tableView.rowHeight = UITableView.automaticDimension
|
tableView.rowHeight = UITableView.automaticDimension
|
||||||
|
@ -107,6 +108,10 @@ class InstanceSelectorTableViewController: UITableViewController {
|
||||||
}
|
}
|
||||||
.debounce(for: .seconds(1), scheduler: RunLoop.main)
|
.debounce(for: .seconds(1), scheduler: RunLoop.main)
|
||||||
.sink { [weak self] in self?.updateSpecificInstance(domain: $0) }
|
.sink { [weak self] in self?.updateSpecificInstance(domain: $0) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
|
super.viewWillAppear(animated)
|
||||||
|
|
||||||
loadRecommendedInstances()
|
loadRecommendedInstances()
|
||||||
}
|
}
|
||||||
|
@ -203,7 +208,7 @@ class InstanceSelectorTableViewController: UITableViewController {
|
||||||
private func createActivityIndicatorHeader() {
|
private func createActivityIndicatorHeader() {
|
||||||
let header = UITableViewHeaderFooterView()
|
let header = UITableViewHeaderFooterView()
|
||||||
header.translatesAutoresizingMaskIntoConstraints = false
|
header.translatesAutoresizingMaskIntoConstraints = false
|
||||||
header.contentView.backgroundColor = .systemGroupedBackground
|
header.contentView.backgroundColor = .appGroupedBackground
|
||||||
|
|
||||||
activityIndicator = UIActivityIndicatorView(style: .large)
|
activityIndicator = UIActivityIndicatorView(style: .large)
|
||||||
activityIndicator.translatesAutoresizingMaskIntoConstraints = false
|
activityIndicator.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
|
|
@ -42,6 +42,7 @@ struct AboutView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.appGroupedListRowBackground()
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
Link("Website", destination: URL(string: "https://vaccor.space/tusker")!)
|
Link("Website", destination: URL(string: "https://vaccor.space/tusker")!)
|
||||||
|
@ -67,7 +68,10 @@ struct AboutView: View {
|
||||||
Link("Source Code", destination: URL(string: "https://git.shadowfacts.net/shadowfacts/Tusker")!)
|
Link("Source Code", destination: URL(string: "https://git.shadowfacts.net/shadowfacts/Tusker")!)
|
||||||
Link("Issue Tracker", destination: URL(string: "https://git.shadowfacts.net/shadowfacts/Tusker/issues")!)
|
Link("Issue Tracker", destination: URL(string: "https://git.shadowfacts.net/shadowfacts/Tusker/issues")!)
|
||||||
}
|
}
|
||||||
|
.appGroupedListRowBackground()
|
||||||
}
|
}
|
||||||
|
.listStyle(.insetGrouped)
|
||||||
|
.appGroupedListBackground(container: PreferencesNavigationController.self)
|
||||||
.sheet(isPresented: $isShowingMailSheet) {
|
.sheet(isPresented: $isShowingMailSheet) {
|
||||||
MailSheet(logData: logData)
|
MailSheet(logData: logData)
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ struct AcknowledgementsView: View {
|
||||||
Text(text)
|
Text(text)
|
||||||
.padding(.horizontal, 16)
|
.padding(.horizontal, 16)
|
||||||
}
|
}
|
||||||
|
.appGroupedListBackground(container: PreferencesNavigationController.self)
|
||||||
.navigationTitle("Acknowledgements")
|
.navigationTitle("Acknowledgements")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,8 @@ struct AdvancedPrefsView : View {
|
||||||
errorReportingSection
|
errorReportingSection
|
||||||
cachingSection
|
cachingSection
|
||||||
}
|
}
|
||||||
.listStyle(InsetGroupedListStyle())
|
.listStyle(.insetGrouped)
|
||||||
|
.appGroupedListBackground(container: PreferencesNavigationController.self)
|
||||||
.navigationBarTitle(Text("Advanced"))
|
.navigationBarTitle(Text("Advanced"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,6 +51,7 @@ struct AdvancedPrefsView : View {
|
||||||
// see FB6838291
|
// see FB6838291
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.appGroupedListRowBackground()
|
||||||
}
|
}
|
||||||
|
|
||||||
var cloudKitSection: some View {
|
var cloudKitSection: some View {
|
||||||
|
@ -74,7 +76,9 @@ struct AdvancedPrefsView : View {
|
||||||
Text(String(describing: cloudKitStatus!))
|
Text(String(describing: cloudKitStatus!))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.task {
|
}
|
||||||
|
.appGroupedListRowBackground()
|
||||||
|
.task {
|
||||||
CKContainer.default().accountStatus { status, error in
|
CKContainer.default().accountStatus { status, error in
|
||||||
if let error {
|
if let error {
|
||||||
Logging.general.error("Unable to get CloudKit status: \(String(describing: error))")
|
Logging.general.error("Unable to get CloudKit status: \(String(describing: error))")
|
||||||
|
@ -99,6 +103,7 @@ struct AdvancedPrefsView : View {
|
||||||
.lineLimit(nil)
|
.lineLimit(nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.appGroupedListRowBackground()
|
||||||
}
|
}
|
||||||
|
|
||||||
var cachingSection: some View {
|
var cachingSection: some View {
|
||||||
|
@ -120,7 +125,9 @@ struct AdvancedPrefsView : View {
|
||||||
s += AttributedString("\nMastodon cache size: \(ByteCountFormatter().string(fromByteCount: mastodonCacheSize))")
|
s += AttributedString("\nMastodon cache size: \(ByteCountFormatter().string(fromByteCount: mastodonCacheSize))")
|
||||||
}
|
}
|
||||||
return Text(s)
|
return Text(s)
|
||||||
}.task {
|
}
|
||||||
|
.appGroupedListRowBackground()
|
||||||
|
.task {
|
||||||
imageCacheSize = [
|
imageCacheSize = [
|
||||||
ImageCache.avatars,
|
ImageCache.avatars,
|
||||||
.headers,
|
.headers,
|
||||||
|
|
|
@ -6,21 +6,19 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import Combine
|
||||||
|
|
||||||
struct AppearancePrefsView : View {
|
struct AppearancePrefsView : View {
|
||||||
@ObservedObject var preferences = Preferences.shared
|
@ObservedObject var preferences = Preferences.shared
|
||||||
|
|
||||||
private var theme: Binding<UIUserInterfaceStyle> = Binding(get: {
|
private var appearanceChangePublisher: some Publisher<Void, Never> {
|
||||||
Preferences.shared.theme
|
preferences.$theme
|
||||||
}, set: {
|
.map { _ in () }
|
||||||
Preferences.shared.theme = $0
|
.merge(with: preferences.$pureBlackDarkMode.map { _ in () },
|
||||||
NotificationCenter.default.post(name: .themePreferenceChanged, object: nil)
|
preferences.$accentColor.map { _ in () }
|
||||||
})
|
)
|
||||||
private var accentColor: Binding<Preferences.AccentColor> = Binding {
|
// the prefrence publishers are all willSet, but want to notify after the change, so wait one runloop iteration
|
||||||
Preferences.shared.accentColor
|
.receive(on: DispatchQueue.main)
|
||||||
} set: {
|
|
||||||
Preferences.shared.accentColor = $0
|
|
||||||
NotificationCenter.default.post(name: .themePreferenceChanged, object: nil)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var useCircularAvatars: Binding<Bool> = Binding(get: {
|
private var useCircularAvatars: Binding<Bool> = Binding(get: {
|
||||||
|
@ -50,19 +48,27 @@ struct AppearancePrefsView : View {
|
||||||
accountsSection
|
accountsSection
|
||||||
postsSection
|
postsSection
|
||||||
}
|
}
|
||||||
.listStyle(InsetGroupedListStyle())
|
.listStyle(.insetGrouped)
|
||||||
|
.appGroupedListBackground(container: PreferencesNavigationController.self)
|
||||||
.navigationBarTitle(Text("Appearance"))
|
.navigationBarTitle(Text("Appearance"))
|
||||||
}
|
}
|
||||||
|
|
||||||
private var themeSection: some View {
|
private var themeSection: some View {
|
||||||
Section {
|
Section {
|
||||||
Picker(selection: theme, label: Text("Theme")) {
|
Picker(selection: $preferences.theme, label: Text("Theme")) {
|
||||||
Text("Use System Theme").tag(UIUserInterfaceStyle.unspecified)
|
Text("Use System Theme").tag(UIUserInterfaceStyle.unspecified)
|
||||||
Text("Light").tag(UIUserInterfaceStyle.light)
|
Text("Light").tag(UIUserInterfaceStyle.light)
|
||||||
Text("Dark").tag(UIUserInterfaceStyle.dark)
|
Text("Dark").tag(UIUserInterfaceStyle.dark)
|
||||||
}
|
}
|
||||||
|
|
||||||
Picker(selection: accentColor, label: Text("Accent Color")) {
|
// macOS system dark mode isn't pure black, so this isn't necessary
|
||||||
|
if !ProcessInfo.processInfo.isMacCatalystApp && !ProcessInfo.processInfo.isiOSAppOnMac {
|
||||||
|
Toggle(isOn: $preferences.pureBlackDarkMode) {
|
||||||
|
Text("Pure Black Dark Mode")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Picker(selection: $preferences.accentColor, label: Text("Accent Color")) {
|
||||||
ForEach(accentColorsAndImages, id: \.0.rawValue) { (color, image) in
|
ForEach(accentColorsAndImages, id: \.0.rawValue) { (color, image) in
|
||||||
HStack {
|
HStack {
|
||||||
Text(color.name)
|
Text(color.name)
|
||||||
|
@ -75,6 +81,10 @@ struct AppearancePrefsView : View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.onReceive(appearanceChangePublisher) { _ in
|
||||||
|
NotificationCenter.default.post(name: .themePreferenceChanged, object: nil)
|
||||||
|
}
|
||||||
|
.appGroupedListRowBackground()
|
||||||
}
|
}
|
||||||
|
|
||||||
private var accountsSection: some View {
|
private var accountsSection: some View {
|
||||||
|
@ -86,6 +96,7 @@ struct AppearancePrefsView : View {
|
||||||
Text("Hide Custom Emoji in Usernames")
|
Text("Hide Custom Emoji in Usernames")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.appGroupedListRowBackground()
|
||||||
}
|
}
|
||||||
|
|
||||||
private var postsSection: some View {
|
private var postsSection: some View {
|
||||||
|
@ -113,6 +124,7 @@ struct AppearancePrefsView : View {
|
||||||
.navigationTitle("Trailing Swipe Actions")
|
.navigationTitle("Trailing Swipe Actions")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.appGroupedListRowBackground()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,8 @@ struct BehaviorPrefsView: View {
|
||||||
linksSection
|
linksSection
|
||||||
contentWarningsSection
|
contentWarningsSection
|
||||||
}
|
}
|
||||||
.listStyle(InsetGroupedListStyle())
|
.listStyle(.insetGrouped)
|
||||||
|
.appGroupedListBackground(container: PreferencesNavigationController.self)
|
||||||
.navigationBarTitle(Text("Behavior"))
|
.navigationBarTitle(Text("Behavior"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,6 +29,7 @@ struct BehaviorPrefsView: View {
|
||||||
Text("Require Confirmation Before Reblogging")
|
Text("Require Confirmation Before Reblogging")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.appGroupedListRowBackground()
|
||||||
}
|
}
|
||||||
|
|
||||||
private var timelineSection: some View {
|
private var timelineSection: some View {
|
||||||
|
@ -38,6 +40,7 @@ struct BehaviorPrefsView: View {
|
||||||
} header: {
|
} header: {
|
||||||
Text("Timeline")
|
Text("Timeline")
|
||||||
}
|
}
|
||||||
|
.appGroupedListRowBackground()
|
||||||
}
|
}
|
||||||
|
|
||||||
private var linksSection: some View {
|
private var linksSection: some View {
|
||||||
|
@ -52,6 +55,7 @@ struct BehaviorPrefsView: View {
|
||||||
Text("Always Use Reader Mode in In-App Safari")
|
Text("Always Use Reader Mode in In-App Safari")
|
||||||
}.disabled(!preferences.useInAppSafari)
|
}.disabled(!preferences.useInAppSafari)
|
||||||
}
|
}
|
||||||
|
.appGroupedListRowBackground()
|
||||||
}
|
}
|
||||||
|
|
||||||
private var contentWarningsSection: some View {
|
private var contentWarningsSection: some View {
|
||||||
|
@ -68,6 +72,7 @@ struct BehaviorPrefsView: View {
|
||||||
Text(preferences.expandAllContentWarnings ? "Collapse Posts with Keywords in CWs" : "Expand Posts with Keywords in CWs")
|
Text(preferences.expandAllContentWarnings ? "Collapse Posts with Keywords in CWs" : "Expand Posts with Keywords in CWs")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.appGroupedListRowBackground()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,8 @@ struct ComposingPrefsView: View {
|
||||||
replyingSection
|
replyingSection
|
||||||
writingSection
|
writingSection
|
||||||
}
|
}
|
||||||
.listStyle(InsetGroupedListStyle())
|
.listStyle(.insetGrouped)
|
||||||
|
.appGroupedListBackground(container: PreferencesNavigationController.self)
|
||||||
.navigationBarTitle("Composing")
|
.navigationBarTitle("Composing")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,6 +52,7 @@ struct ComposingPrefsView: View {
|
||||||
} footer: {
|
} footer: {
|
||||||
Text("When starting a reply, Tusker will use your preferred visibility or the visibility of the post to which you're replying, whichever is narrower.")
|
Text("When starting a reply, Tusker will use your preferred visibility or the visibility of the post to which you're replying, whichever is narrower.")
|
||||||
}
|
}
|
||||||
|
.appGroupedListRowBackground()
|
||||||
}
|
}
|
||||||
|
|
||||||
var composingSection: some View {
|
var composingSection: some View {
|
||||||
|
@ -62,6 +64,7 @@ struct ComposingPrefsView: View {
|
||||||
Text("Require Attachment Descriptions")
|
Text("Require Attachment Descriptions")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.appGroupedListRowBackground()
|
||||||
}
|
}
|
||||||
|
|
||||||
var replyingSection: some View {
|
var replyingSection: some View {
|
||||||
|
@ -75,6 +78,7 @@ struct ComposingPrefsView: View {
|
||||||
Text("Mention Reblogger")
|
Text("Mention Reblogger")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.appGroupedListRowBackground()
|
||||||
}
|
}
|
||||||
|
|
||||||
var writingSection: some View {
|
var writingSection: some View {
|
||||||
|
@ -83,6 +87,7 @@ struct ComposingPrefsView: View {
|
||||||
Text("Show @ and # on Keyboard")
|
Text("Show @ and # on Keyboard")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.appGroupedListRowBackground()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,8 @@ struct MediaPrefsView: View {
|
||||||
List {
|
List {
|
||||||
viewingSection
|
viewingSection
|
||||||
}
|
}
|
||||||
.listStyle(InsetGroupedListStyle())
|
.listStyle(.insetGrouped)
|
||||||
|
.appGroupedListBackground(container: PreferencesNavigationController.self)
|
||||||
.navigationBarTitle("Media")
|
.navigationBarTitle("Media")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,6 +43,7 @@ struct MediaPrefsView: View {
|
||||||
Text("Show Uncropped Media Inline")
|
Text("Show Uncropped Media Inline")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.appGroupedListRowBackground()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,8 +23,8 @@ struct OppositeCollapseKeywordsView: View {
|
||||||
ZStack {
|
ZStack {
|
||||||
// the background from the grouped ListStyle clips to the safe area, so when the keyboard is hiding/showing
|
// the background from the grouped ListStyle clips to the safe area, so when the keyboard is hiding/showing
|
||||||
// the color behind it can be seen, which looks odd
|
// the color behind it can be seen, which looks odd
|
||||||
Color(UIColor.secondarySystemBackground)
|
// Color(UIColor.secondarySystemBackground)
|
||||||
.edgesIgnoringSafeArea(.bottom)
|
// .edgesIgnoringSafeArea(.bottom)
|
||||||
|
|
||||||
List {
|
List {
|
||||||
Section(footer: Text("A post matches if its content warning contains the text of a keyword, ignoring case.")) {
|
Section(footer: Text("A post matches if its content warning contains the text of a keyword, ignoring case.")) {
|
||||||
|
@ -37,9 +37,11 @@ struct OppositeCollapseKeywordsView: View {
|
||||||
|
|
||||||
FocusableTextField(placeholder: "Add Keyword", text: $valueToAdd, becomeFirstResponder: $makeAddFieldFirstResponder, onCommit: self.addKeyword)
|
FocusableTextField(placeholder: "Add Keyword", text: $valueToAdd, becomeFirstResponder: $makeAddFieldFirstResponder, onCommit: self.addKeyword)
|
||||||
}
|
}
|
||||||
|
.appGroupedListRowBackground()
|
||||||
}
|
}
|
||||||
.animation(.default, value: keywords.map(\.id))
|
.animation(.default, value: keywords.map(\.id))
|
||||||
.listStyle(GroupedListStyle())
|
.listStyle(.grouped)
|
||||||
|
.appGroupedListBackground(container: PreferencesNavigationController.self)
|
||||||
}
|
}
|
||||||
.onAppear(perform: updateAppearance)
|
.onAppear(perform: updateAppearance)
|
||||||
.navigationBarTitle(preferences.expandAllContentWarnings ? "Collapse Post CW Keywords" : "Expand Post CW Keywords")
|
.navigationBarTitle(preferences.expandAllContentWarnings ? "Collapse Post CW Keywords" : "Expand Post CW Keywords")
|
||||||
|
|
|
@ -24,6 +24,7 @@ struct PreferencesView: View {
|
||||||
aboutSection
|
aboutSection
|
||||||
}
|
}
|
||||||
.listStyle(.insetGrouped)
|
.listStyle(.insetGrouped)
|
||||||
|
.appGroupedListBackground(container: PreferencesNavigationController.self)
|
||||||
.navigationBarTitle("Preferences")
|
.navigationBarTitle("Preferences")
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
}
|
}
|
||||||
|
@ -84,6 +85,7 @@ struct PreferencesView: View {
|
||||||
} header: {
|
} header: {
|
||||||
Text("Accounts")
|
Text("Accounts")
|
||||||
}
|
}
|
||||||
|
.appGroupedListRowBackground()
|
||||||
}
|
}
|
||||||
|
|
||||||
private var preferencesSection: some View {
|
private var preferencesSection: some View {
|
||||||
|
@ -107,6 +109,7 @@ struct PreferencesView: View {
|
||||||
Text("Advanced")
|
Text("Advanced")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.appGroupedListRowBackground()
|
||||||
}
|
}
|
||||||
|
|
||||||
private var aboutSection: some View {
|
private var aboutSection: some View {
|
||||||
|
@ -121,6 +124,7 @@ struct PreferencesView: View {
|
||||||
AcknowledgementsView()
|
AcknowledgementsView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.appGroupedListRowBackground()
|
||||||
}
|
}
|
||||||
|
|
||||||
func logoutPressed() {
|
func logoutPressed() {
|
||||||
|
|
|
@ -42,6 +42,7 @@ class SwipeActionsPrefsViewController: UIViewController, UICollectionViewDelegat
|
||||||
override func loadView() {
|
override func loadView() {
|
||||||
let layout = UICollectionViewCompositionalLayout { [unowned self] sectionIndex, environment in
|
let layout = UICollectionViewCompositionalLayout { [unowned self] sectionIndex, environment in
|
||||||
var config = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
|
var config = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
|
||||||
|
config.backgroundColor = .appGroupedBackground
|
||||||
if dataSource.sectionIdentifier(for: sectionIndex) == .selected {
|
if dataSource.sectionIdentifier(for: sectionIndex) == .selected {
|
||||||
config.headerMode = .supplementary
|
config.headerMode = .supplementary
|
||||||
}
|
}
|
||||||
|
@ -59,6 +60,15 @@ class SwipeActionsPrefsViewController: UIViewController, UICollectionViewDelegat
|
||||||
config.image = UIImage(systemName: item.systemImageName)
|
config.image = UIImage(systemName: item.systemImageName)
|
||||||
cell.contentConfiguration = config
|
cell.contentConfiguration = config
|
||||||
cell.accessories = [.reorder(displayed: .always)]
|
cell.accessories = [.reorder(displayed: .always)]
|
||||||
|
cell.configurationUpdateHandler = { cell, state in
|
||||||
|
var config = UIBackgroundConfiguration.listGroupedCell().updated(for: state)
|
||||||
|
if state.isHighlighted || state.isSelected {
|
||||||
|
config.backgroundColor = .appSelectedCellBackground
|
||||||
|
} else {
|
||||||
|
config.backgroundColor = .appGroupedCellBackground
|
||||||
|
}
|
||||||
|
cell.backgroundConfiguration = config
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in
|
let dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in
|
||||||
return collectionView.dequeueConfiguredReusableCell(using: listCell, for: indexPath, item: itemIdentifier)
|
return collectionView.dequeueConfiguredReusableCell(using: listCell, for: indexPath, item: itemIdentifier)
|
||||||
|
|
|
@ -26,8 +26,12 @@ struct TipJarView: View {
|
||||||
@StateObject private var observer = UbiquitousKeyValueStoreObserver()
|
@StateObject private var observer = UbiquitousKeyValueStoreObserver()
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
ZStack {
|
||||||
|
Color.appGroupedBackground
|
||||||
|
.edgesIgnoringSafeArea(.all)
|
||||||
|
|
||||||
productsView
|
productsView
|
||||||
.overlay {
|
|
||||||
if showConfetti {
|
if showConfetti {
|
||||||
ConfettiView()
|
ConfettiView()
|
||||||
.transition(.opacity.animation(.default))
|
.transition(.opacity.animation(.default))
|
||||||
|
|
|
@ -19,7 +19,8 @@ struct WellnessPrefsView: View {
|
||||||
disableInfiniteScrolling
|
disableInfiniteScrolling
|
||||||
hideTrends
|
hideTrends
|
||||||
}
|
}
|
||||||
.listStyle(InsetGroupedListStyle())
|
.listStyle(.insetGrouped)
|
||||||
|
.appGroupedListBackground(container: PreferencesNavigationController.self)
|
||||||
.navigationBarTitle(Text("Digital Wellness"))
|
.navigationBarTitle(Text("Digital Wellness"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,6 +30,7 @@ struct WellnessPrefsView: View {
|
||||||
Text("Favorite and Reblog Counts in Conversations")
|
Text("Favorite and Reblog Counts in Conversations")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.appGroupedListRowBackground()
|
||||||
}
|
}
|
||||||
|
|
||||||
private var notificationsMode: some View {
|
private var notificationsMode: some View {
|
||||||
|
@ -39,6 +41,7 @@ struct WellnessPrefsView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.appGroupedListRowBackground()
|
||||||
}
|
}
|
||||||
|
|
||||||
private var grayscaleImages: some View {
|
private var grayscaleImages: some View {
|
||||||
|
@ -47,6 +50,7 @@ struct WellnessPrefsView: View {
|
||||||
Text("Grayscale Images")
|
Text("Grayscale Images")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.appGroupedListRowBackground()
|
||||||
}
|
}
|
||||||
|
|
||||||
private var disableInfiniteScrolling: some View {
|
private var disableInfiniteScrolling: some View {
|
||||||
|
@ -55,6 +59,7 @@ struct WellnessPrefsView: View {
|
||||||
Text("Disable Infinite Scrolling")
|
Text("Disable Infinite Scrolling")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.appGroupedListRowBackground()
|
||||||
}
|
}
|
||||||
|
|
||||||
private var hideTrends: some View {
|
private var hideTrends: some View {
|
||||||
|
@ -63,6 +68,7 @@ struct WellnessPrefsView: View {
|
||||||
Text("Hide Trends")
|
Text("Hide Trends")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.appGroupedListRowBackground()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,7 @@ class ProfileHeaderCollectionViewCell: UICollectionViewCell {
|
||||||
override init(frame: CGRect) {
|
override init(frame: CGRect) {
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
|
|
||||||
contentView.backgroundColor = .systemBackground
|
contentView.backgroundColor = .appBackground
|
||||||
isOpaque = true
|
isOpaque = true
|
||||||
contentView.isOpaque = true
|
contentView.isOpaque = true
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,6 +56,7 @@ class ProfileStatusesViewController: UIViewController, TimelineLikeCollectionVie
|
||||||
|
|
||||||
override func loadView() {
|
override func loadView() {
|
||||||
var config = UICollectionLayoutListConfiguration(appearance: .plain)
|
var config = UICollectionLayoutListConfiguration(appearance: .plain)
|
||||||
|
config.backgroundColor = .appBackground
|
||||||
config.leadingSwipeActionsConfigurationProvider = { [unowned self] in
|
config.leadingSwipeActionsConfigurationProvider = { [unowned self] in
|
||||||
(collectionView.cellForItem(at: $0) as? TimelineStatusCollectionViewCell)?.leadingSwipeActions()
|
(collectionView.cellForItem(at: $0) as? TimelineStatusCollectionViewCell)?.leadingSwipeActions()
|
||||||
}
|
}
|
||||||
|
@ -82,7 +83,9 @@ class ProfileStatusesViewController: UIViewController, TimelineLikeCollectionVie
|
||||||
}
|
}
|
||||||
let layout = UICollectionViewCompositionalLayout { [unowned self] sectionIndex, environment in
|
let layout = UICollectionViewCompositionalLayout { [unowned self] sectionIndex, environment in
|
||||||
if case .header = dataSource.sectionIdentifier(for: sectionIndex) {
|
if case .header = dataSource.sectionIdentifier(for: sectionIndex) {
|
||||||
return .list(using: .init(appearance: .plain), layoutEnvironment: environment)
|
var config = UICollectionLayoutListConfiguration(appearance: .plain)
|
||||||
|
config.backgroundColor = .appBackground
|
||||||
|
return .list(using: config, layoutEnvironment: environment)
|
||||||
} else {
|
} else {
|
||||||
let section = NSCollectionLayoutSection.list(using: config, layoutEnvironment: environment)
|
let section = NSCollectionLayoutSection.list(using: config, layoutEnvironment: environment)
|
||||||
if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
|
if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
|
||||||
|
|
|
@ -68,7 +68,7 @@ class ProfileViewController: UIViewController {
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
view.backgroundColor = .systemBackground
|
view.backgroundColor = .appBackground
|
||||||
|
|
||||||
for pageController in pageControllers {
|
for pageController in pageControllers {
|
||||||
pageController.profileHeaderDelegate = self
|
pageController.profileHeaderDelegate = self
|
||||||
|
|
|
@ -34,8 +34,15 @@ struct ReportAddStatusView: View {
|
||||||
ReportStatusView(status: status, mastodonController: mastodonController)
|
ReportStatusView(status: status, mastodonController: mastodonController)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.appGroupedListRowBackground()
|
||||||
}
|
}
|
||||||
|
.modifier(ScrollBackgroundModifier())
|
||||||
} else {
|
} else {
|
||||||
|
ZStack {
|
||||||
|
// because the background needs to fill the entire screen
|
||||||
|
Color.appGroupedBackground
|
||||||
|
.edgesIgnoringSafeArea(.all)
|
||||||
|
|
||||||
ProgressView()
|
ProgressView()
|
||||||
.progressViewStyle(.circular)
|
.progressViewStyle(.circular)
|
||||||
.alertWithData("Error Loading Posts", data: $error, actions: { _ in
|
.alertWithData("Error Loading Posts", data: $error, actions: { _ in
|
||||||
|
@ -55,4 +62,29 @@ struct ReportAddStatusView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,11 +47,27 @@ struct ReportSelectRulesView: View {
|
||||||
.foregroundColor(selectedRuleIDs.contains(rule.id) ? .accentColor : .clear)
|
.foregroundColor(selectedRuleIDs.contains(rule.id) ? .accentColor : .clear)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.appGroupedListRowBackground()
|
||||||
}
|
}
|
||||||
|
.withAppBackgroundIfAvailable()
|
||||||
.navigationTitle("Rules")
|
.navigationTitle("Rules")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private extension View {
|
||||||
|
@available(iOS, obsoleted: 16.0)
|
||||||
|
@ViewBuilder
|
||||||
|
func withAppBackgroundIfAvailable() -> some View {
|
||||||
|
if #available(iOS 16.0, *) {
|
||||||
|
self
|
||||||
|
.scrollContentBackground(.hidden)
|
||||||
|
.background(Color.appGroupedBackground)
|
||||||
|
} else {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//struct ReportSelectRulesView_Previews: PreviewProvider {
|
//struct ReportSelectRulesView_Previews: PreviewProvider {
|
||||||
// static var previews: some View {
|
// static var previews: some View {
|
||||||
// ReportSelectRulesView()
|
// ReportSelectRulesView()
|
||||||
|
|
|
@ -32,6 +32,7 @@ struct ReportView: View {
|
||||||
if #available(iOS 16.0, *) {
|
if #available(iOS 16.0, *) {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
navigationViewContent
|
navigationViewContent
|
||||||
|
.scrollDismissesKeyboard(.interactively)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
NavigationView {
|
NavigationView {
|
||||||
|
@ -93,12 +94,14 @@ struct ReportView: View {
|
||||||
} header: {
|
} header: {
|
||||||
Text("Reason")
|
Text("Reason")
|
||||||
}
|
}
|
||||||
|
.appGroupedListRowBackground()
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
ComposeTextView(text: $report.comment, placeholder: Text("Add any additional comments"))
|
ComposeTextView(text: $report.comment, placeholder: Text("Add any additional comments"))
|
||||||
.backgroundColor(.clear)
|
.backgroundColor(.clear)
|
||||||
.listRowInsets(EdgeInsets(top: 8, leading: 8, bottom: 8, trailing: 8))
|
.listRowInsets(EdgeInsets(top: 8, leading: 8, bottom: 8, trailing: 8))
|
||||||
}
|
}
|
||||||
|
.appGroupedListRowBackground()
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
ForEach(report.statusIDs, id: \.self) { id in
|
ForEach(report.statusIDs, id: \.self) { id in
|
||||||
|
@ -116,12 +119,14 @@ struct ReportView: View {
|
||||||
} footer: {
|
} footer: {
|
||||||
Text("Attach posts to your report to provide additional context for moderators.")
|
Text("Attach posts to your report to provide additional context for moderators.")
|
||||||
}
|
}
|
||||||
|
.appGroupedListRowBackground()
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
Toggle("Forward", isOn: $report.forward)
|
Toggle("Forward", isOn: $report.forward)
|
||||||
} footer: {
|
} footer: {
|
||||||
Text("You can choose to anonymously forward your report to the moderators of **\(account.url.host!)**.")
|
Text("You can choose to anonymously forward your report to the moderators of **\(account.url.host!)**.")
|
||||||
}
|
}
|
||||||
|
.appGroupedListRowBackground()
|
||||||
|
|
||||||
Button(action: self.sendReport) {
|
Button(action: self.sendReport) {
|
||||||
if isReporting {
|
if isReporting {
|
||||||
|
@ -134,7 +139,10 @@ struct ReportView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.disabled(isReporting)
|
.disabled(isReporting)
|
||||||
|
.appGroupedListRowBackground()
|
||||||
}
|
}
|
||||||
|
.listStyle(.insetGrouped)
|
||||||
|
.appGroupedListBackground(container: UIHostingController<ReportView>.self, applyBackground: true)
|
||||||
.alertWithData("Error Reporting", data: $error, actions: { error in
|
.alertWithData("Error Reporting", data: $error, actions: { error in
|
||||||
Button("OK") {}
|
Button("OK") {}
|
||||||
}, message: { error in
|
}, message: { error in
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
import UIKit
|
import UIKit
|
||||||
import Combine
|
import Combine
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
|
import WebURLFoundationExtras
|
||||||
|
|
||||||
fileprivate let accountCell = "accountCell"
|
fileprivate let accountCell = "accountCell"
|
||||||
fileprivate let statusCell = "statusCell"
|
fileprivate let statusCell = "statusCell"
|
||||||
|
@ -26,17 +27,15 @@ extension SearchResultsViewControllerDelegate {
|
||||||
func selectedSearchResult(status statusID: String) {}
|
func selectedSearchResult(status statusID: String) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SearchResultsViewController: EnhancedTableViewController {
|
class SearchResultsViewController: UIViewController, CollectionViewController {
|
||||||
|
|
||||||
weak var mastodonController: MastodonController!
|
weak var mastodonController: MastodonController!
|
||||||
|
|
||||||
weak var exploreNavigationController: UINavigationController?
|
weak var exploreNavigationController: UINavigationController?
|
||||||
weak var delegate: SearchResultsViewControllerDelegate?
|
weak var delegate: SearchResultsViewControllerDelegate?
|
||||||
|
|
||||||
var dataSource: UITableViewDiffableDataSource<Section, Item>!
|
var collectionView: UICollectionView! { view as? UICollectionView }
|
||||||
|
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
||||||
private var activityIndicator: UIActivityIndicatorView!
|
|
||||||
private var errorLabel: UILabel!
|
|
||||||
|
|
||||||
/// Types of results to search for.
|
/// Types of results to search for.
|
||||||
var scope: Scope
|
var scope: Scope
|
||||||
|
@ -50,9 +49,7 @@ class SearchResultsViewController: EnhancedTableViewController {
|
||||||
self.mastodonController = mastodonController
|
self.mastodonController = mastodonController
|
||||||
self.scope = scope
|
self.scope = scope
|
||||||
|
|
||||||
super.init(style: .grouped)
|
super.init(nibName: nil, bundle: nil)
|
||||||
|
|
||||||
dragEnabled = true
|
|
||||||
|
|
||||||
title = NSLocalizedString("Search", comment: "search screen title")
|
title = NSLocalizedString("Search", comment: "search screen title")
|
||||||
}
|
}
|
||||||
|
@ -61,59 +58,45 @@ class SearchResultsViewController: EnhancedTableViewController {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func loadView() {
|
||||||
|
let layout = UICollectionViewCompositionalLayout { [unowned self] sectionIndex, environment in
|
||||||
|
var config = UICollectionLayoutListConfiguration(appearance: .grouped)
|
||||||
|
config.backgroundColor = .appGroupedBackground
|
||||||
|
config.headerMode = .supplementary
|
||||||
|
switch self.dataSource.sectionIdentifier(for: sectionIndex) {
|
||||||
|
case .loadingIndicator:
|
||||||
|
config.showsSeparators = false
|
||||||
|
config.headerMode = .none
|
||||||
|
case .statuses:
|
||||||
|
config.leadingSwipeActionsConfigurationProvider = { [unowned self] in
|
||||||
|
(self.collectionView.cellForItem(at: $0) as? TimelineStatusCollectionViewCell)?.leadingSwipeActions()
|
||||||
|
}
|
||||||
|
config.trailingSwipeActionsConfigurationProvider = { [unowned self] in
|
||||||
|
(self.collectionView.cellForItem(at: $0) as? TimelineStatusCollectionViewCell)?.trailingSwipeActions()
|
||||||
|
}
|
||||||
|
config.separatorConfiguration.topSeparatorInsets = TimelineStatusCollectionViewCell.separatorInsets
|
||||||
|
config.separatorConfiguration.bottomSeparatorInsets = TimelineStatusCollectionViewCell.separatorInsets
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
let section = NSCollectionLayoutSection.list(using: config, layoutEnvironment: environment)
|
||||||
|
if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
|
||||||
|
section.contentInsetsReference = .readableContent
|
||||||
|
}
|
||||||
|
return section
|
||||||
|
}
|
||||||
|
view = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
||||||
|
collectionView.delegate = self
|
||||||
|
collectionView.dragDelegate = self
|
||||||
|
collectionView.allowsFocus = true
|
||||||
|
collectionView.backgroundColor = .appGroupedBackground
|
||||||
|
|
||||||
|
dataSource = createDataSource()
|
||||||
|
}
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
errorLabel = UILabel()
|
|
||||||
errorLabel.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
errorLabel.font = .preferredFont(forTextStyle: .callout)
|
|
||||||
errorLabel.textColor = .secondaryLabel
|
|
||||||
errorLabel.numberOfLines = 0
|
|
||||||
errorLabel.textAlignment = .center
|
|
||||||
errorLabel.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
||||||
tableView.addSubview(errorLabel)
|
|
||||||
NSLayoutConstraint.activate([
|
|
||||||
errorLabel.centerYAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerYAnchor),
|
|
||||||
errorLabel.centerXAnchor.constraint(equalTo: tableView.centerXAnchor),
|
|
||||||
errorLabel.leadingAnchor.constraint(equalToSystemSpacingAfter: tableView.leadingAnchor, multiplier: 1),
|
|
||||||
tableView.trailingAnchor.constraint(equalToSystemSpacingAfter: errorLabel.trailingAnchor, multiplier: 1),
|
|
||||||
])
|
|
||||||
|
|
||||||
tableView.register(UINib(nibName: "AccountTableViewCell", bundle: .main), forCellReuseIdentifier: accountCell)
|
|
||||||
tableView.register(UINib(nibName: "TimelineStatusTableViewCell", bundle: .main), forCellReuseIdentifier: statusCell)
|
|
||||||
tableView.register(UINib(nibName: "HashtagTableViewCell", bundle: .main), forCellReuseIdentifier: hashtagCell)
|
|
||||||
|
|
||||||
tableView.allowsFocus = true
|
|
||||||
|
|
||||||
dataSource = DataSource(tableView: tableView, cellProvider: { (tableView, indexPath, item) -> UITableViewCell? in
|
|
||||||
switch item {
|
|
||||||
case let .account(id):
|
|
||||||
let cell = tableView.dequeueReusableCell(withIdentifier: accountCell, for: indexPath) as! AccountTableViewCell
|
|
||||||
cell.delegate = self
|
|
||||||
cell.updateUI(accountID: id)
|
|
||||||
return cell
|
|
||||||
case let .hashtag(tag):
|
|
||||||
let cell = tableView.dequeueReusableCell(withIdentifier: hashtagCell, for: indexPath) as! HashtagTableViewCell
|
|
||||||
cell.delegate = self
|
|
||||||
cell.updateUI(hashtag: tag)
|
|
||||||
return cell
|
|
||||||
case let .status(id, state):
|
|
||||||
let cell = tableView.dequeueReusableCell(withIdentifier: statusCell, for: indexPath) as! TimelineStatusTableViewCell
|
|
||||||
cell.delegate = self
|
|
||||||
cell.updateUI(statusID: id, state: state)
|
|
||||||
return cell
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
activityIndicator = UIActivityIndicatorView(style: .large)
|
|
||||||
activityIndicator.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
activityIndicator.isHidden = true
|
|
||||||
view.addSubview(activityIndicator)
|
|
||||||
NSLayoutConstraint.activate([
|
|
||||||
activityIndicator.centerXAnchor.constraint(equalTo: view.centerXAnchor),
|
|
||||||
activityIndicator.topAnchor.constraint(equalTo: view.topAnchor, constant: 8)
|
|
||||||
])
|
|
||||||
|
|
||||||
_ = searchSubject
|
_ = searchSubject
|
||||||
.debounce(for: .milliseconds(500), scheduler: RunLoop.main)
|
.debounce(for: .milliseconds(500), scheduler: RunLoop.main)
|
||||||
.map { $0?.trimmingCharacters(in: .whitespacesAndNewlines) }
|
.map { $0?.trimmingCharacters(in: .whitespacesAndNewlines) }
|
||||||
|
@ -125,6 +108,66 @@ class SearchResultsViewController: EnhancedTableViewController {
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(handleStatusDeleted), name: .statusDeleted, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(handleStatusDeleted), name: .statusDeleted, object: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> {
|
||||||
|
let sectionHeader = UICollectionView.SupplementaryRegistration<UICollectionViewListCell>(elementKind: UICollectionView.elementKindSectionHeader) { [unowned self] supplementaryView, elementKind, indexPath in
|
||||||
|
let section = self.dataSource.sectionIdentifier(for: indexPath.section)!
|
||||||
|
var config = UIListContentConfiguration.groupedHeader()
|
||||||
|
config.text = section.displayName
|
||||||
|
supplementaryView.contentConfiguration = config
|
||||||
|
}
|
||||||
|
let loadingCell = UICollectionView.CellRegistration<LoadingCollectionViewCell, Void> { cell, indexPath, itemIdentifier in
|
||||||
|
cell.indicator.startAnimating()
|
||||||
|
}
|
||||||
|
let accountCell = UICollectionView.CellRegistration<AccountCollectionViewCell, String> { cell, indexPath, itemIdentifier in
|
||||||
|
cell.delegate = self
|
||||||
|
cell.updateUI(accountID: itemIdentifier)
|
||||||
|
}
|
||||||
|
let hashtagCell = UICollectionView.CellRegistration<TrendingHashtagCollectionViewCell, Hashtag> { cell, indexPath, itemIdentifier in
|
||||||
|
cell.updateUI(hashtag: itemIdentifier)
|
||||||
|
}
|
||||||
|
let statusCell = UICollectionView.CellRegistration<TimelineStatusCollectionViewCell, (String, CollapseState)> { [unowned self] cell, indexPath, itemIdentifier in
|
||||||
|
cell.delegate = self
|
||||||
|
cell.updateUI(statusID: itemIdentifier.0, state: itemIdentifier.1, filterResult: .allow, precomputedContent: nil)
|
||||||
|
}
|
||||||
|
let dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in
|
||||||
|
let cell: UICollectionViewCell
|
||||||
|
switch itemIdentifier {
|
||||||
|
case .loadingIndicator:
|
||||||
|
return collectionView.dequeueConfiguredReusableCell(using: loadingCell, for: indexPath, item: ())
|
||||||
|
case .account(let accountID):
|
||||||
|
cell = collectionView.dequeueConfiguredReusableCell(using: accountCell, for: indexPath, item: accountID)
|
||||||
|
case .hashtag(let hashtag):
|
||||||
|
cell = collectionView.dequeueConfiguredReusableCell(using: hashtagCell, for: indexPath, item: hashtag)
|
||||||
|
case .status(let id, let state):
|
||||||
|
cell = collectionView.dequeueConfiguredReusableCell(using: statusCell, for: indexPath, item: (id, state))
|
||||||
|
}
|
||||||
|
cell.configurationUpdateHandler = { cell, state in
|
||||||
|
var config = UIBackgroundConfiguration.listGroupedCell().updated(for: state)
|
||||||
|
if state.isHighlighted || state.isSelected {
|
||||||
|
config.backgroundColor = .appSelectedCellBackground
|
||||||
|
} else {
|
||||||
|
config.backgroundColor = .appGroupedCellBackground
|
||||||
|
}
|
||||||
|
cell.backgroundConfiguration = config
|
||||||
|
}
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
dataSource.supplementaryViewProvider = { collectionView, elementKind, indexPath in
|
||||||
|
if elementKind == UICollectionView.elementKindSectionHeader {
|
||||||
|
return collectionView.dequeueConfiguredReusableSupplementary(using: sectionHeader, for: indexPath)
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dataSource
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
|
super.viewWillAppear(animated)
|
||||||
|
|
||||||
|
clearSelectionOnAppear(animated: animated)
|
||||||
|
}
|
||||||
|
|
||||||
override func targetViewController(forAction action: Selector, sender: Any?) -> UIViewController? {
|
override func targetViewController(forAction action: Selector, sender: Any?) -> UIViewController? {
|
||||||
// if we're showing a view controller, we need to go up to the explore VC's nav controller
|
// if we're showing a view controller, we need to go up to the explore VC's nav controller
|
||||||
// the UISearchController that is our parent is not part of the normal VC hierarchy and itself doesn't have a parent
|
// the UISearchController that is our parent is not part of the normal VC hierarchy and itself doesn't have a parent
|
||||||
|
@ -149,25 +192,19 @@ class SearchResultsViewController: EnhancedTableViewController {
|
||||||
}
|
}
|
||||||
self.currentQuery = query
|
self.currentQuery = query
|
||||||
|
|
||||||
activityIndicator.isHidden = false
|
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||||
activityIndicator.startAnimating()
|
snapshot.appendSections([.loadingIndicator])
|
||||||
errorLabel.isHidden = true
|
snapshot.appendItems([.loadingIndicator])
|
||||||
|
dataSource.apply(snapshot)
|
||||||
|
|
||||||
let request = Client.search(query: query, types: scope.resultTypes, resolve: true, limit: 10, following: following)
|
let request = Client.search(query: query, types: scope.resultTypes, resolve: true, limit: 10, following: following)
|
||||||
mastodonController.run(request) { (response) in
|
mastodonController.run(request) { (response) in
|
||||||
switch response {
|
switch response {
|
||||||
case let .success(results, _):
|
case let .success(results, _):
|
||||||
guard self.currentQuery == query else { return }
|
guard self.currentQuery == query else { return }
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.activityIndicator.isHidden = true
|
|
||||||
self.activityIndicator.stopAnimating()
|
|
||||||
}
|
|
||||||
self.showSearchResults(results)
|
self.showSearchResults(results)
|
||||||
case let .failure(error):
|
case let .failure(error):
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.activityIndicator.isHidden = true
|
|
||||||
self.activityIndicator.stopAnimating()
|
|
||||||
|
|
||||||
self.showSearchError(error)
|
self.showSearchError(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -195,7 +232,6 @@ class SearchResultsViewController: EnhancedTableViewController {
|
||||||
}
|
}
|
||||||
}, completion: {
|
}, completion: {
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.errorLabel.isHidden = true
|
|
||||||
self.dataSource.apply(snapshot)
|
self.dataSource.apply(snapshot)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -205,8 +241,11 @@ class SearchResultsViewController: EnhancedTableViewController {
|
||||||
let snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
let snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||||
dataSource.apply(snapshot)
|
dataSource.apply(snapshot)
|
||||||
|
|
||||||
errorLabel.isHidden = false
|
let config = ToastConfiguration(from: error, with: "Error Searching", in: self) { [unowned self] toast in
|
||||||
errorLabel.text = error.localizedDescription
|
toast.dismissToast(animated: true)
|
||||||
|
self.performSearch(query: self.currentQuery)
|
||||||
|
}
|
||||||
|
showToast(configuration: config, animated: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func handleStatusDeleted(_ notification: Foundation.Notification) {
|
@objc private func handleStatusDeleted(_ notification: Foundation.Notification) {
|
||||||
|
@ -230,26 +269,6 @@ class SearchResultsViewController: EnhancedTableViewController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// MARK: - Table view delegate
|
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
|
||||||
if let delegate = delegate {
|
|
||||||
switch dataSource.itemIdentifier(for: indexPath) {
|
|
||||||
case nil:
|
|
||||||
return
|
|
||||||
case let .account(id):
|
|
||||||
delegate.selectedSearchResult(account: id)
|
|
||||||
case let .hashtag(hashtag):
|
|
||||||
delegate.selectedSearchResult(hashtag: hashtag)
|
|
||||||
case let .status(id, _):
|
|
||||||
delegate.selectedSearchResult(status: id)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
super.tableView(tableView, didSelectRowAt: indexPath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension SearchResultsViewController {
|
extension SearchResultsViewController {
|
||||||
|
@ -289,12 +308,15 @@ extension SearchResultsViewController {
|
||||||
|
|
||||||
extension SearchResultsViewController {
|
extension SearchResultsViewController {
|
||||||
enum Section: CaseIterable {
|
enum Section: CaseIterable {
|
||||||
|
case loadingIndicator
|
||||||
case accounts
|
case accounts
|
||||||
case hashtags
|
case hashtags
|
||||||
case statuses
|
case statuses
|
||||||
|
|
||||||
var displayName: String {
|
var displayName: String? {
|
||||||
switch self {
|
switch self {
|
||||||
|
case .loadingIndicator:
|
||||||
|
return nil
|
||||||
case .accounts:
|
case .accounts:
|
||||||
return NSLocalizedString("People", comment: "accounts search results section")
|
return NSLocalizedString("People", comment: "accounts search results section")
|
||||||
case .hashtags:
|
case .hashtags:
|
||||||
|
@ -305,12 +327,15 @@ extension SearchResultsViewController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
enum Item: Hashable {
|
enum Item: Hashable {
|
||||||
|
case loadingIndicator
|
||||||
case account(String)
|
case account(String)
|
||||||
case hashtag(Hashtag)
|
case hashtag(Hashtag)
|
||||||
case status(String, CollapseState)
|
case status(String, CollapseState)
|
||||||
|
|
||||||
func hash(into hasher: inout Hasher) {
|
func hash(into hasher: inout Hasher) {
|
||||||
switch self {
|
switch self {
|
||||||
|
case .loadingIndicator:
|
||||||
|
hasher.combine("loadingIndicator")
|
||||||
case let .account(id):
|
case let .account(id):
|
||||||
hasher.combine("account")
|
hasher.combine("account")
|
||||||
hasher.combine(id)
|
hasher.combine(id)
|
||||||
|
@ -322,16 +347,121 @@ extension SearchResultsViewController {
|
||||||
hasher.combine(id)
|
hasher.combine(id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func ==(lhs: Item, rhs: Item) -> Bool {
|
||||||
|
switch (lhs, rhs) {
|
||||||
|
case (.loadingIndicator, .loadingIndicator):
|
||||||
|
return true
|
||||||
|
case (.account(let a), .account(let b)):
|
||||||
|
return a == b
|
||||||
|
case (.hashtag(let a), .hashtag(let b)):
|
||||||
|
return a.name == b.name
|
||||||
|
case (.status(let a, _), .status(let b, _)):
|
||||||
|
return a == b
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SearchResultsViewController: UICollectionViewDelegate {
|
||||||
|
func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool {
|
||||||
|
switch dataSource.itemIdentifier(for: indexPath) {
|
||||||
|
case .loadingIndicator:
|
||||||
|
return false
|
||||||
|
default:
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class DataSource: UITableViewDiffableDataSource<Section, Item> {
|
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||||
override func tableView(_ tableView: UITableView, titleForHeaderInSection sectionIndex: Int) -> String? {
|
switch dataSource.itemIdentifier(for: indexPath) {
|
||||||
let currentSnapshot = snapshot()
|
case nil, .loadingIndicator:
|
||||||
for section in Section.allCases where currentSnapshot.indexOfSection(section) == sectionIndex {
|
return
|
||||||
return section.displayName
|
case let .account(id):
|
||||||
|
if let delegate {
|
||||||
|
delegate.selectedSearchResult(account: id)
|
||||||
|
} else {
|
||||||
|
selected(account: id)
|
||||||
}
|
}
|
||||||
|
case let .hashtag(hashtag):
|
||||||
|
if let delegate {
|
||||||
|
delegate.selectedSearchResult(hashtag: hashtag)
|
||||||
|
} else {
|
||||||
|
selected(tag: hashtag)
|
||||||
|
}
|
||||||
|
case let .status(id, state):
|
||||||
|
if let delegate {
|
||||||
|
delegate.selectedSearchResult(status: id)
|
||||||
|
} else {
|
||||||
|
selected(status: id, state: state.copy())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
|
||||||
|
guard let item = dataSource.itemIdentifier(for: indexPath),
|
||||||
|
let cell = collectionView.cellForItem(at: indexPath) else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
switch item {
|
||||||
|
case .loadingIndicator:
|
||||||
|
return nil
|
||||||
|
case .account(let id):
|
||||||
|
return UIContextMenuConfiguration {
|
||||||
|
ProfileViewController(accountID: id, mastodonController: self.mastodonController)
|
||||||
|
} actionProvider: { _ in
|
||||||
|
UIMenu(children: self.actionsForProfile(accountID: id, source: .view(cell)))
|
||||||
|
}
|
||||||
|
case .hashtag(let tag):
|
||||||
|
return UIContextMenuConfiguration {
|
||||||
|
HashtagTimelineViewController(for: tag, mastodonController: self.mastodonController)
|
||||||
|
} actionProvider: { _ in
|
||||||
|
UIMenu(children: self.actionsForHashtag(tag, source: .view(cell)))
|
||||||
|
}
|
||||||
|
case .status(_, _):
|
||||||
|
return (cell as? TimelineStatusCollectionViewCell)?.contextMenuConfiguration()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectionView(_ collectionView: UICollectionView, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) {
|
||||||
|
MenuPreviewHelper.willPerformPreviewAction(animator: animator, presenter: self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SearchResultsViewController: UICollectionViewDragDelegate {
|
||||||
|
func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
|
||||||
|
guard let accountInfo = mastodonController.accountInfo,
|
||||||
|
let item = dataSource.itemIdentifier(for: indexPath) else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
let url: URL
|
||||||
|
let activity: NSUserActivity
|
||||||
|
switch item {
|
||||||
|
case .loadingIndicator:
|
||||||
|
return []
|
||||||
|
case .account(let id):
|
||||||
|
guard let account = mastodonController.persistentContainer.account(for: id) else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
url = account.url
|
||||||
|
activity = UserActivityManager.showProfileActivity(id: id, accountID: accountInfo.id)
|
||||||
|
case .hashtag(let tag):
|
||||||
|
url = URL(tag.url)!
|
||||||
|
activity = UserActivityManager.showTimelineActivity(timeline: .tag(hashtag: tag.name), accountID: accountInfo.id)!
|
||||||
|
case .status(let id, _):
|
||||||
|
guard let status = mastodonController.persistentContainer.status(for: id),
|
||||||
|
status.url != nil else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
url = status.url!
|
||||||
|
activity = UserActivityManager.showConversationActivity(mainStatusID: id, accountID: accountInfo.id)
|
||||||
|
}
|
||||||
|
activity.displaysAuxiliaryScene = true
|
||||||
|
let provider = NSItemProvider(object: url as NSURL)
|
||||||
|
provider.registerObject(activity, visibility: .all)
|
||||||
|
return [UIDragItem(itemProvider: provider)]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -348,8 +478,25 @@ extension SearchResultsViewController: UISearchBarDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
func searchBar(_ searchBar: UISearchBar, selectedScopeButtonIndexDidChange selectedScope: Int) {
|
func searchBar(_ searchBar: UISearchBar, selectedScopeButtonIndexDidChange selectedScope: Int) {
|
||||||
self.scope = Scope.allCases[selectedScope]
|
let newQuery = searchBar.text?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
performSearch(query: searchBar.text?.trimmingCharacters(in: .whitespacesAndNewlines))
|
let newScope = Scope.allCases[selectedScope]
|
||||||
|
if self.scope == .all && currentQuery == newQuery {
|
||||||
|
self.scope = newScope
|
||||||
|
var snapshot = dataSource.snapshot()
|
||||||
|
if snapshot.sectionIdentifiers.contains(.accounts) && scope != .people {
|
||||||
|
snapshot.deleteSections([.accounts])
|
||||||
|
}
|
||||||
|
if snapshot.sectionIdentifiers.contains(.hashtags) && scope != .hashtags {
|
||||||
|
snapshot.deleteSections([.hashtags])
|
||||||
|
}
|
||||||
|
if snapshot.sectionIdentifiers.contains(.statuses) && scope != .posts {
|
||||||
|
snapshot.deleteSections([.statuses])
|
||||||
|
}
|
||||||
|
dataSource.apply(snapshot)
|
||||||
|
} else {
|
||||||
|
self.scope = newScope
|
||||||
|
performSearch(query: newQuery)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -363,9 +510,16 @@ extension SearchResultsViewController: ToastableViewController {
|
||||||
extension SearchResultsViewController: MenuActionProvider {
|
extension SearchResultsViewController: MenuActionProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension SearchResultsViewController: StatusTableViewCellDelegate {
|
extension SearchResultsViewController: StatusCollectionViewCellDelegate {
|
||||||
func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) {
|
func statusCellNeedsReconfigure(_ cell: StatusCollectionViewCell, animated: Bool, completion: (() -> Void)?) {
|
||||||
tableView.beginUpdates()
|
if let indexPath = collectionView.indexPath(for: cell) {
|
||||||
tableView.endUpdates()
|
var snapshot = dataSource.snapshot()
|
||||||
|
snapshot.reconfigureItems([dataSource.itemIdentifier(for: indexPath)!])
|
||||||
|
dataSource.apply(snapshot, animatingDifferences: animated, completion: completion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func statusCellShowFiltered(_ cell: StatusCollectionViewCell) {
|
||||||
|
// not yet supported
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -86,10 +86,30 @@ class StatusActionAccountListCollectionViewController: UIViewController, Collect
|
||||||
let statusCell = UICollectionView.CellRegistration<TimelineStatusCollectionViewCell, (String, CollapseState)> { [unowned self] cell, indexPath, item in
|
let statusCell = UICollectionView.CellRegistration<TimelineStatusCollectionViewCell, (String, CollapseState)> { [unowned self] cell, indexPath, item in
|
||||||
cell.delegate = self
|
cell.delegate = self
|
||||||
cell.updateUI(statusID: item.0, state: item.1, filterResult: .allow, precomputedContent: nil)
|
cell.updateUI(statusID: item.0, state: item.1, filterResult: .allow, precomputedContent: nil)
|
||||||
|
|
||||||
|
cell.configurationUpdateHandler = { cell, state in
|
||||||
|
var config = UIBackgroundConfiguration.listGroupedCell().updated(for: state)
|
||||||
|
if state.isHighlighted || state.isSelected {
|
||||||
|
config.backgroundColor = .appSelectedCellBackground
|
||||||
|
} else {
|
||||||
|
config.backgroundColor = .appGroupedCellBackground
|
||||||
|
}
|
||||||
|
cell.backgroundConfiguration = config
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let accountCell = UICollectionView.CellRegistration<AccountCollectionViewCell, String> { [unowned self] cell, indexPath, item in
|
let accountCell = UICollectionView.CellRegistration<AccountCollectionViewCell, String> { [unowned self] cell, indexPath, item in
|
||||||
cell.delegate = self
|
cell.delegate = self
|
||||||
cell.updateUI(accountID: item)
|
cell.updateUI(accountID: item)
|
||||||
|
|
||||||
|
cell.configurationUpdateHandler = { cell, state in
|
||||||
|
var config = UIBackgroundConfiguration.listGroupedCell().updated(for: state)
|
||||||
|
if state.isHighlighted || state.isSelected {
|
||||||
|
config.backgroundColor = .appSelectedCellBackground
|
||||||
|
} else {
|
||||||
|
config.backgroundColor = .appGroupedCellBackground
|
||||||
|
}
|
||||||
|
cell.backgroundConfiguration = config
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let loadingCell = UICollectionView.CellRegistration<LoadingCollectionViewCell, Void> { cell, indexPath, item in
|
let loadingCell = UICollectionView.CellRegistration<LoadingCollectionViewCell, Void> { cell, indexPath, item in
|
||||||
cell.indicator.startAnimating()
|
cell.indicator.startAnimating()
|
||||||
|
|
|
@ -88,7 +88,7 @@ class StatusActionAccountListViewController: UIViewController {
|
||||||
title = NSLocalizedString("Reblogged By", comment: "status reblogged by accounts list title")
|
title = NSLocalizedString("Reblogged By", comment: "status reblogged by accounts list title")
|
||||||
}
|
}
|
||||||
|
|
||||||
view.backgroundColor = .systemBackground
|
view.backgroundColor = .appBackground
|
||||||
|
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(handleStatusDeleted), name: .statusDeleted, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(handleStatusDeleted), name: .statusDeleted, object: nil)
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@ protocol InstanceTimelineViewControllerDelegate: AnyObject {
|
||||||
|
|
||||||
class InstanceTimelineViewController: TimelineViewController {
|
class InstanceTimelineViewController: TimelineViewController {
|
||||||
|
|
||||||
weak var delegate: InstanceTimelineViewControllerDelegate?
|
weak var instanceTimelineDelegate: InstanceTimelineViewControllerDelegate?
|
||||||
|
|
||||||
weak var parentMastodonController: MastodonController?
|
weak var parentMastodonController: MastodonController?
|
||||||
|
|
||||||
|
@ -102,7 +102,7 @@ class InstanceTimelineViewController: TimelineViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
collectionView.isHidden = true
|
collectionView.isHidden = true
|
||||||
view.backgroundColor = .systemBackground
|
view.backgroundColor = .appBackground
|
||||||
|
|
||||||
let image = UIImageView(image: UIImage(systemName: "lock.fill"))
|
let image = UIImageView(image: UIImage(systemName: "lock.fill"))
|
||||||
image.tintColor = .secondaryLabel
|
image.tintColor = .secondaryLabel
|
||||||
|
@ -145,10 +145,10 @@ class InstanceTimelineViewController: TimelineViewController {
|
||||||
let existing = try? context.fetch(req).first
|
let existing = try? context.fetch(req).first
|
||||||
if let existing = existing {
|
if let existing = existing {
|
||||||
context.delete(existing)
|
context.delete(existing)
|
||||||
delegate?.didUnsaveInstance(url: instanceURL)
|
instanceTimelineDelegate?.didUnsaveInstance(url: instanceURL)
|
||||||
} else {
|
} else {
|
||||||
_ = SavedInstance(url: instanceURL, account: parentMastodonController!.accountInfo!, context: context)
|
_ = SavedInstance(url: instanceURL, account: parentMastodonController!.accountInfo!, context: context)
|
||||||
delegate?.didSaveInstance(url: instanceURL)
|
instanceTimelineDelegate?.didSaveInstance(url: instanceURL)
|
||||||
}
|
}
|
||||||
mastodonController.persistentContainer.save(context: context)
|
mastodonController.persistentContainer.save(context: context)
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,7 @@ class TimelineGapCollectionViewCell: UICollectionViewCell {
|
||||||
|
|
||||||
override var isHighlighted: Bool {
|
override var isHighlighted: Bool {
|
||||||
didSet {
|
didSet {
|
||||||
backgroundColor = isHighlighted ? .systemFill : .systemGroupedBackground
|
backgroundColor = isHighlighted ? .appFill : .appGroupedBackground
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,7 +38,7 @@ class TimelineGapCollectionViewCell: UICollectionViewCell {
|
||||||
override init(frame: CGRect) {
|
override init(frame: CGRect) {
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
|
|
||||||
backgroundColor = .systemGroupedBackground
|
backgroundColor = .appGroupedBackground
|
||||||
|
|
||||||
indicator.isHidden = true
|
indicator.isHidden = true
|
||||||
indicator.color = .tintColor
|
indicator.color = .tintColor
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -11,7 +11,23 @@ import Pachyderm
|
||||||
import Combine
|
import Combine
|
||||||
import Sentry
|
import Sentry
|
||||||
|
|
||||||
|
protocol TimelineViewControllerDelegate: AnyObject {
|
||||||
|
func timelineViewController(_ timelineViewController: TimelineViewController, willShowJumpToPresentToastWith animator: UIViewPropertyAnimator?)
|
||||||
|
func timelineViewController(_ timelineViewController: TimelineViewController, willDismissJumpToPresentToastWith animator: UIViewPropertyAnimator?)
|
||||||
|
func timelineViewController(_ timelineViewController: TimelineViewController, willShowSyncToastWith animator: UIViewPropertyAnimator?)
|
||||||
|
func timelineViewController(_ timelineViewController: TimelineViewController, willDismissSyncToastWith animator: UIViewPropertyAnimator?)
|
||||||
|
}
|
||||||
|
|
||||||
|
extension TimelineViewControllerDelegate {
|
||||||
|
func timelineViewController(_ timelineViewController: TimelineViewController, willShowJumpToPresentToastWith animator: UIViewPropertyAnimator?) {}
|
||||||
|
func timelineViewController(_ timelineViewController: TimelineViewController, willDismissJumpToPresentToastWith animator: UIViewPropertyAnimator?) {}
|
||||||
|
func timelineViewController(_ timelineViewController: TimelineViewController, willShowSyncToastWith animator: UIViewPropertyAnimator?) {}
|
||||||
|
func timelineViewController(_ timelineViewController: TimelineViewController, willDismissSyncToastWith animator: UIViewPropertyAnimator?) {}
|
||||||
|
}
|
||||||
|
|
||||||
class TimelineViewController: UIViewController, TimelineLikeCollectionViewController, CollectionViewController, RefreshableViewController {
|
class TimelineViewController: UIViewController, TimelineLikeCollectionViewController, CollectionViewController, RefreshableViewController {
|
||||||
|
weak var delegate: TimelineViewControllerDelegate?
|
||||||
|
|
||||||
let timeline: Timeline
|
let timeline: Timeline
|
||||||
weak var mastodonController: MastodonController!
|
weak var mastodonController: MastodonController!
|
||||||
let filterer: Filterer
|
let filterer: Filterer
|
||||||
|
@ -63,6 +79,7 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
var config = UICollectionLayoutListConfiguration(appearance: .plain)
|
var config = UICollectionLayoutListConfiguration(appearance: .plain)
|
||||||
|
config.backgroundColor = .appBackground
|
||||||
config.leadingSwipeActionsConfigurationProvider = { [unowned self] in
|
config.leadingSwipeActionsConfigurationProvider = { [unowned self] in
|
||||||
(collectionView.cellForItem(at: $0) as? TimelineStatusCollectionViewCell)?.leadingSwipeActions()
|
(collectionView.cellForItem(at: $0) as? TimelineStatusCollectionViewCell)?.leadingSwipeActions()
|
||||||
}
|
}
|
||||||
|
@ -99,6 +116,7 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
|
||||||
collectionView.delegate = self
|
collectionView.delegate = self
|
||||||
collectionView.dragDelegate = self
|
collectionView.dragDelegate = self
|
||||||
collectionView.allowsFocus = true
|
collectionView.allowsFocus = true
|
||||||
|
collectionView.backgroundColor = .appBackground
|
||||||
collectionView.translatesAutoresizingMaskIntoConstraints = false
|
collectionView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
view.addSubview(collectionView)
|
view.addSubview(collectionView)
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
|
@ -143,7 +161,9 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
|
||||||
}
|
}
|
||||||
.debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)
|
.debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)
|
||||||
.sink { [unowned self] _ in
|
.sink { [unowned self] _ in
|
||||||
_ = syncPositionIfNecessary(alwaysPrompt: true)
|
Task {
|
||||||
|
_ = await syncPositionIfNecessary(alwaysPrompt: true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(handleStatusDeleted), name: .statusDeleted, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(handleStatusDeleted), name: .statusDeleted, object: nil)
|
||||||
|
@ -225,7 +245,7 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
|
||||||
if case .notLoadedInitial = controller.state {
|
if case .notLoadedInitial = controller.state {
|
||||||
Task {
|
Task {
|
||||||
if await restoreState() {
|
if await restoreState() {
|
||||||
await checkPresent(jumpImmediately: false)
|
await checkPresent(jumpImmediately: false, animateImmediateJump: false)
|
||||||
} else {
|
} else {
|
||||||
await controller.loadInitial()
|
await controller.loadInitial()
|
||||||
}
|
}
|
||||||
|
@ -357,28 +377,12 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
private func loadStatusesToRestore(position: TimelinePosition) async -> Bool {
|
private func loadStatusesToRestore(position: TimelinePosition) async -> Bool {
|
||||||
{
|
|
||||||
let crumb = Breadcrumb(level: .info, category: "TimelineViewController")
|
|
||||||
crumb.message = "Original statusIDs before filtering"
|
|
||||||
crumb.data = [
|
|
||||||
"statusIDs": position.statusIDs,
|
|
||||||
]
|
|
||||||
SentrySDK.addBreadcrumb(crumb)
|
|
||||||
}()
|
|
||||||
let originalPositionStatusIDs = position.statusIDs
|
let originalPositionStatusIDs = position.statusIDs
|
||||||
|
|
||||||
let unloaded = position.statusIDs.filter({ mastodonController.persistentContainer.status(for: $0) == nil })
|
let unloaded = position.statusIDs.filter({ mastodonController.persistentContainer.status(for: $0) == nil })
|
||||||
guard !unloaded.isEmpty else {
|
guard !unloaded.isEmpty else {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
{
|
|
||||||
let crumb = Breadcrumb(level: .info, category: "TimelineViewController")
|
|
||||||
crumb.message = "Unloaded ids"
|
|
||||||
crumb.data = [
|
|
||||||
"unloaded": unloaded
|
|
||||||
]
|
|
||||||
SentrySDK.addBreadcrumb(crumb)
|
|
||||||
}()
|
|
||||||
let results = await withTaskGroup(of: (String, Result<Status, Swift.Error>).self) { group -> [(String, Result<Status, Swift.Error>)] in
|
let results = await withTaskGroup(of: (String, Result<Status, Swift.Error>).self) { group -> [(String, Result<Status, Swift.Error>)] in
|
||||||
for id in unloaded {
|
for id in unloaded {
|
||||||
group.addTask {
|
group.addTask {
|
||||||
|
@ -399,9 +403,6 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
|
||||||
switch result {
|
switch result {
|
||||||
case .success(let status):
|
case .success(let status):
|
||||||
statuses.append(status)
|
statuses.append(status)
|
||||||
let crumb = Breadcrumb(level: .info, category: "TimelineViewController")
|
|
||||||
crumb.message = "Loaded status \(id)"
|
|
||||||
SentrySDK.addBreadcrumb(crumb)
|
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
let crumb = Breadcrumb(level: .error, category: "TimelineViewController")
|
let crumb = Breadcrumb(level: .error, category: "TimelineViewController")
|
||||||
crumb.message = "Error loading status"
|
crumb.message = "Error loading status"
|
||||||
|
@ -414,15 +415,6 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
|
||||||
}
|
}
|
||||||
await mastodonController.persistentContainer.addAll(statuses: statuses, in: mastodonController.persistentContainer.viewContext)
|
await mastodonController.persistentContainer.addAll(statuses: statuses, in: mastodonController.persistentContainer.viewContext)
|
||||||
|
|
||||||
_ = {
|
|
||||||
let crumb = Breadcrumb(level: .info, category: "TimelineViewController")
|
|
||||||
crumb.message = "Position statusIDs before filtering"
|
|
||||||
crumb.data = [
|
|
||||||
"statusIDs": position.statusIDs,
|
|
||||||
]
|
|
||||||
SentrySDK.addBreadcrumb(crumb)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// if an icloud sync completed in between starting to load the statuses and finishing, try to load again
|
// if an icloud sync completed in between starting to load the statuses and finishing, try to load again
|
||||||
if position.statusIDs != originalPositionStatusIDs {
|
if position.statusIDs != originalPositionStatusIDs {
|
||||||
let crumb = Breadcrumb(level: .info, category: "TimelineViewController")
|
let crumb = Breadcrumb(level: .info, category: "TimelineViewController")
|
||||||
|
@ -444,15 +436,6 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
|
||||||
!unloaded.contains(id) || statuses.contains(where: { $0.id == id })
|
!unloaded.contains(id) || statuses.contains(where: { $0.id == id })
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
|
||||||
let crumb = Breadcrumb(level: .info, category: "TimelineViewController")
|
|
||||||
crumb.message = "Filtered position statusIDs"
|
|
||||||
crumb.data = [
|
|
||||||
"statusIDs": position.statusIDs,
|
|
||||||
]
|
|
||||||
SentrySDK.addBreadcrumb(crumb)
|
|
||||||
}()
|
|
||||||
|
|
||||||
return !position.statusIDs.isEmpty
|
return !position.statusIDs.isEmpty
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -563,7 +546,7 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
|
||||||
saveState()
|
saveState()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func syncPositionIfNecessary(alwaysPrompt: Bool) -> Bool {
|
func syncPositionIfNecessary(alwaysPrompt: Bool) async -> Bool {
|
||||||
guard persistsState,
|
guard persistsState,
|
||||||
let timelinePosition = mastodonController.persistentContainer.getTimelinePosition(timeline: timeline) else {
|
let timelinePosition = mastodonController.persistentContainer.getTimelinePosition(timeline: timeline) else {
|
||||||
return false
|
return false
|
||||||
|
@ -591,9 +574,7 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
|
||||||
}
|
}
|
||||||
stateRestorationLogger.info("Potential restore with centerStatusID: \(timelinePosition.centerStatusID ?? "<none>")")
|
stateRestorationLogger.info("Potential restore with centerStatusID: \(timelinePosition.centerStatusID ?? "<none>")")
|
||||||
if !alwaysPrompt {
|
if !alwaysPrompt {
|
||||||
Task {
|
|
||||||
_ = await restoreState()
|
_ = await restoreState()
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
var config = ToastConfiguration(title: "Sync Position")
|
var config = ToastConfiguration(title: "Sync Position")
|
||||||
config.edge = .top
|
config.edge = .top
|
||||||
|
@ -615,6 +596,12 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
|
||||||
toast.dismissToast(animated: true)
|
toast.dismissToast(animated: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
config.onAppear = { [unowned self] animator in
|
||||||
|
self.delegate?.timelineViewController(self, willShowSyncToastWith: animator)
|
||||||
|
}
|
||||||
|
config.onDismiss = { [unowned self] animator in
|
||||||
|
self.delegate?.timelineViewController(self, willDismissSyncToastWith: animator)
|
||||||
|
}
|
||||||
showToast(configuration: config, animated: true)
|
showToast(configuration: config, animated: true)
|
||||||
UIAccessibility.post(notification: .announcement, argument: "Synced Position Updated")
|
UIAccessibility.post(notification: .announcement, argument: "Synced Position Updated")
|
||||||
}
|
}
|
||||||
|
@ -652,32 +639,33 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
self.disappearedAt = nil
|
self.disappearedAt = nil
|
||||||
if syncPositionIfNecessary(alwaysPrompt: false) {
|
Task {
|
||||||
|
if await syncPositionIfNecessary(alwaysPrompt: false) {
|
||||||
// no-op
|
// no-op
|
||||||
} else {
|
} else {
|
||||||
Task {
|
await checkPresent(jumpImmediately: false, animateImmediateJump: false)
|
||||||
await checkPresent(jumpImmediately: false)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkPresent(jumpImmediately: Bool) async {
|
func checkPresent(jumpImmediately: Bool, animateImmediateJump: Bool) async {
|
||||||
if case .idle = controller.state,
|
guard case .idle = controller.state,
|
||||||
let presentItems = try? await loadInitial(),
|
let presentItems = try? await loadInitial(),
|
||||||
!presentItems.isEmpty {
|
!presentItems.isEmpty else {
|
||||||
|
return
|
||||||
|
}
|
||||||
if jumpImmediately {
|
if jumpImmediately {
|
||||||
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||||
snapshot.appendSections([.statuses])
|
snapshot.appendSections([.statuses])
|
||||||
snapshot.appendItems(presentItems.map { .status(id: $0, collapseState: .unknown, filterState: .unknown) }, toSection: .statuses)
|
snapshot.appendItems(presentItems.map { .status(id: $0, collapseState: .unknown, filterState: .unknown) }, toSection: .statuses)
|
||||||
dataSource.apply(snapshot, animatingDifferences: false) {
|
dataSource.apply(snapshot, animatingDifferences: animateImmediateJump) {
|
||||||
self.collectionView.scrollToItem(at: IndexPath(row: 0, section: 0), at: .top, animated: false)
|
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)))
|
UIAccessibility.post(notification: .screenChanged, argument: self.collectionView.cellForItem(at: IndexPath(row: 0, section: 0)))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
insertPresentItemsAndShowJumpToast(presentItems)
|
insertPresentItemsAndShowJumpToast(presentItems)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private func insertPresentItemsAndShowJumpToast(_ presentItems: [String]) {
|
private func insertPresentItemsAndShowJumpToast(_ presentItems: [String]) {
|
||||||
var snapshot = dataSource.snapshot()
|
var snapshot = dataSource.snapshot()
|
||||||
|
@ -778,6 +766,12 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
|
||||||
self.showToast(configuration: config, animated: true)
|
self.showToast(configuration: config, animated: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
config.onAppear = { [unowned self] animator in
|
||||||
|
self.delegate?.timelineViewController(self, willShowJumpToPresentToastWith: animator)
|
||||||
|
}
|
||||||
|
config.onDismiss = { [unowned self] animator in
|
||||||
|
self.delegate?.timelineViewController(self, willDismissJumpToPresentToastWith: animator)
|
||||||
|
}
|
||||||
self.showToast(configuration: config, animated: true)
|
self.showToast(configuration: config, animated: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,12 +13,22 @@ import Combine
|
||||||
|
|
||||||
class TimelinesPageViewController: SegmentedPageViewController<TimelinesPageViewController.Page> {
|
class TimelinesPageViewController: SegmentedPageViewController<TimelinesPageViewController.Page> {
|
||||||
|
|
||||||
|
static let jumpToPresentTitle: NSAttributedString = {
|
||||||
|
let s = NSMutableAttributedString("Jump to Present")
|
||||||
|
// otherwise it pronounces it as 'pɹizˈənt'
|
||||||
|
// its IPA is also bad, this should be an alveolar approximant not a trill
|
||||||
|
s.addAttribute(.accessibilitySpeechIPANotation, value: "ˈprɛ.zənt", range: NSRange(location: "Jump to ".count, length: "Present".count))
|
||||||
|
return s
|
||||||
|
}()
|
||||||
|
|
||||||
private let homeTitle = NSLocalizedString("Home", comment: "home timeline tab title")
|
private let homeTitle = NSLocalizedString("Home", comment: "home timeline tab title")
|
||||||
private let federatedTitle = NSLocalizedString("Federated", comment: "federated timeline tab title")
|
private let federatedTitle = NSLocalizedString("Federated", comment: "federated timeline tab title")
|
||||||
private let localTitle = NSLocalizedString("Local", comment: "local timeline tab title")
|
private let localTitle = NSLocalizedString("Local", comment: "local timeline tab title")
|
||||||
|
|
||||||
weak var mastodonController: MastodonController!
|
weak var mastodonController: MastodonController!
|
||||||
|
|
||||||
|
private let jumpButton = TimelineJumpButton()
|
||||||
|
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
init(mastodonController: MastodonController) {
|
init(mastodonController: MastodonController) {
|
||||||
|
@ -46,30 +56,17 @@ class TimelinesPageViewController: SegmentedPageViewController<TimelinesPageView
|
||||||
customizeItem.accessibilityLabel = "Customize Timelines"
|
customizeItem.accessibilityLabel = "Customize Timelines"
|
||||||
navigationItem.rightBarButtonItem = customizeItem
|
navigationItem.rightBarButtonItem = customizeItem
|
||||||
|
|
||||||
let jumpToPresentName = NSMutableAttributedString("Jump to Present")
|
jumpButton.action = { [unowned self] mode in
|
||||||
// otherwise it pronounces it as 'pɹizˈənt'
|
switch mode {
|
||||||
// its IPA is also bad, this should be an alveolar approximant not a trill
|
case .jump:
|
||||||
jumpToPresentName.addAttribute(.accessibilitySpeechIPANotation, value: "ˈprɛ.zənt", range: NSRange(location: "Jump to ".count, length: "Present".count))
|
await (self.currentViewController as! TimelineViewController).checkPresent(jumpImmediately: true, animateImmediateJump: true)
|
||||||
segmentedControl.accessibilityCustomActions = [
|
case .sync:
|
||||||
UIAccessibilityCustomAction(attributedName: jumpToPresentName, actionHandler: { [unowned self] _ in
|
_ = await (self.currentViewController as! TimelineViewController).syncPositionIfNecessary(alwaysPrompt: false)
|
||||||
guard let vc = currentViewController as? TimelineViewController else {
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
Task {
|
|
||||||
await vc.checkPresent(jumpImmediately: true)
|
|
||||||
}
|
}
|
||||||
return true
|
let jumpItem = UIBarButtonItem(customView: jumpButton)
|
||||||
}),
|
jumpItem.accessibilityAttributedLabel = Self.jumpToPresentTitle
|
||||||
UIAccessibilityCustomAction(name: "Jump to Sync Position", actionHandler: { [unowned self] _ in
|
navigationItem.leftBarButtonItem = jumpItem
|
||||||
guard let vc = currentViewController as? TimelineViewController else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
Task {
|
|
||||||
_ = await vc.restoreState()
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}),
|
|
||||||
]
|
|
||||||
|
|
||||||
mastodonController.accountPreferences.publisher(for: \.pinnedTimelinesData)
|
mastodonController.accountPreferences.publisher(for: \.pinnedTimelinesData)
|
||||||
.map { _ in () }
|
.map { _ in () }
|
||||||
|
@ -87,6 +84,23 @@ class TimelinesPageViewController: SegmentedPageViewController<TimelinesPageView
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func configureViewController(_ viewController: UIViewController) {
|
||||||
|
let vc = viewController as! TimelineViewController
|
||||||
|
vc.delegate = self
|
||||||
|
}
|
||||||
|
|
||||||
|
override func selectPage(_ page: Page, animated: Bool) {
|
||||||
|
super.selectPage(page, animated: animated)
|
||||||
|
if jumpButton.offscreen {
|
||||||
|
jumpButton.setMode(.jump, animated: false)
|
||||||
|
UIView.animate(withDuration: 0.2, delay: 0) {
|
||||||
|
self.jumpButton.offscreen = false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
jumpButton.setMode(.jump, animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func selectTimeline(_ timeline: PinnedTimeline, animated: Bool) {
|
func selectTimeline(_ timeline: PinnedTimeline, animated: Bool) {
|
||||||
self.selectPage(Page(mastodonController: mastodonController, timeline: timeline), animated: animated)
|
self.selectPage(Page(mastodonController: mastodonController, timeline: timeline), animated: animated)
|
||||||
}
|
}
|
||||||
|
@ -136,3 +150,75 @@ extension TimelinesPageViewController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension TimelinesPageViewController: TimelineViewControllerDelegate {
|
||||||
|
func timelineViewController(_ timelineViewController: TimelineViewController, willShowJumpToPresentToastWith animator: UIViewPropertyAnimator?) {
|
||||||
|
guard timelineViewController === currentViewController else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if let animator {
|
||||||
|
animator.addAnimations {
|
||||||
|
self.jumpButton.offscreen = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.jumpButton.offscreen = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func timelineViewController(_ timelineViewController: TimelineViewController, willDismissJumpToPresentToastWith animator: UIViewPropertyAnimator?) {
|
||||||
|
guard timelineViewController === currentViewController else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
jumpButton.setMode(.jump, animated: false)
|
||||||
|
if let animator {
|
||||||
|
animator.addAnimations {
|
||||||
|
self.jumpButton.offscreen = false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.jumpButton.offscreen = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func timelineViewController(_ timelineViewController: TimelineViewController, willShowSyncToastWith animator: UIViewPropertyAnimator?) {
|
||||||
|
guard timelineViewController === currentViewController else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if let animator {
|
||||||
|
animator.addAnimations {
|
||||||
|
self.jumpButton.offscreen = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.jumpButton.offscreen = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func timelineViewController(_ timelineViewController: TimelineViewController, willDismissSyncToastWith animator: UIViewPropertyAnimator?) {
|
||||||
|
guard timelineViewController === currentViewController else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
jumpButton.setMode(.sync, animated: false)
|
||||||
|
func resetJumpButtonMode() {
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(5)) { [weak self] in
|
||||||
|
guard let self,
|
||||||
|
timelineViewController === self.currentViewController,
|
||||||
|
!self.jumpButton.isSyncing else { return }
|
||||||
|
self.jumpButton.setMode(.jump, animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let animator {
|
||||||
|
animator.addAnimations {
|
||||||
|
self.jumpButton.offscreen = false
|
||||||
|
}
|
||||||
|
animator.addCompletion { position in
|
||||||
|
if position == .end {
|
||||||
|
resetJumpButtonMode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.jumpButton.offscreen = false
|
||||||
|
resetJumpButtonMode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -47,7 +47,7 @@ class CustomAlertController: UIViewController {
|
||||||
|
|
||||||
blurView = UIVisualEffectView(effect: UIBlurEffect(style: .systemChromeMaterial))
|
blurView = UIVisualEffectView(effect: UIBlurEffect(style: .systemChromeMaterial))
|
||||||
|
|
||||||
blurView.backgroundColor = .systemBackground
|
blurView.backgroundColor = .appBackground
|
||||||
blurView.layer.cornerRadius = 15
|
blurView.layer.cornerRadius = 15
|
||||||
blurView.layer.cornerCurve = .continuous
|
blurView.layer.cornerCurve = .continuous
|
||||||
blurView.layer.masksToBounds = true
|
blurView.layer.masksToBounds = true
|
||||||
|
|
|
@ -346,12 +346,16 @@ extension MenuActionProvider {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func actionsForTrendingLink(card: Card) -> [UIMenuElement] {
|
func actionsForTrendingLink(card: Card, source: PopoverSource) -> [UIMenuElement] {
|
||||||
guard let url = URL(card.url) else {
|
guard let url = URL(card.url) else {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
return [
|
return [
|
||||||
openInSafariAction(url: url),
|
openInSafariAction(url: url),
|
||||||
|
createAction(identifier: "share", title: "Share", systemImageName: "square.and.arrow.up", handler: { [weak self] (_) in
|
||||||
|
guard let self else { return }
|
||||||
|
self.navigationDelegate?.showMoreOptions(forURL: url, source: source)
|
||||||
|
}),
|
||||||
createAction(identifier: "postlink", title: "Post this Link", systemImageName: "square.and.pencil", handler: { [weak self] _ in
|
createAction(identifier: "postlink", title: "Post this Link", systemImageName: "square.and.pencil", handler: { [weak self] _ in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
let draft = self.mastodonController!.createDraft()
|
let draft = self.mastodonController!.createDraft()
|
||||||
|
|
|
@ -79,7 +79,7 @@ class SegmentedPageViewController<Page: SegmentedPageViewControllerPage>: UIView
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
view.backgroundColor = .systemBackground
|
view.backgroundColor = .appBackground
|
||||||
|
|
||||||
selectPage(initialPage, animated: false)
|
selectPage(initialPage, animated: false)
|
||||||
|
|
||||||
|
@ -94,6 +94,9 @@ class SegmentedPageViewController<Page: SegmentedPageViewControllerPage>: UIView
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func configureViewController(_ viewController: UIViewController) {
|
||||||
|
}
|
||||||
|
|
||||||
func selectPage(_ page: Page, animated: Bool) {
|
func selectPage(_ page: Page, animated: Bool) {
|
||||||
guard pages.contains(page) else {
|
guard pages.contains(page) else {
|
||||||
fatalError("invalid page \(page) that is not in SegmentedPageViewController.pages")
|
fatalError("invalid page \(page) that is not in SegmentedPageViewController.pages")
|
||||||
|
@ -116,6 +119,7 @@ class SegmentedPageViewController<Page: SegmentedPageViewControllerPage>: UIView
|
||||||
newController = existing
|
newController = existing
|
||||||
} else {
|
} else {
|
||||||
newController = pageProvider(page)
|
newController = pageProvider(page)
|
||||||
|
configureViewController(newController)
|
||||||
pageControllers[page] = newController
|
pageControllers[page] = newController
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -130,7 +134,16 @@ class SegmentedPageViewController<Page: SegmentedPageViewControllerPage>: UIView
|
||||||
animated != .none else {
|
animated != .none else {
|
||||||
currentViewController?.removeViewAndController()
|
currentViewController?.removeViewAndController()
|
||||||
newViewController.view.translatesAutoresizingMaskIntoConstraints = false
|
newViewController.view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
embedChild(newViewController)
|
// don't use embedChild here because it triggers an appearance transition, even though this vc hasn't appeared yet
|
||||||
|
addChild(newViewController)
|
||||||
|
view.addSubview(newViewController.view)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
newViewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||||
|
newViewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||||
|
newViewController.view.topAnchor.constraint(equalTo: view.topAnchor),
|
||||||
|
newViewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||||
|
])
|
||||||
|
newViewController.didMove(toParent: self)
|
||||||
self.currentViewController = newViewController
|
self.currentViewController = newViewController
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,8 +12,6 @@ import Pachyderm
|
||||||
|
|
||||||
protocol TuskerNavigationDelegate: UIViewController, ToastableViewController {
|
protocol TuskerNavigationDelegate: UIViewController, ToastableViewController {
|
||||||
var apiController: MastodonController! { get }
|
var apiController: MastodonController! { get }
|
||||||
|
|
||||||
func conversation(mainStatusID: String, state: CollapseState) -> ConversationViewController
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension TuskerNavigationDelegate {
|
extension TuskerNavigationDelegate {
|
||||||
|
@ -82,16 +80,12 @@ extension TuskerNavigationDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func conversation(mainStatusID: String, state: CollapseState) -> ConversationViewController {
|
|
||||||
return ConversationViewController(for: mainStatusID, state: state, mastodonController: apiController)
|
|
||||||
}
|
|
||||||
|
|
||||||
func selected(status statusID: String) {
|
func selected(status statusID: String) {
|
||||||
self.selected(status: statusID, state: .unknown)
|
self.selected(status: statusID, state: .unknown)
|
||||||
}
|
}
|
||||||
|
|
||||||
func selected(status statusID: String, state: CollapseState) {
|
func selected(status statusID: String, state: CollapseState) {
|
||||||
show(conversation(mainStatusID: statusID, state: state), sender: self)
|
show(ConversationViewController(for: statusID, state: state, mastodonController: apiController), sender: self)
|
||||||
}
|
}
|
||||||
|
|
||||||
func compose(editing draft: Draft, animated: Bool = true) {
|
func compose(editing draft: Draft, animated: Bool = true) {
|
||||||
|
@ -119,15 +113,11 @@ extension TuskerNavigationDelegate {
|
||||||
compose(editing: draft, animated: animated)
|
compose(editing: draft, animated: animated)
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadingLargeImage(url: URL, cache: ImageCache, description: String?, animatingFrom sourceView: UIImageView) -> LoadingLargeImageViewController {
|
func showLoadingLargeImage(url: URL, cache: ImageCache, description: String?, animatingFrom sourceView: UIImageView) {
|
||||||
let vc = LoadingLargeImageViewController(url: url, cache: cache, imageDescription: description)
|
let vc = LoadingLargeImageViewController(url: url, cache: cache, imageDescription: description)
|
||||||
vc.animationSourceView = sourceView
|
vc.animationSourceView = sourceView
|
||||||
vc.transitioningDelegate = self
|
vc.transitioningDelegate = self
|
||||||
return vc
|
present(vc, animated: true)
|
||||||
}
|
|
||||||
|
|
||||||
func showLoadingLargeImage(url: URL, cache: ImageCache, description: String?, animatingFrom sourceView: UIImageView) {
|
|
||||||
present(loadingLargeImage(url: url, cache: cache, description: description, animatingFrom: sourceView), animated: true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func gallery(attachments: [Attachment], sourceViews: [UIImageView?], startIndex: Int) -> GalleryViewController {
|
func gallery(attachments: [Attachment], sourceViews: [UIImageView?], startIndex: Int) -> GalleryViewController {
|
||||||
|
@ -183,16 +173,6 @@ extension TuskerNavigationDelegate {
|
||||||
present(vc, animated: true)
|
present(vc, animated: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func showFollowedByList(accountIDs: [String]) {
|
|
||||||
let vc = AccountListViewController(accountIDs: accountIDs, mastodonController: apiController)
|
|
||||||
vc.title = NSLocalizedString("Followed By", comment: "followed by accounts list title")
|
|
||||||
show(vc, sender: self)
|
|
||||||
}
|
|
||||||
|
|
||||||
func statusActionAccountList(action: StatusActionAccountListViewController.ActionType, statusID: String, statusState state: CollapseState, accountIDs: [String]?) -> StatusActionAccountListViewController {
|
|
||||||
return StatusActionAccountListViewController(actionType: action, statusID: statusID, statusState: state, accountIDs: accountIDs, mastodonController: apiController)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum PopoverSource {
|
enum PopoverSource {
|
||||||
|
|
|
@ -21,6 +21,16 @@ class AlbumTableViewCell: UITableViewCell {
|
||||||
thumbnailImageView.layer.cornerRadius = 0.05 * thumbnailImageView.bounds.width
|
thumbnailImageView.layer.cornerRadius = 0.05 * thumbnailImageView.bounds.width
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func updateConfiguration(using state: UICellConfigurationState) {
|
||||||
|
var config = UIBackgroundConfiguration.listGroupedCell().updated(for: state)
|
||||||
|
if state.isHighlighted || state.isSelected {
|
||||||
|
config.backgroundColor = .appSelectedCellBackground
|
||||||
|
} else {
|
||||||
|
config.backgroundColor = .appBackground
|
||||||
|
}
|
||||||
|
backgroundConfiguration = config
|
||||||
|
}
|
||||||
|
|
||||||
func updateUI(album: PHAssetCollection) {
|
func updateUI(album: PHAssetCollection) {
|
||||||
albumTitleLabel.text = album.localizedTitle
|
albumTitleLabel.text = album.localizedTitle
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,7 +28,7 @@ class ConfirmLoadMoreCollectionViewCell: UICollectionViewCell {
|
||||||
override init(frame: CGRect) {
|
override init(frame: CGRect) {
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
|
|
||||||
backgroundColor = .systemGroupedBackground
|
backgroundColor = .appGroupedBackground
|
||||||
|
|
||||||
let label = UILabel()
|
let label = UILabel()
|
||||||
label.text = "Infinite scrolling is off. Do you want to keep going?"
|
label.text = "Infinite scrolling is off. Do you want to keep going?"
|
||||||
|
|
|
@ -324,7 +324,7 @@ extension ContentTextView: UIContextMenuInteractionDelegate {
|
||||||
// Create a dummy containerview for the snapshot view, since using a view with a CALayer mask and UIPreviewParameters(textLineRects:)
|
// Create a dummy containerview for the snapshot view, since using a view with a CALayer mask and UIPreviewParameters(textLineRects:)
|
||||||
// causes the mask to be ignored. See FB7832297
|
// causes the mask to be ignored. See FB7832297
|
||||||
let snapshotContainer = UIView(frame: snapshot.bounds)
|
let snapshotContainer = UIView(frame: snapshot.bounds)
|
||||||
snapshotContainer.backgroundColor = .systemBackground
|
snapshotContainer.backgroundColor = .appBackground
|
||||||
snapshotContainer.addSubview(snapshot)
|
snapshotContainer.addSubview(snapshot)
|
||||||
|
|
||||||
let preview = UITargetedPreview(view: snapshotContainer, parameters: parameters, target: target)
|
let preview = UITargetedPreview(view: snapshotContainer, parameters: parameters, target: target)
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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>
|
|
|
@ -18,8 +18,6 @@ class TrendingHashtagCollectionViewCell: UICollectionViewCell {
|
||||||
override init(frame: CGRect) {
|
override init(frame: CGRect) {
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
|
|
||||||
backgroundColor = .systemBackground
|
|
||||||
|
|
||||||
hashtagLabel.font = .preferredFont(forTextStyle: .title2)
|
hashtagLabel.font = .preferredFont(forTextStyle: .title2)
|
||||||
hashtagLabel.adjustsFontForContentSizeCategory = true
|
hashtagLabel.adjustsFontForContentSizeCategory = true
|
||||||
peopleTodayLabel.font = .preferredFont(forTextStyle: .caption1)
|
peopleTodayLabel.font = .preferredFont(forTextStyle: .caption1)
|
||||||
|
@ -60,6 +58,16 @@ class TrendingHashtagCollectionViewCell: UICollectionViewCell {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func updateConfiguration(using state: UICellConfigurationState) {
|
||||||
|
var config = UIBackgroundConfiguration.listGroupedCell().updated(for: state)
|
||||||
|
if state.isHighlighted || state.isSelected {
|
||||||
|
config.backgroundColor = .appSelectedCellBackground
|
||||||
|
} else {
|
||||||
|
config.backgroundColor = .appGroupedCellBackground
|
||||||
|
}
|
||||||
|
backgroundConfiguration = config
|
||||||
|
}
|
||||||
|
|
||||||
func updateUI(hashtag: Hashtag) {
|
func updateUI(hashtag: Hashtag) {
|
||||||
hashtagLabel.text = "#\(hashtag.name)"
|
hashtagLabel.text = "#\(hashtag.name)"
|
||||||
historyView.setHistory(hashtag.history)
|
historyView.setHistory(hashtag.history)
|
||||||
|
|
|
@ -39,6 +39,16 @@ class InstanceTableViewCell: UITableViewCell {
|
||||||
descriptionTextView.adjustsFontForContentSizeCategory = true
|
descriptionTextView.adjustsFontForContentSizeCategory = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func updateConfiguration(using state: UICellConfigurationState) {
|
||||||
|
var config = UIBackgroundConfiguration.listGroupedCell()
|
||||||
|
if state.isHighlighted || state.isSelected {
|
||||||
|
config.backgroundColor = .appSelectedCellBackground
|
||||||
|
} else {
|
||||||
|
config.backgroundColor = .appGroupedCellBackground
|
||||||
|
}
|
||||||
|
backgroundConfiguration = config
|
||||||
|
}
|
||||||
|
|
||||||
func updateUI(instance: InstanceSelector.Instance) {
|
func updateUI(instance: InstanceSelector.Instance) {
|
||||||
self.selectorInstance = instance
|
self.selectorInstance = instance
|
||||||
self.instance = nil
|
self.instance = nil
|
||||||
|
|
|
@ -14,6 +14,8 @@ class LoadingTableViewCell: UITableViewCell {
|
||||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||||
|
|
||||||
|
backgroundColor = .appBackground
|
||||||
|
|
||||||
indicator.translatesAutoresizingMaskIntoConstraints = false
|
indicator.translatesAutoresizingMaskIntoConstraints = false
|
||||||
contentView.addSubview(indicator)
|
contentView.addSubview(indicator)
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
|
|
|
@ -48,6 +48,16 @@ class ActionNotificationGroupTableViewCell: UITableViewCell {
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(updateUIForPreferences), name: .preferencesChanged, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(updateUIForPreferences), name: .preferencesChanged, object: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func updateConfiguration(using state: UICellConfigurationState) {
|
||||||
|
var config = UIBackgroundConfiguration.listPlainCell().updated(for: state)
|
||||||
|
if state.isHighlighted || state.isSelected {
|
||||||
|
config.backgroundColor = .appSelectedCellBackground
|
||||||
|
} else {
|
||||||
|
config.backgroundColor = .appBackground
|
||||||
|
}
|
||||||
|
backgroundConfiguration = config
|
||||||
|
}
|
||||||
|
|
||||||
@objc func updateUIForPreferences() {
|
@objc func updateUIForPreferences() {
|
||||||
for case let imageView as UIImageView in actionAvatarStackView.arrangedSubviews {
|
for case let imageView as UIImageView in actionAvatarStackView.arrangedSubviews {
|
||||||
imageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: imageView)
|
imageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: imageView)
|
||||||
|
@ -263,7 +273,7 @@ extension ActionNotificationGroupTableViewCell: SelectableTableViewCell {
|
||||||
default:
|
default:
|
||||||
fatalError()
|
fatalError()
|
||||||
}
|
}
|
||||||
let vc = delegate.statusActionAccountList(action: action, statusID: statusID, statusState: .unknown, accountIDs: accountIDs)
|
let vc = StatusActionAccountListViewController(actionType: action, statusID: statusID, statusState: .unknown, accountIDs: accountIDs, mastodonController: mastodonController)
|
||||||
vc.showInacurateCountWarning = false
|
vc.showInacurateCountWarning = false
|
||||||
delegate.show(vc)
|
delegate.show(vc)
|
||||||
}
|
}
|
||||||
|
@ -272,9 +282,6 @@ extension ActionNotificationGroupTableViewCell: SelectableTableViewCell {
|
||||||
extension ActionNotificationGroupTableViewCell: MenuPreviewProvider {
|
extension ActionNotificationGroupTableViewCell: MenuPreviewProvider {
|
||||||
func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? {
|
func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? {
|
||||||
return (content: {
|
return (content: {
|
||||||
guard let delegate = self.delegate else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
let notifications = self.group.notifications
|
let notifications = self.group.notifications
|
||||||
let accountIDs = notifications.map { $0.account.id }
|
let accountIDs = notifications.map { $0.account.id }
|
||||||
let action: StatusActionAccountListViewController.ActionType
|
let action: StatusActionAccountListViewController.ActionType
|
||||||
|
@ -286,7 +293,7 @@ extension ActionNotificationGroupTableViewCell: MenuPreviewProvider {
|
||||||
default:
|
default:
|
||||||
fatalError()
|
fatalError()
|
||||||
}
|
}
|
||||||
let vc = delegate.statusActionAccountList(action: action, statusID: self.statusID, statusState: .unknown, accountIDs: accountIDs)
|
let vc = StatusActionAccountListViewController(actionType: action, statusID: self.statusID, statusState: .unknown, accountIDs: accountIDs, mastodonController: self.mastodonController)
|
||||||
vc.showInacurateCountWarning = false
|
vc.showInacurateCountWarning = false
|
||||||
return vc
|
return vc
|
||||||
}, actions: {
|
}, actions: {
|
||||||
|
|
|
@ -43,6 +43,16 @@ class FollowNotificationGroupTableViewCell: UITableViewCell {
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(updateUIForPreferences), name: .preferencesChanged, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(updateUIForPreferences), name: .preferencesChanged, object: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func updateConfiguration(using state: UICellConfigurationState) {
|
||||||
|
var config = UIBackgroundConfiguration.listPlainCell().updated(for: state)
|
||||||
|
if state.isHighlighted || state.isSelected {
|
||||||
|
config.backgroundColor = .appSelectedCellBackground
|
||||||
|
} else {
|
||||||
|
config.backgroundColor = .appBackground
|
||||||
|
}
|
||||||
|
backgroundConfiguration = config
|
||||||
|
}
|
||||||
|
|
||||||
@objc func updateUIForPreferences() {
|
@objc func updateUIForPreferences() {
|
||||||
for case let imageView as UIImageView in avatarStackView.arrangedSubviews {
|
for case let imageView as UIImageView in avatarStackView.arrangedSubviews {
|
||||||
imageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: imageView)
|
imageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: imageView)
|
||||||
|
@ -213,7 +223,9 @@ extension FollowNotificationGroupTableViewCell: SelectableTableViewCell {
|
||||||
case 1:
|
case 1:
|
||||||
delegate?.selected(account: accountIDs.first!)
|
delegate?.selected(account: accountIDs.first!)
|
||||||
default:
|
default:
|
||||||
delegate?.showFollowedByList(accountIDs: accountIDs)
|
let vc = AccountListViewController(accountIDs: accountIDs, mastodonController: mastodonController)
|
||||||
|
vc.title = NSLocalizedString("Followed By", comment: "followed by accounts list title")
|
||||||
|
delegate?.show(vc)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,6 +49,16 @@ class FollowRequestNotificationTableViewCell: UITableViewCell {
|
||||||
updateUIForPreferences()
|
updateUIForPreferences()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func updateConfiguration(using state: UICellConfigurationState) {
|
||||||
|
var config = UIBackgroundConfiguration.listPlainCell().updated(for: state)
|
||||||
|
if state.isHighlighted || state.isSelected {
|
||||||
|
config.backgroundColor = .appSelectedCellBackground
|
||||||
|
} else {
|
||||||
|
config.backgroundColor = .appBackground
|
||||||
|
}
|
||||||
|
backgroundConfiguration = config
|
||||||
|
}
|
||||||
|
|
||||||
@objc func updateUIForPreferences() {
|
@objc func updateUIForPreferences() {
|
||||||
avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * 30
|
avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * 30
|
||||||
|
|
||||||
|
|
|
@ -38,6 +38,16 @@ class PollFinishedTableViewCell: UITableViewCell {
|
||||||
displayNameLabel.adjustsFontForContentSizeCategory = true
|
displayNameLabel.adjustsFontForContentSizeCategory = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func updateConfiguration(using state: UICellConfigurationState) {
|
||||||
|
var config = UIBackgroundConfiguration.listPlainCell().updated(for: state)
|
||||||
|
if state.isHighlighted || state.isSelected {
|
||||||
|
config.backgroundColor = .appSelectedCellBackground
|
||||||
|
} else {
|
||||||
|
config.backgroundColor = .appBackground
|
||||||
|
}
|
||||||
|
backgroundConfiguration = config
|
||||||
|
}
|
||||||
|
|
||||||
func updateUI(notification: Pachyderm.Notification) {
|
func updateUI(notification: Pachyderm.Notification) {
|
||||||
guard let statusID = notification.status?.id,
|
guard let statusID = notification.status?.id,
|
||||||
let status = delegate?.apiController.persistentContainer.status(for: statusID),
|
let status = delegate?.apiController.persistentContainer.status(for: statusID),
|
||||||
|
@ -120,8 +130,7 @@ extension PollFinishedTableViewCell: SelectableTableViewCell {
|
||||||
let status = notification?.status else {
|
let status = notification?.status else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let vc = delegate.conversation(mainStatusID: status.id, state: .unknown)
|
delegate.selected(status: status.id)
|
||||||
delegate.show(vc)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -133,7 +142,7 @@ extension PollFinishedTableViewCell: MenuPreviewProvider {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return (content: {
|
return (content: {
|
||||||
delegate.conversation(mainStatusID: statusID, state: .unknown)
|
ConversationViewController(for: statusID, state: .unknown, mastodonController: delegate.apiController)
|
||||||
}, actions: {
|
}, actions: {
|
||||||
delegate.actionsForStatus(status, source: .view(self))
|
delegate.actionsForStatus(status, source: .view(self))
|
||||||
})
|
})
|
||||||
|
|
|
@ -35,6 +35,16 @@ class StatusUpdatedNotificationTableViewCell: UITableViewCell {
|
||||||
displayNameLabel.adjustsFontForContentSizeCategory = true
|
displayNameLabel.adjustsFontForContentSizeCategory = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func updateConfiguration(using state: UICellConfigurationState) {
|
||||||
|
var config = UIBackgroundConfiguration.listPlainCell().updated(for: state)
|
||||||
|
if state.isHighlighted || state.isSelected {
|
||||||
|
config.backgroundColor = .appSelectedCellBackground
|
||||||
|
} else {
|
||||||
|
config.backgroundColor = .appBackground
|
||||||
|
}
|
||||||
|
backgroundConfiguration = config
|
||||||
|
}
|
||||||
|
|
||||||
func updateUI(notification: Pachyderm.Notification) {
|
func updateUI(notification: Pachyderm.Notification) {
|
||||||
guard notification.kind == .update,
|
guard notification.kind == .update,
|
||||||
let status = notification.status else {
|
let status = notification.status else {
|
||||||
|
@ -109,8 +119,7 @@ extension StatusUpdatedNotificationTableViewCell: SelectableTableViewCell {
|
||||||
let status = notification?.status else {
|
let status = notification?.status else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let vc = delegate.conversation(mainStatusID: status.id, state: .unknown)
|
delegate.selected(status: status.id)
|
||||||
delegate.show(vc)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -122,7 +131,7 @@ extension StatusUpdatedNotificationTableViewCell: MenuPreviewProvider {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return (content: {
|
return (content: {
|
||||||
delegate.conversation(mainStatusID: statusID, state: .unknown)
|
ConversationViewController(for: statusID, state: .unknown, mastodonController: delegate.apiController)
|
||||||
}, actions: {
|
}, actions: {
|
||||||
delegate.actionsForStatus(status, source: .view(self))
|
delegate.actionsForStatus(status, source: .view(self))
|
||||||
})
|
})
|
||||||
|
|
|
@ -14,6 +14,8 @@ class ProfileFieldsView: UIView {
|
||||||
|
|
||||||
weak var delegate: ProfileHeaderViewDelegate?
|
weak var delegate: ProfileHeaderViewDelegate?
|
||||||
|
|
||||||
|
private var fields = [Account.Field]()
|
||||||
|
|
||||||
private let stack = UIStackView()
|
private let stack = UIStackView()
|
||||||
private var fieldViews: [(EmojiLabel, ProfileFieldValueView)] = []
|
private var fieldViews: [(EmojiLabel, ProfileFieldValueView)] = []
|
||||||
private var fieldConstraints: [NSLayoutConstraint] = []
|
private var fieldConstraints: [NSLayoutConstraint] = []
|
||||||
|
@ -62,9 +64,11 @@ class ProfileFieldsView: UIView {
|
||||||
|
|
||||||
func updateUI(account: AccountMO) {
|
func updateUI(account: AccountMO) {
|
||||||
isHidden = account.fields.isEmpty
|
isHidden = account.fields.isEmpty
|
||||||
guard !account.fields.isEmpty else {
|
guard !account.fields.isEmpty,
|
||||||
|
fields != account.fields else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
fields = account.fields
|
||||||
|
|
||||||
for (name, value) in fieldViews {
|
for (name, value) in fieldViews {
|
||||||
name.removeFromSuperview()
|
name.removeFromSuperview()
|
||||||
|
@ -183,6 +187,7 @@ private class ProfileFieldValueView: UIView {
|
||||||
super.init(frame: .zero)
|
super.init(frame: .zero)
|
||||||
|
|
||||||
textView.isSelectable = false
|
textView.isSelectable = false
|
||||||
|
textView.backgroundColor = .clear
|
||||||
textView.defaultFont = .preferredFont(forTextStyle: .body)
|
textView.defaultFont = .preferredFont(forTextStyle: .body)
|
||||||
textView.monospaceFont = UIFontMetrics.default.scaledFont(for: .monospacedSystemFont(ofSize: 17, weight: .regular))
|
textView.monospaceFont = UIFontMetrics.default.scaledFont(for: .monospacedSystemFont(ofSize: 17, weight: .regular))
|
||||||
textView.adjustsFontForContentSizeCategory = true
|
textView.adjustsFontForContentSizeCategory = true
|
||||||
|
@ -221,7 +226,6 @@ private class ProfileFieldValueView: UIView {
|
||||||
textView.topAnchor.constraint(equalTo: topAnchor),
|
textView.topAnchor.constraint(equalTo: topAnchor),
|
||||||
textView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
textView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||||
])
|
])
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
|
|
|
@ -61,6 +61,9 @@ class ProfileHeaderView: UIView {
|
||||||
override func awakeFromNib() {
|
override func awakeFromNib() {
|
||||||
super.awakeFromNib()
|
super.awakeFromNib()
|
||||||
|
|
||||||
|
backgroundColor = .appBackground
|
||||||
|
|
||||||
|
avatarContainerView.backgroundColor = .appBackground
|
||||||
avatarContainerView.layer.masksToBounds = true
|
avatarContainerView.layer.masksToBounds = true
|
||||||
avatarImageView.layer.masksToBounds = true
|
avatarImageView.layer.masksToBounds = true
|
||||||
avatarImageView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(avatarPressed)))
|
avatarImageView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(avatarPressed)))
|
||||||
|
|
|
@ -79,7 +79,6 @@
|
||||||
</textView>
|
</textView>
|
||||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="vKC-m1-Sbs" customClass="ProfileFieldsView" customModule="Tusker" customModuleProvider="target">
|
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="vKC-m1-Sbs" customClass="ProfileFieldsView" customModule="Tusker" customModuleProvider="target">
|
||||||
<rect key="frame" x="0.0" y="263.5" width="398" height="128"/>
|
<rect key="frame" x="0.0" y="263.5" width="398" height="128"/>
|
||||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
|
||||||
<constraints>
|
<constraints>
|
||||||
<constraint firstAttribute="height" constant="128" placeholder="YES" id="xbR-M6-H0I"/>
|
<constraint firstAttribute="height" constant="128" placeholder="YES" id="xbR-M6-H0I"/>
|
||||||
</constraints>
|
</constraints>
|
||||||
|
|
|
@ -317,6 +317,12 @@ class ConversationMainStatusCollectionViewCell: UICollectionViewListCell, Status
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func updateConfiguration(using state: UICellConfigurationState) {
|
||||||
|
var config = UIBackgroundConfiguration.listPlainCell().updated(for: state)
|
||||||
|
config.backgroundColor = .appBackground
|
||||||
|
backgroundConfiguration = config
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: Configure UI
|
// MARK: Configure UI
|
||||||
|
|
||||||
func updateUI(statusID: String, state: CollapseState) {
|
func updateUI(statusID: String, state: CollapseState) {
|
||||||
|
@ -410,7 +416,7 @@ class ConversationMainStatusCollectionViewCell: UICollectionViewListCell, Status
|
||||||
guard let delegate else {
|
guard let delegate else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let vc = delegate.statusActionAccountList(action: .favorite, statusID: statusID, statusState: statusState.copy(), accountIDs: nil)
|
let vc = StatusActionAccountListViewController(actionType: .favorite, statusID: statusID, statusState: statusState.copy(), accountIDs: nil, mastodonController: mastodonController)
|
||||||
// TODO: only show warning if the instance isn't the logged in one
|
// TODO: only show warning if the instance isn't the logged in one
|
||||||
vc.showInacurateCountWarning = true
|
vc.showInacurateCountWarning = true
|
||||||
delegate.show(vc)
|
delegate.show(vc)
|
||||||
|
@ -420,7 +426,7 @@ class ConversationMainStatusCollectionViewCell: UICollectionViewListCell, Status
|
||||||
guard let delegate else {
|
guard let delegate else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let vc = delegate.statusActionAccountList(action: .reblog, statusID: statusID, statusState: statusState.copy(), accountIDs: nil)
|
let vc = StatusActionAccountListViewController(actionType: .favorite, statusID: statusID, statusState: statusState.copy(), accountIDs: nil, mastodonController: mastodonController)
|
||||||
vc.showInacurateCountWarning = true
|
vc.showInacurateCountWarning = true
|
||||||
delegate.show(vc)
|
delegate.show(vc)
|
||||||
}
|
}
|
||||||
|
|
|
@ -351,6 +351,16 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func updateConfiguration(using state: UICellConfigurationState) {
|
||||||
|
var config = UIBackgroundConfiguration.listPlainCell().updated(for: state)
|
||||||
|
if state.isHighlighted || state.isSelected {
|
||||||
|
config.backgroundColor = .appSelectedCellBackground
|
||||||
|
} else {
|
||||||
|
config.backgroundColor = .appBackground
|
||||||
|
}
|
||||||
|
backgroundConfiguration = config
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: Accessibility
|
// MARK: Accessibility
|
||||||
|
|
||||||
override var isAccessibilityElement: Bool {
|
override var isAccessibilityElement: Bool {
|
||||||
|
|
|
@ -93,6 +93,16 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
|
||||||
updateActionsVisibility()
|
updateActionsVisibility()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func updateConfiguration(using state: UICellConfigurationState) {
|
||||||
|
var config = UIBackgroundConfiguration.listPlainCell().updated(for: state)
|
||||||
|
if state.isHighlighted || state.isSelected {
|
||||||
|
config.backgroundColor = .appSelectedCellBackground
|
||||||
|
} else {
|
||||||
|
config.backgroundColor = .appBackground
|
||||||
|
}
|
||||||
|
backgroundConfiguration = config
|
||||||
|
}
|
||||||
|
|
||||||
override func createObserversIfNecessary() {
|
override func createObserversIfNecessary() {
|
||||||
super.createObserversIfNecessary()
|
super.createObserversIfNecessary()
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -22,6 +22,8 @@ struct ToastConfiguration {
|
||||||
var edge: Edge = .automatic
|
var edge: Edge = .automatic
|
||||||
var dismissOnScroll = true
|
var dismissOnScroll = true
|
||||||
var dismissAutomaticallyAfter: TimeInterval? = nil
|
var dismissAutomaticallyAfter: TimeInterval? = nil
|
||||||
|
var onAppear: ((UIViewPropertyAnimator?) -> Void)?
|
||||||
|
var onDismiss: ((UIViewPropertyAnimator?) -> Void)?
|
||||||
|
|
||||||
init(title: String) {
|
init(title: String) {
|
||||||
self.title = title
|
self.title = title
|
||||||
|
|
|
@ -127,22 +127,30 @@ class ToastView: UIView {
|
||||||
func dismissToast(animated: Bool) {
|
func dismissToast(animated: Bool) {
|
||||||
guard animated else {
|
guard animated else {
|
||||||
removeFromSuperview()
|
removeFromSuperview()
|
||||||
|
configuration.onDismiss?(nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
UIView.animate(withDuration: 0.25, delay: 0, options: .curveEaseInOut) {
|
let animator = UIViewPropertyAnimator(duration: 0.25, curve: .easeInOut)
|
||||||
|
animator.addAnimations {
|
||||||
self.transform = CGAffineTransform(translationX: 0, y: self.offscreenTranslation)
|
self.transform = CGAffineTransform(translationX: 0, y: self.offscreenTranslation)
|
||||||
} completion: { (_) in
|
}
|
||||||
|
animator.addCompletion { _ in
|
||||||
self.removeFromSuperview()
|
self.removeFromSuperview()
|
||||||
}
|
}
|
||||||
|
configuration.onDismiss?(animator)
|
||||||
|
animator.startAnimation()
|
||||||
}
|
}
|
||||||
|
|
||||||
func animateAppearance() {
|
func animateAppearance() {
|
||||||
self.transform = CGAffineTransform(translationX: 0, y: offscreenTranslation)
|
self.transform = CGAffineTransform(translationX: 0, y: offscreenTranslation)
|
||||||
let duration = 0.5
|
let duration = 0.5
|
||||||
let velocity = 0.5
|
let velocity = CGVector(dx: 0, dy: 0.5)
|
||||||
UIView.animate(withDuration: duration, delay: 0, usingSpringWithDamping: 0.65, initialSpringVelocity: velocity, options: []) {
|
let animator = UIViewPropertyAnimator(duration: duration, timingParameters: UISpringTimingParameters(dampingRatio: 0.65, initialVelocity: velocity))
|
||||||
|
animator.addAnimations {
|
||||||
self.transform = .identity
|
self.transform = .identity
|
||||||
}
|
}
|
||||||
|
configuration.onAppear?(animator)
|
||||||
|
animator.startAnimation()
|
||||||
}
|
}
|
||||||
|
|
||||||
func setupDismissOnScroll(connectedTo scrollView: UIScrollView) {
|
func setupDismissOnScroll(connectedTo scrollView: UIScrollView) {
|
||||||
|
@ -184,11 +192,14 @@ class ToastView: UIView {
|
||||||
shrinkAnimator = nil
|
shrinkAnimator = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var panGestureDismissAnimator: UIViewPropertyAnimator?
|
||||||
|
|
||||||
@objc private func panRecognized(_ recognizer: UIPanGestureRecognizer) {
|
@objc private func panRecognized(_ recognizer: UIPanGestureRecognizer) {
|
||||||
let translation = recognizer.translation(in: self).y
|
let translation = recognizer.translation(in: self).y
|
||||||
|
|
||||||
let isDraggingAwayFromDismissalEdge = (configuration.edge == .top && translation > 0) || (configuration.edge == .bottom && translation < 0)
|
let isDraggingAwayFromDismissalEdge = (configuration.edge == .top && translation > 0) || (configuration.edge == .bottom && translation < 0)
|
||||||
|
|
||||||
|
|
||||||
switch recognizer.state {
|
switch recognizer.state {
|
||||||
case .began:
|
case .began:
|
||||||
recognizedGesture = true
|
recognizedGesture = true
|
||||||
|
@ -196,19 +207,25 @@ class ToastView: UIView {
|
||||||
UIView.animate(withDuration: 0.1, delay: 0, options: [.curveEaseInOut, .allowUserInteraction]) {
|
UIView.animate(withDuration: 0.1, delay: 0, options: [.curveEaseInOut, .allowUserInteraction]) {
|
||||||
self.transform = .identity
|
self.transform = .identity
|
||||||
}
|
}
|
||||||
break
|
panGestureDismissAnimator = UIViewPropertyAnimator(duration: 0.2, curve: .easeInOut)
|
||||||
|
configuration.onDismiss?(panGestureDismissAnimator!)
|
||||||
|
|
||||||
case .changed:
|
case .changed:
|
||||||
var distance: CGFloat
|
var distance: CGFloat
|
||||||
|
var alongsideAnimationProgress: CGFloat
|
||||||
if isDraggingAwayFromDismissalEdge {
|
if isDraggingAwayFromDismissalEdge {
|
||||||
distance = sqrt(abs(translation))
|
distance = sqrt(abs(translation))
|
||||||
if configuration.edge == .bottom {
|
if configuration.edge == .bottom {
|
||||||
distance *= -1
|
distance *= -1
|
||||||
}
|
}
|
||||||
|
alongsideAnimationProgress = 0
|
||||||
} else {
|
} else {
|
||||||
distance = translation
|
distance = translation
|
||||||
|
alongsideAnimationProgress = abs(translation) / (configuration.edgeSpacing + bounds.height)
|
||||||
}
|
}
|
||||||
|
|
||||||
transform = CGAffineTransform(translationX: 0, y: distance)
|
transform = CGAffineTransform(translationX: 0, y: distance)
|
||||||
|
panGestureDismissAnimator!.fractionComplete = alongsideAnimationProgress
|
||||||
|
|
||||||
case .ended, .cancelled:
|
case .ended, .cancelled:
|
||||||
let velocity = recognizer.velocity(in: self).y
|
let velocity = recognizer.velocity(in: self).y
|
||||||
|
@ -219,27 +236,31 @@ class ToastView: UIView {
|
||||||
|
|
||||||
let minDismissalVelocity: CGFloat = 250
|
let minDismissalVelocity: CGFloat = 250
|
||||||
let dismissDueToVelocity = configuration.edge == .bottom ? velocity > minDismissalDistance : velocity < -minDismissalVelocity
|
let dismissDueToVelocity = configuration.edge == .bottom ? velocity > minDismissalDistance : velocity < -minDismissalVelocity
|
||||||
|
|
||||||
if dismissDueToDistance || dismissDueToVelocity {
|
if dismissDueToDistance || dismissDueToVelocity {
|
||||||
|
|
||||||
if abs(translation) < abs(offscreenTranslation) {
|
if abs(translation) < abs(offscreenTranslation) {
|
||||||
let distanceLeft = offscreenTranslation - translation
|
panGestureDismissAnimator!.addAnimations {
|
||||||
let duration = 1 / TimeInterval(max(velocity, minDismissalVelocity) / distanceLeft)
|
|
||||||
|
|
||||||
UIView.animate(withDuration: duration, delay: 0, options: .allowUserInteraction) {
|
|
||||||
self.transform = CGAffineTransform(translationX: 0, y: self.offscreenTranslation)
|
self.transform = CGAffineTransform(translationX: 0, y: self.offscreenTranslation)
|
||||||
} completion: { (_) in
|
}
|
||||||
|
panGestureDismissAnimator!.addCompletion { _ in
|
||||||
self.removeFromSuperview()
|
self.removeFromSuperview()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
self.removeFromSuperview()
|
self.removeFromSuperview()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
panGestureDismissAnimator!.startAnimation()
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
let duration = 0.5
|
let duration = 0.5
|
||||||
let velocity = distance * duration
|
let velocity = distance * duration
|
||||||
UIView.animate(withDuration: duration, delay: 0, usingSpringWithDamping: 0.65, initialSpringVelocity: velocity, options: .allowUserInteraction) {
|
UIView.animate(withDuration: duration, delay: 0, usingSpringWithDamping: 0.65, initialSpringVelocity: velocity, options: .allowUserInteraction) {
|
||||||
self.transform = .identity
|
self.transform = .identity
|
||||||
}
|
}
|
||||||
|
|
||||||
|
panGestureDismissAnimator!.isReversed = true
|
||||||
|
panGestureDismissAnimator!.startAnimation()
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -77,6 +77,8 @@ extension ToastableViewController {
|
||||||
|
|
||||||
if animated {
|
if animated {
|
||||||
toast.animateAppearance()
|
toast.animateAppearance()
|
||||||
|
} else {
|
||||||
|
config.onAppear?(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.dismissOnScroll,
|
if config.dismissOnScroll,
|
||||||
|
|
|
@ -16,7 +16,7 @@ class TrendHistoryView: UIView {
|
||||||
private let curveRadius: CGFloat = 10
|
private let curveRadius: CGFloat = 10
|
||||||
|
|
||||||
/// The base background color used for the graph fill.
|
/// The base background color used for the graph fill.
|
||||||
var effectiveBackgroundColor = UIColor.systemBackground
|
var effectiveBackgroundColor = UIColor.appBackground
|
||||||
|
|
||||||
override func layoutSubviews() {
|
override func layoutSubviews() {
|
||||||
super.layoutSubviews()
|
super.layoutSubviews()
|
||||||
|
|
Loading…
Reference in New Issue