Compare commits
127 Commits
8fc915d6a0
...
ee630cf9df
Author | SHA1 | Date |
---|---|---|
Shadowfacts | ee630cf9df | |
Shadowfacts | c786c022b8 | |
Shadowfacts | 33649cc5c0 | |
Shadowfacts | 71a10f8514 | |
Shadowfacts | a864f4e344 | |
Shadowfacts | 007d5d6791 | |
Shadowfacts | f176a6c8eb | |
Shadowfacts | 104981f3d3 | |
Shadowfacts | 2ba6b64485 | |
Shadowfacts | 81ac3708a3 | |
Shadowfacts | 8e9e0fa346 | |
Shadowfacts | b6f32ca6be | |
Shadowfacts | e042754be1 | |
Shadowfacts | 38ac5858a9 | |
Shadowfacts | 0c0180264e | |
Shadowfacts | 3d9477f0c9 | |
Shadowfacts | 6f51f321f6 | |
Shadowfacts | ab17a688cf | |
Shadowfacts | 18bc6ce61e | |
Shadowfacts | 765b5e1a7c | |
Shadowfacts | a3e64703ab | |
Shadowfacts | d74be9d81d | |
Shadowfacts | 6ca5bb0c74 | |
Shadowfacts | 76550d8fb8 | |
Shadowfacts | daf3741c9a | |
Shadowfacts | b2977540e0 | |
Shadowfacts | bcc70e9f8c | |
Shadowfacts | 2252b6d09e | |
Shadowfacts | 8deb502140 | |
Shadowfacts | 2582907919 | |
Shadowfacts | 266868376d | |
Shadowfacts | 71fa3910a1 | |
Shadowfacts | 75f290ae8f | |
Shadowfacts | 073a1afbde | |
Shadowfacts | aaa031f212 | |
Shadowfacts | 762d298c06 | |
Shadowfacts | 2a892fa6ec | |
Shadowfacts | cb82826fcf | |
Shadowfacts | 6e5498430f | |
Shadowfacts | 57fb921573 | |
Shadowfacts | d1b5126288 | |
Shadowfacts | 9d2324b587 | |
Shadowfacts | 60921cb95f | |
Shadowfacts | 9e76879ce6 | |
Shadowfacts | 1992a4c60b | |
Shadowfacts | f833bc3a6f | |
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 | afed69e43e | |
Shadowfacts | b2096f22c3 | |
Shadowfacts | 14c456df22 | |
Shadowfacts | 3f34357692 | |
Shadowfacts | 429dcefa88 | |
Shadowfacts | d1a35620c9 | |
Shadowfacts | ce741d6e1f | |
Shadowfacts | 5a82851fe9 | |
Shadowfacts | 92ff900bc0 | |
Shadowfacts | 2a1deb8d7d | |
Shadowfacts | 38eea44a8b | |
Shadowfacts | 2d45fbbd91 | |
Shadowfacts | 32382c4783 | |
Shadowfacts | 521c46c0be | |
Shadowfacts | c114749519 | |
Shadowfacts | 825424cfba | |
Shadowfacts | 985eb24e88 | |
Shadowfacts | 7cadcf1e86 | |
Shadowfacts | a314521b96 | |
Shadowfacts | ab3bad0e16 | |
Shadowfacts | ec75906bc1 | |
Shadowfacts | 137a537f68 | |
Shadowfacts | 91123fd24a | |
Shadowfacts | 597dd56032 | |
Shadowfacts | 37847a2f9f | |
Shadowfacts | 471d3459a6 | |
Shadowfacts | 512eec09a8 | |
Shadowfacts | af8a9faaeb | |
Shadowfacts | 20c4c4bb2f | |
Shadowfacts | 76268e7a14 | |
Shadowfacts | 29596180a1 | |
Shadowfacts | ebfd8b3efd | |
Shadowfacts | 509acbde19 | |
Shadowfacts | 474064669d | |
Shadowfacts | 1940368c43 | |
Shadowfacts | 49c9c69b5a | |
Shadowfacts | ff29f2768b | |
Shadowfacts | 942df433b3 | |
Shadowfacts | 5e2b551045 | |
Shadowfacts | 2e64500c35 | |
Shadowfacts | 7b7c05ff68 | |
Shadowfacts | aec5c0b787 | |
Shadowfacts | d8901b38f5 | |
Shadowfacts | 9d7c876e3c | |
Shadowfacts | 455273f322 | |
Shadowfacts | 16347b2ad0 | |
Shadowfacts | 0e1cbce10d | |
Shadowfacts | 8bd6f53f01 | |
Shadowfacts | fe32356bce | |
Shadowfacts | 1f337613be | |
Shadowfacts | 3f4a62f5f9 | |
Shadowfacts | b506704716 | |
Shadowfacts | 6a3dcca9ee | |
Shadowfacts | edd1e55cbb | |
Shadowfacts | f1facea929 | |
Shadowfacts | d638ea054b | |
Shadowfacts | e11784904b | |
Shadowfacts | 9f1d3804d9 | |
Shadowfacts | 333295367a | |
Shadowfacts | e9d14c6cbf |
|
@ -0,0 +1,47 @@
|
||||||
|
## 2023.4
|
||||||
|
Features/Improvements:
|
||||||
|
- Add preference for non-pure-black dark mode
|
||||||
|
- Add Jump to Present button to timelines on the home tab
|
||||||
|
- Consolidate Trends into a single screen
|
||||||
|
- Allow pinning instance public timelines to the Home tab
|
||||||
|
- Add GIF/ALT badges to attachments (and preference to hide them)
|
||||||
|
- Add action to show hide/show reblogs from specific accounts
|
||||||
|
- Add preference to hide link preview cards
|
||||||
|
- Hide placeholder image in link preview card for previews without images
|
||||||
|
- Truncate links in posts
|
||||||
|
- Move Drafts button in Compose screen to nav bar to reduce accidental presses
|
||||||
|
- Load more posts/notifications on each page
|
||||||
|
- Update Bookmarks screen when posts are bookmarked/unbookmarked
|
||||||
|
- Add infinite scrolling to Bookmarks screen
|
||||||
|
- Add Favorites screen to the Explore tab
|
||||||
|
- Make attachment description text selectable in gallery
|
||||||
|
- Add long press to copy username on profile screens
|
||||||
|
- Optimize conversation loading
|
||||||
|
- Apply server-configured poll limits in Compose screen
|
||||||
|
- Add infinite scrolling to trending links/hashtags/posts
|
||||||
|
- Add state restoration for more screens
|
||||||
|
- Persist state when switching between accounts
|
||||||
|
- Add Handoff support for various screens
|
||||||
|
- Add preference to sync timeline position using Mastodon API, rather than iCloud
|
||||||
|
- Show percentage of voters for multi-choice polls, rather than percentage of votes
|
||||||
|
- Display message on remote profiles with no posts
|
||||||
|
- Indicate moved profiles
|
||||||
|
- Make Load More button on timelines more prominent
|
||||||
|
- VoiceOver: Make fast account switcher accessible
|
||||||
|
- VoiceOver: Improve labels for notifications
|
||||||
|
- VoiceOver: Fix custom emoji picker not having labels
|
||||||
|
|
||||||
|
Bugfixes:
|
||||||
|
- Workaround for not being able to sign in to certain instances
|
||||||
|
- Fix timeline position sync not working in certain circumstances
|
||||||
|
- Fix local-only posts not being decodable when logged in to Akkoma instances
|
||||||
|
- Fix Trends sometimes appearing in Explore/sidebar on non-Mastodon instances
|
||||||
|
- Fix favoriters/rebloggers list not resizing on screen rotation
|
||||||
|
- Fix crash when tapping My Profile tab immediately after app launch
|
||||||
|
- Handle authentication required errors on instance public timelines
|
||||||
|
- Fix follow request accept/reject buttons not matching accent color preference
|
||||||
|
- Fix tapping reblog count in conversation main status showing favorites list
|
||||||
|
- Fix crash when certain tags are present in post HTML
|
||||||
|
- Fix crash when opening Report screen in certain circumstances
|
||||||
|
- iPadOS: Fix crash when resizing window while on the Explore screen
|
||||||
|
- iOS 15: Fix accent colors not being displayed in Preferences
|
92
CHANGELOG.md
92
CHANGELOG.md
|
@ -1,5 +1,97 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2023.4 (75)
|
||||||
|
This build contains tweaks to automatic error reporting for the timeline marker API. The previous build's changelog is included below.
|
||||||
|
|
||||||
|
## 2023.4 (74)
|
||||||
|
Features/Improvements:
|
||||||
|
- Add state restoration for more screens
|
||||||
|
- Persist state when switching between accounts
|
||||||
|
- Add handoff for various screens
|
||||||
|
- Add preference to hide GIF/ALT badges on attachments
|
||||||
|
- Add preference to use Mastodon timeline marker API for syncing Home timeline position
|
||||||
|
- Show percentage of voters for multi-choice poll results, rather than percentage of votes
|
||||||
|
- Change search results view controller to dismiss keyboard on scroll
|
||||||
|
- Only show inaccurate favorite/reblog count warning for posts from remote instances
|
||||||
|
- Show message on remote profiles with no statuses
|
||||||
|
- Add banner to profiles that have moved
|
||||||
|
- Hide placeholder image for link cards without images
|
||||||
|
- Don't check for present statuses when refreshing timeline
|
||||||
|
- Make timeline Load More button more prominent
|
||||||
|
- iOS 16.4: Use iOS-provided link previews in Share Sheet
|
||||||
|
|
||||||
|
Bugfixes:
|
||||||
|
- Fix tapping reblog count in conversation main status showing favorites list
|
||||||
|
- Fix status favorite/reblog list not adjusting to non-pure-black dark mode
|
||||||
|
- Fix non-pure-black dark mode not applying to auxiliary windows
|
||||||
|
- Fix poll option tracking gesture unselecting options when touch location moves between options
|
||||||
|
- Fix crash when tapping conversation "More Replies" cell
|
||||||
|
- Fix crash when script/style tags are present in post HTML
|
||||||
|
- Fix crash when opening Report screen in certain circumstances
|
||||||
|
|
||||||
|
## 2023.4 (73)
|
||||||
|
Features/Improvements:
|
||||||
|
- Add preference for non-pure-black dark mode
|
||||||
|
- Add Jump to Present button to timelines
|
||||||
|
- Improve status collapse animation in search results screen
|
||||||
|
- Add more trending links/hashtags/profiles buttons to Trends screen
|
||||||
|
- Add infinite scrolling to trending links/hashtags screens
|
||||||
|
- Add Share action to trending link context menu
|
||||||
|
|
||||||
|
Bugfixes:
|
||||||
|
- Fix icon in suggested profile popover not adjusting to dark mode
|
||||||
|
|
||||||
|
## 2023.4 (72)
|
||||||
|
Features/Improvements:
|
||||||
|
- Consolidate Trends into a single screen
|
||||||
|
- Make attachment description text selectable in gallery
|
||||||
|
- Add long press to copy usernames on profile screen
|
||||||
|
- Add Favorites screen to Explore tab
|
||||||
|
- Optimize conversation loading when opening a conversation that is already fully-loaded
|
||||||
|
- Apply Mastodon poll limits in Compose screen
|
||||||
|
- VoiceOver: Fast account switcher improvements (make the screen modal, select the first account upon opening the switcher, make each account a single item)
|
||||||
|
- VoiceOver: Improve labels for notifications
|
||||||
|
- VoiceOver: Fix custom emoji picker buttons not having labels
|
||||||
|
|
||||||
|
Bugfixes:
|
||||||
|
- Fix trends sometimes appearing in Explore/sidebar on non-Mastodon instances
|
||||||
|
- Fix status favorite/reblog accounts list not resizing on device rotation
|
||||||
|
- Fix bookmarks screen sometimes going haywire
|
||||||
|
- Fix trending statuses not being deselected upon navigating back
|
||||||
|
- Fix crash when tapping My Profile tab too early in app lifecycle
|
||||||
|
- Handle 401 errors on instance timelines properly
|
||||||
|
- Fix potential crash when showing context menu previews for status
|
||||||
|
- Fix follow request accept/reject buttons not matching accent color preference
|
||||||
|
- iPadOS: Fix crash when switching between sidebar and tab bar while on the Explore screen
|
||||||
|
- iOS 15: Fix accent colors not being disaplyed in Preferences
|
||||||
|
|
||||||
|
## 2023.4 (71)
|
||||||
|
Features/Improvements:
|
||||||
|
- Allow pinning instance public timelines to the Home tab
|
||||||
|
- Improve UI and retry mechanism when adding account
|
||||||
|
- Increase page size to 40 on a bunch of screens
|
||||||
|
- Update bookmarks screen when posts are bookmarked/unbookmarked
|
||||||
|
- Allow loading older and refreshing bookmarks screen
|
||||||
|
- Tweak follow count button color
|
||||||
|
|
||||||
|
Bugfixes:
|
||||||
|
- Fix timeline position sync not working in certain circumstances
|
||||||
|
- iPadOS: Fix flicker when opening favorite/reblog list in notificationss
|
||||||
|
|
||||||
|
## 2023.4 (70)
|
||||||
|
Features/Improvements:
|
||||||
|
- Add GIF/ALT badges to attachments
|
||||||
|
- Add menu action to hide/show reblogs from specific accounts
|
||||||
|
- Apply Mastodon's link truncation
|
||||||
|
- Add preference to hide link preview cards
|
||||||
|
- Tweak link preview card border color in dark mode
|
||||||
|
- Unify haptic feedback across the app
|
||||||
|
- Move Drafts button to the nav bar when the post doesn't have any content, to reduce accidental presses
|
||||||
|
|
||||||
|
Bugfixes:
|
||||||
|
- Fix status URLs with fragments not being resolved
|
||||||
|
- Workaround for local-only posts not being decodable when logged in to Akkoma instances
|
||||||
|
|
||||||
## 2023.3 (69)
|
## 2023.3 (69)
|
||||||
Features/Improvements:
|
Features/Improvements:
|
||||||
- Add Tip Jar under Preferences
|
- Add Tip Jar under Preferences
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
# Haptic Feedback
|
||||||
|
|
||||||
|
## Selection changed
|
||||||
|
`UISelectionFeedbackGenerator`
|
||||||
|
|
||||||
|
## Actions
|
||||||
|
On success, `UIImpactFeedbackGenerator` with the `.light` style. On error, `UINotificationFeedbackGenerator` with the `.error` type.
|
|
@ -62,7 +62,7 @@ public class DuckableContainerViewController: UIViewController, DuckableViewCont
|
||||||
guard case .idle = state else {
|
guard case .idle = state else {
|
||||||
if animated,
|
if animated,
|
||||||
case .ducked(_, placeholder: let placeholder) = state {
|
case .ducked(_, placeholder: let placeholder) = state {
|
||||||
UIImpactFeedbackGenerator(style: .soft).impactOccurred()
|
UIImpactFeedbackGenerator(style: .light).impactOccurred()
|
||||||
let origConstant = placeholder.topConstraint.constant
|
let origConstant = placeholder.topConstraint.constant
|
||||||
UIView.animateKeyframes(withDuration: 0.4, delay: 0) {
|
UIView.animateKeyframes(withDuration: 0.4, delay: 0) {
|
||||||
UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.5) {
|
UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.5) {
|
||||||
|
|
|
@ -155,6 +155,27 @@ public class Client {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func revokeAccessToken() async throws {
|
||||||
|
guard let accessToken else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let request = Request<Empty>(method: .post, path: "/oauth/revoke", body: ParametersBody([
|
||||||
|
"token" => accessToken,
|
||||||
|
"client_id" => clientID!,
|
||||||
|
"client_secret" => clientSecret!,
|
||||||
|
]))
|
||||||
|
return try await withCheckedThrowingContinuation({ continuation in
|
||||||
|
self.run(request) { response in
|
||||||
|
switch response {
|
||||||
|
case .failure(let error):
|
||||||
|
continuation.resume(throwing: error)
|
||||||
|
case .success(_, _):
|
||||||
|
continuation.resume()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
public func nodeInfo(completion: @escaping Callback<NodeInfo>) {
|
public func nodeInfo(completion: @escaping Callback<NodeInfo>) {
|
||||||
let wellKnown = Request<WellKnown>(method: .get, path: "/.well-known/nodeinfo")
|
let wellKnown = Request<WellKnown>(method: .get, path: "/.well-known/nodeinfo")
|
||||||
run(wellKnown) { result in
|
run(wellKnown) { result in
|
||||||
|
@ -178,8 +199,10 @@ public class Client {
|
||||||
return Request<Account>(method: .get, path: "/api/v1/accounts/verify_credentials")
|
return Request<Account>(method: .get, path: "/api/v1/accounts/verify_credentials")
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func getFavourites() -> Request<[Status]> {
|
public static func getFavourites(range: RequestRange = .default) -> Request<[Status]> {
|
||||||
return Request<[Status]>(method: .get, path: "/api/v1/favourites")
|
var request = Request<[Status]>(method: .get, path: "/api/v1/favourites")
|
||||||
|
request.range = range
|
||||||
|
return request
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func getRelationships(accounts: [String]? = nil) -> Request<[Relationship]> {
|
public static func getRelationships(accounts: [String]? = nil) -> Request<[Relationship]> {
|
||||||
|
@ -394,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)
|
||||||
}
|
}
|
||||||
|
@ -447,7 +484,7 @@ public class Client {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Client {
|
extension Client {
|
||||||
public struct Error: LocalizedError {
|
public struct Error: LocalizedError, Sendable {
|
||||||
public let requestMethod: Method
|
public let requestMethod: Method
|
||||||
public let requestEndpoint: Endpoint
|
public let requestEndpoint: Endpoint
|
||||||
public let type: ErrorType
|
public let type: ErrorType
|
||||||
|
@ -482,7 +519,7 @@ extension Client {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public enum ErrorType: LocalizedError {
|
public enum ErrorType: LocalizedError, Sendable {
|
||||||
case networkError(Swift.Error)
|
case networkError(Swift.Error)
|
||||||
case unexpectedStatus(Int)
|
case unexpectedStatus(Int)
|
||||||
case invalidRequest
|
case invalidRequest
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public final class Account: AccountProtocol, Decodable {
|
public final class Account: AccountProtocol, Decodable, Sendable {
|
||||||
public let id: String
|
public let id: String
|
||||||
public let username: String
|
public let username: String
|
||||||
public let acct: String
|
public let acct: String
|
||||||
|
@ -25,7 +25,7 @@ public final class Account: AccountProtocol, Decodable {
|
||||||
public let avatarStatic: URL?
|
public let avatarStatic: URL?
|
||||||
public let header: URL?
|
public let header: URL?
|
||||||
public let headerStatic: URL?
|
public let headerStatic: URL?
|
||||||
public private(set) var emojis: [Emoji]
|
public let emojis: [Emoji]
|
||||||
public let moved: Bool?
|
public let moved: Bool?
|
||||||
public let movedTo: Account?
|
public let movedTo: Account?
|
||||||
public let fields: [Field]
|
public let fields: [Field]
|
||||||
|
@ -109,6 +109,12 @@ public final class Account: AccountProtocol, Decodable {
|
||||||
return Request<Relationship>(method: .post, path: "/api/v1/accounts/\(accountID)/follow")
|
return Request<Relationship>(method: .post, path: "/api/v1/accounts/\(accountID)/follow")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static func setShowReblogs(_ accountID: String, showReblogs: Bool) -> Request<Relationship> {
|
||||||
|
return Request(method: .post, path: "/api/v1/accounts/\(accountID)/follow", body: ParametersBody([
|
||||||
|
"reblogs" => showReblogs
|
||||||
|
]))
|
||||||
|
}
|
||||||
|
|
||||||
public static func unfollow(_ accountID: String) -> Request<Relationship> {
|
public static func unfollow(_ accountID: String) -> Request<Relationship> {
|
||||||
return Request<Relationship>(method: .post, path: "/api/v1/accounts/\(accountID)/unfollow")
|
return Request<Relationship>(method: .post, path: "/api/v1/accounts/\(accountID)/unfollow")
|
||||||
}
|
}
|
||||||
|
@ -165,7 +171,7 @@ extension Account: CustomDebugStringConvertible {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Account {
|
extension Account {
|
||||||
public struct Field: Codable {
|
public struct Field: Codable, Equatable, Sendable {
|
||||||
public let name: String
|
public let name: String
|
||||||
public let value: String
|
public let value: String
|
||||||
public let verifiedAt: Date?
|
public let verifiedAt: Date?
|
||||||
|
|
|
@ -8,11 +8,11 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public class Application: Decodable {
|
public struct Application: Decodable, Sendable {
|
||||||
public let name: String
|
public let name: String
|
||||||
public let website: URL?
|
public let website: URL?
|
||||||
|
|
||||||
public required init(from decoder: Decoder) throws {
|
public init(from decoder: Decoder) throws {
|
||||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
|
||||||
self.name = try container.decode(String.self, forKey: .name)
|
self.name = try container.decode(String.self, forKey: .name)
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public class Attachment: Codable {
|
public struct Attachment: Codable, Sendable {
|
||||||
public let id: String
|
public let id: String
|
||||||
public let kind: Kind
|
public let kind: Kind
|
||||||
public let url: URL
|
public let url: URL
|
||||||
|
@ -25,7 +25,7 @@ public class Attachment: Codable {
|
||||||
], nil))
|
], nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
required public init(from decoder: Decoder) throws {
|
public init(from decoder: Decoder) throws {
|
||||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
self.id = try container.decode(String.self, forKey: .id)
|
self.id = try container.decode(String.self, forKey: .id)
|
||||||
self.kind = try container.decode(Kind.self, forKey: .kind)
|
self.kind = try container.decode(Kind.self, forKey: .kind)
|
||||||
|
@ -50,7 +50,7 @@ public class Attachment: Codable {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Attachment {
|
extension Attachment {
|
||||||
public enum Kind: String, Codable {
|
public enum Kind: String, Codable, Sendable {
|
||||||
case image
|
case image
|
||||||
case video
|
case video
|
||||||
case gifv
|
case gifv
|
||||||
|
@ -77,7 +77,7 @@ extension Attachment {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Attachment {
|
extension Attachment {
|
||||||
public struct Metadata: Codable {
|
public struct Metadata: Codable, Sendable {
|
||||||
public let length: String?
|
public let length: String?
|
||||||
public let duration: Float?
|
public let duration: Float?
|
||||||
public let audioEncoding: String?
|
public let audioEncoding: String?
|
||||||
|
@ -108,7 +108,7 @@ extension Attachment {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct ImageMetadata: Codable {
|
public struct ImageMetadata: Codable, Sendable {
|
||||||
public let width: Int?
|
public let width: Int?
|
||||||
public let height: Int?
|
public let height: Int?
|
||||||
public let size: String?
|
public let size: String?
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
import WebURL
|
import WebURL
|
||||||
|
|
||||||
public class Card: Codable {
|
public struct Card: Codable, Sendable {
|
||||||
public let url: WebURL
|
public let url: WebURL
|
||||||
public let title: String
|
public let title: String
|
||||||
public let description: String
|
public let description: String
|
||||||
|
@ -26,7 +26,7 @@ public class Card: Codable {
|
||||||
/// Only present when returned from the trending links endpoint
|
/// Only present when returned from the trending links endpoint
|
||||||
public let history: [History]?
|
public let history: [History]?
|
||||||
|
|
||||||
public required init(from decoder: Decoder) throws {
|
public init(from decoder: Decoder) throws {
|
||||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
|
||||||
self.url = try container.decode(WebURL.self, forKey: .url)
|
self.url = try container.decode(WebURL.self, forKey: .url)
|
||||||
|
@ -75,7 +75,7 @@ public class Card: Codable {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Card {
|
extension Card {
|
||||||
public enum Kind: String, Codable {
|
public enum Kind: String, Codable, Sendable {
|
||||||
case link
|
case link
|
||||||
case photo
|
case photo
|
||||||
case video
|
case video
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public class ConversationContext: Decodable {
|
public struct ConversationContext: Decodable, Sendable {
|
||||||
public let ancestors: [Status]
|
public let ancestors: [Status]
|
||||||
public let descendants: [Status]
|
public let descendants: [Status]
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public enum DirectoryOrder: String, CaseIterable {
|
public enum DirectoryOrder: String, CaseIterable, Sendable {
|
||||||
case active
|
case active
|
||||||
case new
|
case new
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
import WebURL
|
import WebURL
|
||||||
|
|
||||||
public class Emoji: Codable {
|
public struct Emoji: Codable, Sendable {
|
||||||
public let shortcode: String
|
public let shortcode: String
|
||||||
// these shouldn't need to be WebURLs as they're not external resources,
|
// these shouldn't need to be WebURLs as they're not external resources,
|
||||||
// but some instances (pleroma?) has emoji urls that Foundation considers malformed so we use WebURL to be more lenient
|
// but some instances (pleroma?) has emoji urls that Foundation considers malformed so we use WebURL to be more lenient
|
||||||
|
@ -18,7 +18,7 @@ public class Emoji: Codable {
|
||||||
public let visibleInPicker: Bool
|
public let visibleInPicker: Bool
|
||||||
public let category: String?
|
public let category: String?
|
||||||
|
|
||||||
public required init(from decoder: Decoder) throws {
|
public init(from decoder: Decoder) throws {
|
||||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
|
||||||
self.shortcode = try container.decode(String.self, forKey: .shortcode)
|
self.shortcode = try container.decode(String.self, forKey: .shortcode)
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public struct FilterV1: Decodable {
|
public struct FilterV1: Decodable, Sendable {
|
||||||
public let id: String
|
public let id: String
|
||||||
public let phrase: String
|
public let phrase: String
|
||||||
private let context: [String]
|
private let context: [String]
|
||||||
|
@ -45,7 +45,7 @@ public struct FilterV1: Decodable {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension FilterV1 {
|
extension FilterV1 {
|
||||||
public enum Context: String, Decodable, CaseIterable {
|
public enum Context: String, Decodable, CaseIterable, Sendable {
|
||||||
case home
|
case home
|
||||||
case notifications
|
case notifications
|
||||||
case `public`
|
case `public`
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public struct FilterV2: Decodable {
|
public struct FilterV2: Decodable, Sendable {
|
||||||
public let id: String
|
public let id: String
|
||||||
public let title: String
|
public let title: String
|
||||||
public let context: [FilterV1.Context]
|
public let context: [FilterV1.Context]
|
||||||
|
@ -80,14 +80,14 @@ public struct FilterV2: Decodable {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension FilterV2 {
|
extension FilterV2 {
|
||||||
public enum Action: String, Decodable, Hashable, CaseIterable {
|
public enum Action: String, Decodable, Hashable, CaseIterable, Sendable {
|
||||||
case warn
|
case warn
|
||||||
case hide
|
case hide
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension FilterV2 {
|
extension FilterV2 {
|
||||||
public struct Keyword: Decodable {
|
public struct Keyword: Decodable, Sendable {
|
||||||
public let id: String
|
public let id: String
|
||||||
public let keyword: String
|
public let keyword: String
|
||||||
public let wholeWord: Bool
|
public let wholeWord: Bool
|
||||||
|
|
|
@ -10,7 +10,7 @@ import Foundation
|
||||||
import WebURL
|
import WebURL
|
||||||
import WebURLFoundationExtras
|
import WebURLFoundationExtras
|
||||||
|
|
||||||
public class Hashtag: Codable {
|
public struct Hashtag: Codable, Sendable {
|
||||||
public let name: String
|
public let name: String
|
||||||
public let url: WebURL
|
public let url: WebURL
|
||||||
/// Only present when returned from the trending hashtags endpoint
|
/// Only present when returned from the trending hashtags endpoint
|
||||||
|
@ -25,7 +25,7 @@ public class Hashtag: Codable {
|
||||||
self.following = nil
|
self.following = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
public required init(from decoder: Decoder) throws {
|
public init(from decoder: Decoder) throws {
|
||||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
self.name = try container.decode(String.self, forKey: .name)
|
self.name = try container.decode(String.self, forKey: .name)
|
||||||
// pixelfed (possibly others) don't fully escape special characters in the hashtag url
|
// pixelfed (possibly others) don't fully escape special characters in the hashtag url
|
||||||
|
|
|
@ -8,12 +8,12 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public class History: Codable {
|
public struct History: Codable, Sendable {
|
||||||
public let day: Date
|
public let day: Date
|
||||||
public let uses: Int
|
public let uses: Int
|
||||||
public let accounts: Int
|
public let accounts: Int
|
||||||
|
|
||||||
public required init(from decoder: Decoder) throws {
|
public init(from decoder: Decoder) throws {
|
||||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
|
||||||
if let day = try? container.decode(Date.self, forKey: .day) {
|
if let day = try? container.decode(Date.self, forKey: .day) {
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public class Instance: Decodable {
|
public struct Instance: Decodable, Sendable {
|
||||||
public let uri: String
|
public let uri: String
|
||||||
public let title: String
|
public let title: String
|
||||||
public let description: String
|
public let description: String
|
||||||
|
@ -37,7 +37,7 @@ public class Instance: Decodable {
|
||||||
}
|
}
|
||||||
|
|
||||||
// we need a custom decoder, because all API-compatible implementations don't return some data in the same format
|
// we need a custom decoder, because all API-compatible implementations don't return some data in the same format
|
||||||
public required init(from decoder: Decoder) throws {
|
public init(from decoder: Decoder) throws {
|
||||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
self.uri = try container.decode(String.self, forKey: .uri)
|
self.uri = try container.decode(String.self, forKey: .uri)
|
||||||
self.title = try container.decode(String.self, forKey: .title)
|
self.title = try container.decode(String.self, forKey: .title)
|
||||||
|
@ -93,7 +93,7 @@ public class Instance: Decodable {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Instance {
|
extension Instance {
|
||||||
public struct Stats: Decodable {
|
public struct Stats: Decodable, Sendable {
|
||||||
public let domainCount: Int?
|
public let domainCount: Int?
|
||||||
public let statusCount: Int?
|
public let statusCount: Int?
|
||||||
public let userCount: Int?
|
public let userCount: Int?
|
||||||
|
@ -107,7 +107,7 @@ extension Instance {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Instance {
|
extension Instance {
|
||||||
public struct Configuration: Decodable {
|
public struct Configuration: Decodable, Sendable {
|
||||||
public let statuses: StatusesConfiguration
|
public let statuses: StatusesConfiguration
|
||||||
public let mediaAttachments: MediaAttachmentsConfiguration
|
public let mediaAttachments: MediaAttachmentsConfiguration
|
||||||
/// Use Instance.pollsConfiguration to support older instance that don't have this nested
|
/// Use Instance.pollsConfiguration to support older instance that don't have this nested
|
||||||
|
@ -122,7 +122,7 @@ extension Instance {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Instance {
|
extension Instance {
|
||||||
public struct StatusesConfiguration: Decodable {
|
public struct StatusesConfiguration: Decodable, Sendable {
|
||||||
public let maxCharacters: Int
|
public let maxCharacters: Int
|
||||||
public let maxMediaAttachments: Int
|
public let maxMediaAttachments: Int
|
||||||
public let charactersReservedPerURL: Int
|
public let charactersReservedPerURL: Int
|
||||||
|
@ -136,7 +136,7 @@ extension Instance {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Instance {
|
extension Instance {
|
||||||
public struct MediaAttachmentsConfiguration: Decodable {
|
public struct MediaAttachmentsConfiguration: Decodable, Sendable {
|
||||||
public let supportedMIMETypes: [String]
|
public let supportedMIMETypes: [String]
|
||||||
public let imageSizeLimit: Int
|
public let imageSizeLimit: Int
|
||||||
public let imageMatrixLimit: Int
|
public let imageMatrixLimit: Int
|
||||||
|
@ -156,7 +156,7 @@ extension Instance {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Instance {
|
extension Instance {
|
||||||
public struct PollsConfiguration: Decodable {
|
public struct PollsConfiguration: Decodable, Sendable {
|
||||||
public let maxOptions: Int
|
public let maxOptions: Int
|
||||||
public let maxCharactersPerOption: Int
|
public let maxCharactersPerOption: Int
|
||||||
public let minExpiration: TimeInterval
|
public let minExpiration: TimeInterval
|
||||||
|
@ -172,7 +172,7 @@ extension Instance {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Instance {
|
extension Instance {
|
||||||
public struct Rule: Decodable, Identifiable {
|
public struct Rule: Decodable, Identifiable, Sendable {
|
||||||
public let id: String
|
public let id: String
|
||||||
public let text: String
|
public let text: String
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public class List: Decodable, Equatable, Hashable {
|
public struct List: ListProtocol, Decodable, Equatable, Hashable, Sendable {
|
||||||
public let id: String
|
public let id: String
|
||||||
public let title: String
|
public let title: String
|
||||||
|
|
||||||
|
@ -16,6 +16,11 @@ public class List: Decodable, Equatable, Hashable {
|
||||||
return .list(id: id)
|
return .list(id: id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public init(id: String, title: String) {
|
||||||
|
self.id = id
|
||||||
|
self.title = title
|
||||||
|
}
|
||||||
|
|
||||||
public static func ==(lhs: List, rhs: List) -> Bool {
|
public static func ==(lhs: List, rhs: List) -> Bool {
|
||||||
return lhs.id == rhs.id && lhs.title == rhs.title
|
return lhs.id == rhs.id && lhs.title == rhs.title
|
||||||
}
|
}
|
||||||
|
@ -25,28 +30,28 @@ public class List: Decodable, Equatable, Hashable {
|
||||||
hasher.combine(title)
|
hasher.combine(title)
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func getAccounts(_ list: List, range: RequestRange = .default) -> Request<[Account]> {
|
public static func getAccounts(_ listID: String, range: RequestRange = .default) -> Request<[Account]> {
|
||||||
var request = Request<[Account]>(method: .get, path: "/api/v1/lists/\(list.id)/accounts")
|
var request = Request<[Account]>(method: .get, path: "/api/v1/lists/\(listID)/accounts")
|
||||||
request.range = range
|
request.range = range
|
||||||
return request
|
return request
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func update(_ list: List, title: String) -> Request<List> {
|
public static func update(_ listID: String, title: String) -> Request<List> {
|
||||||
return Request<List>(method: .put, path: "/api/v1/lists/\(list.id)", body: ParametersBody(["title" => title]))
|
return Request<List>(method: .put, path: "/api/v1/lists/\(listID)", body: ParametersBody(["title" => title]))
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func delete(_ list: List) -> Request<Empty> {
|
public static func delete(_ listID: String) -> Request<Empty> {
|
||||||
return Request<Empty>(method: .delete, path: "/api/v1/lists/\(list.id)")
|
return Request<Empty>(method: .delete, path: "/api/v1/lists/\(listID)")
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func add(_ list: List, accounts accountIDs: [String]) -> Request<Empty> {
|
public static func add(_ listID: String, accounts accountIDs: [String]) -> Request<Empty> {
|
||||||
return Request<Empty>(method: .post, path: "/api/v1/lists/\(list.id)/accounts", body: ParametersBody(
|
return Request<Empty>(method: .post, path: "/api/v1/lists/\(listID)/accounts", body: ParametersBody(
|
||||||
"account_ids" => accountIDs
|
"account_ids" => accountIDs
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func remove(_ list: List, accounts accountIDs: [String]) -> Request<Empty> {
|
public static func remove(_ listID: String, accounts accountIDs: [String]) -> Request<Empty> {
|
||||||
return Request<Empty>(method: .delete, path: "/api/v1/lists/\(list.id)/accounts", body: ParametersBody(
|
return Request<Empty>(method: .delete, path: "/api/v1/lists/\(listID)/accounts", body: ParametersBody(
|
||||||
"account_ids" => accountIDs
|
"account_ids" => accountIDs
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public class LoginSettings: Decodable {
|
public struct LoginSettings: Decodable, Sendable {
|
||||||
public let accessToken: String
|
public let accessToken: String
|
||||||
private let scope: String?
|
private let scope: String?
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
import WebURL
|
import WebURL
|
||||||
|
|
||||||
public struct Mention: Codable {
|
public struct Mention: Codable, Sendable {
|
||||||
public let url: WebURL
|
public let url: WebURL
|
||||||
public let username: String
|
public let username: String
|
||||||
public let acct: String
|
public let acct: String
|
||||||
|
|
|
@ -8,11 +8,11 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public struct NodeInfo: Decodable {
|
public struct NodeInfo: Decodable, Sendable {
|
||||||
public let version: String
|
public let version: String
|
||||||
public let software: Software
|
public let software: Software
|
||||||
|
|
||||||
public struct Software: Decodable {
|
public struct Software: Decodable, Sendable {
|
||||||
public let name: String
|
public let name: String
|
||||||
public let version: String
|
public let version: String
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,14 +8,14 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public class Notification: Decodable {
|
public struct Notification: Decodable, Sendable {
|
||||||
public let id: String
|
public let id: String
|
||||||
public let kind: Kind
|
public let kind: Kind
|
||||||
public let createdAt: Date
|
public let createdAt: Date
|
||||||
public let account: Account
|
public let account: Account
|
||||||
public let status: Status?
|
public let status: Status?
|
||||||
|
|
||||||
public required init(from decoder: Decoder) throws {
|
public init(from decoder: Decoder) throws {
|
||||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
|
||||||
self.id = try container.decode(String.self, forKey: .id)
|
self.id = try container.decode(String.self, forKey: .id)
|
||||||
|
@ -45,7 +45,7 @@ public class Notification: Decodable {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Notification {
|
extension Notification {
|
||||||
public enum Kind: String, Decodable, CaseIterable {
|
public enum Kind: String, Decodable, CaseIterable, Sendable {
|
||||||
case mention
|
case mention
|
||||||
case reblog
|
case reblog
|
||||||
case favourite
|
case favourite
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public final class Poll: Codable {
|
public struct Poll: Codable, Sendable {
|
||||||
public let id: String
|
public let id: String
|
||||||
public let expiresAt: Date?
|
public let expiresAt: Date?
|
||||||
public let expired: Bool
|
public let expired: Bool
|
||||||
|
@ -43,7 +43,7 @@ public final class Poll: Codable {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Poll {
|
extension Poll {
|
||||||
public final class Option: Codable {
|
public struct Option: Codable, Sendable {
|
||||||
public let title: String
|
public let title: String
|
||||||
public let votesCount: Int?
|
public let votesCount: Int?
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
//
|
||||||
|
// ListProtocol.swift
|
||||||
|
// Pachyderm
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 2/25/23.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public protocol ListProtocol {
|
||||||
|
var id: String { get }
|
||||||
|
var title: String { get }
|
||||||
|
}
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public class PushSubscription: Decodable {
|
public struct PushSubscription: Decodable, Sendable {
|
||||||
public let id: String
|
public let id: String
|
||||||
public let endpoint: URL
|
public let endpoint: URL
|
||||||
public let serverKey: String
|
public let serverKey: String
|
||||||
|
|
|
@ -8,12 +8,12 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public class RegisteredApplication: Decodable {
|
public struct RegisteredApplication: Decodable, Sendable {
|
||||||
public let id: String
|
public let id: String
|
||||||
public let clientID: String
|
public let clientID: String
|
||||||
public let clientSecret: String
|
public let clientSecret: String
|
||||||
|
|
||||||
public required init(from decoder: Decoder) throws {
|
public init(from decoder: Decoder) throws {
|
||||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
|
||||||
// Pixelfed API returns id/client_id as numbers instead of strings
|
// Pixelfed API returns id/client_id as numbers instead of strings
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public class Relationship: Decodable {
|
public struct Relationship: Decodable, Sendable {
|
||||||
public let id: String
|
public let id: String
|
||||||
public let following: Bool
|
public let following: Bool
|
||||||
public let followedBy: Bool
|
public let followedBy: Bool
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public class Report: Decodable {
|
public struct Report: Decodable, Sendable {
|
||||||
public let id: String
|
public let id: String
|
||||||
public let actionTaken: Bool
|
public let actionTaken: Bool
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public enum Scope: String {
|
public enum Scope: String, Sendable {
|
||||||
case read
|
case read
|
||||||
case write
|
case write
|
||||||
case follow
|
case follow
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public enum SearchResultType: String {
|
public enum SearchResultType: String, Sendable {
|
||||||
case accounts
|
case accounts
|
||||||
case hashtags
|
case hashtags
|
||||||
case statuses
|
case statuses
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public class SearchResults: Decodable {
|
public struct SearchResults: Decodable, Sendable {
|
||||||
public let accounts: [Account]
|
public let accounts: [Account]
|
||||||
public let statuses: [Status]
|
public let statuses: [Status]
|
||||||
public let hashtags: [Hashtag]
|
public let hashtags: [Hashtag]
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
import WebURL
|
import WebURL
|
||||||
|
|
||||||
public final class Status: StatusProtocol, Decodable {
|
public final class Status: StatusProtocol, Decodable, Sendable {
|
||||||
public let id: String
|
public let id: String
|
||||||
public let uri: String
|
public let uri: String
|
||||||
public let url: WebURL?
|
public let url: WebURL?
|
||||||
|
@ -44,6 +44,47 @@ public final class Status: StatusProtocol, Decodable {
|
||||||
|
|
||||||
public var applicationName: String? { application?.name }
|
public var applicationName: String? { application?.name }
|
||||||
|
|
||||||
|
public init(from decoder: Decoder) throws {
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
self.id = try container.decode(String.self, forKey: .id)
|
||||||
|
self.uri = try container.decode(String.self, forKey: .uri)
|
||||||
|
self.url = try container.decodeIfPresent(WebURL.self, forKey: .url)
|
||||||
|
self.account = try container.decode(Account.self, forKey: .account)
|
||||||
|
self.inReplyToID = try container.decodeIfPresent(String.self, forKey: .inReplyToID)
|
||||||
|
self.inReplyToAccountID = try container.decodeIfPresent(String.self, forKey: .inReplyToAccountID)
|
||||||
|
self.reblog = try container.decodeIfPresent(Status.self, forKey: .reblog)
|
||||||
|
self.content = try container.decode(String.self, forKey: .content)
|
||||||
|
self.createdAt = try container.decode(Date.self, forKey: .createdAt)
|
||||||
|
self.emojis = try container.decode([Emoji].self, forKey: .emojis)
|
||||||
|
self.reblogsCount = try container.decode(Int.self, forKey: .reblogsCount)
|
||||||
|
self.favouritesCount = try container.decode(Int.self, forKey: .favouritesCount)
|
||||||
|
self.reblogged = try container.decodeIfPresent(Bool.self, forKey: .reblogged)
|
||||||
|
self.favourited = try container.decodeIfPresent(Bool.self, forKey: .favourited)
|
||||||
|
self.muted = try container.decodeIfPresent(Bool.self, forKey: .muted)
|
||||||
|
self.sensitive = try container.decode(Bool.self, forKey: .sensitive)
|
||||||
|
self.spoilerText = try container.decode(String.self, forKey: .spoilerText)
|
||||||
|
if let visibility = try? container.decode(Status.Visibility.self, forKey: .visibility) {
|
||||||
|
self.visibility = visibility
|
||||||
|
self.localOnly = try container.decodeIfPresent(Bool.self, forKey: .localOnly)
|
||||||
|
} else if let s = try? container.decode(String.self, forKey: .visibility),
|
||||||
|
s == "local" {
|
||||||
|
// hacky workaround for #332, akkoma describes local posts with a separate visibility
|
||||||
|
self.visibility = .public
|
||||||
|
self.localOnly = true
|
||||||
|
} else {
|
||||||
|
throw DecodingError.dataCorruptedError(forKey: .visibility, in: container, debugDescription: "Could not decode visibility")
|
||||||
|
}
|
||||||
|
self.attachments = try container.decode([Attachment].self, forKey: .attachments)
|
||||||
|
self.mentions = try container.decode([Mention].self, forKey: .mentions)
|
||||||
|
self.hashtags = try container.decode([Hashtag].self, forKey: .hashtags)
|
||||||
|
self.application = try container.decodeIfPresent(Application.self, forKey: .application)
|
||||||
|
self.language = try container.decodeIfPresent(String.self, forKey: .language)
|
||||||
|
self.pinned = try container.decodeIfPresent(Bool.self, forKey: .pinned)
|
||||||
|
self.bookmarked = try container.decodeIfPresent(Bool.self, forKey: .bookmarked)
|
||||||
|
self.card = try container.decodeIfPresent(Card.self, forKey: .card)
|
||||||
|
self.poll = try container.decodeIfPresent(Poll.self, forKey: .poll)
|
||||||
|
}
|
||||||
|
|
||||||
public static func getContext(_ statusID: String) -> Request<ConversationContext> {
|
public static func getContext(_ statusID: String) -> Request<ConversationContext> {
|
||||||
return Request<ConversationContext>(method: .get, path: "/api/v1/statuses/\(statusID)/context")
|
return Request<ConversationContext>(method: .get, path: "/api/v1/statuses/\(statusID)/context")
|
||||||
}
|
}
|
||||||
|
@ -147,7 +188,7 @@ public final class Status: StatusProtocol, Decodable {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Status {
|
extension Status {
|
||||||
public enum Visibility: String, Codable, CaseIterable {
|
public enum Visibility: String, Codable, CaseIterable, Sendable {
|
||||||
case `public`
|
case `public`
|
||||||
case unlisted
|
case unlisted
|
||||||
case `private`
|
case `private`
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public enum StatusContentType: String, Codable, CaseIterable {
|
public enum StatusContentType: String, Codable, CaseIterable, Sendable {
|
||||||
case plain, markdown, html
|
case plain, markdown, html
|
||||||
|
|
||||||
var mimeType: String {
|
var mimeType: String {
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public struct Suggestion: Decodable {
|
public struct Suggestion: Decodable, Sendable {
|
||||||
public let source: Source
|
public let source: Source
|
||||||
public let account: Account
|
public let account: Account
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ public struct Suggestion: Decodable {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Suggestion {
|
extension Suggestion {
|
||||||
public enum Source: String, Decodable {
|
public enum Source: String, Decodable, Sendable {
|
||||||
case staff
|
case staff
|
||||||
case pastInteractions = "past_interactions"
|
case pastInteractions = "past_interactions"
|
||||||
case global
|
case global
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public enum Timeline: Equatable, Hashable {
|
public enum Timeline: Equatable, Hashable, Sendable {
|
||||||
case home
|
case home
|
||||||
case `public`(local: Bool)
|
case `public`(local: Bool)
|
||||||
case tag(hashtag: String)
|
case tag(hashtag: String)
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
//
|
||||||
|
// TimelineMarkers.swift
|
||||||
|
// Pachyderm
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 2/14/23.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public struct TimelineMarkers: Decodable, Sendable {
|
||||||
|
public let home: Marker?
|
||||||
|
public let notifications: Marker?
|
||||||
|
|
||||||
|
public static func request(timelines: [Timeline]) -> Request<TimelineMarkers> {
|
||||||
|
return Request(method: .get, path: "/api/v1/markers", queryParameters: "timeline[]" => timelines.map(\.rawValue))
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func update(timeline: Timeline, lastReadID: String) -> Request<Empty> {
|
||||||
|
return Request(method: .post, path: "/api/v1/markers", body: ParametersBody([
|
||||||
|
"\(timeline.rawValue)[last_read_id]" => lastReadID,
|
||||||
|
]))
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum Timeline: String {
|
||||||
|
case home
|
||||||
|
case notifications
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct Marker: Decodable, Sendable {
|
||||||
|
public let lastReadID: String
|
||||||
|
public let version: Int
|
||||||
|
public let updatedAt: Date
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case lastReadID = "last_read_id"
|
||||||
|
case version
|
||||||
|
case updatedAt = "updated_at"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
struct WellKnown: Decodable {
|
struct WellKnown: Decodable, Sendable {
|
||||||
let links: [Link]
|
let links: [Link]
|
||||||
|
|
||||||
struct Link: Decodable {
|
struct Link: Decodable {
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
protocol Body {
|
protocol Body: Sendable {
|
||||||
var mimeType: String? { get }
|
var mimeType: String? { get }
|
||||||
var data: Data? { get }
|
var data: Data? { get }
|
||||||
}
|
}
|
||||||
|
@ -76,7 +76,7 @@ struct FormDataBody: Body {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct JsonBody<T: Encodable>: Body {
|
struct JsonBody<T: Encodable & Sendable>: Body {
|
||||||
let value: T
|
let value: T
|
||||||
|
|
||||||
init(_ value: T) {
|
init(_ value: T) {
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public struct Endpoint: ExpressibleByStringInterpolation, CustomStringConvertible {
|
public struct Endpoint: ExpressibleByStringInterpolation, CustomStringConvertible, Sendable {
|
||||||
let components: [Component]
|
let components: [Component]
|
||||||
|
|
||||||
public init(stringLiteral value: StringLiteralType) {
|
public init(stringLiteral value: StringLiteralType) {
|
||||||
|
@ -54,7 +54,7 @@ public struct Endpoint: ExpressibleByStringInterpolation, CustomStringConvertibl
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Component {
|
enum Component: Sendable {
|
||||||
case literal(String)
|
case literal(String)
|
||||||
case interpolated(String)
|
case interpolated(String)
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public struct FormAttachment {
|
public struct FormAttachment: Sendable {
|
||||||
let mimeType: String
|
let mimeType: String
|
||||||
let data: Data
|
let data: Data
|
||||||
let fileName: String
|
let fileName: String
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public enum Method {
|
public enum Method: Sendable {
|
||||||
case get, post, put, patch, delete
|
case get, post, put, patch, delete
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
struct Parameter {
|
struct Parameter: Sendable {
|
||||||
let name: String
|
let name: String
|
||||||
let value: String?
|
let value: String?
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public struct Request<ResultType: Decodable> {
|
public struct Request<ResultType: Decodable>: Sendable {
|
||||||
let method: Method
|
let method: Method
|
||||||
let endpoint: Endpoint
|
let endpoint: Endpoint
|
||||||
let body: Body
|
let body: Body
|
||||||
|
|
|
@ -8,13 +8,24 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public enum RequestRange {
|
public enum RequestRange: Sendable {
|
||||||
case `default`
|
case `default`
|
||||||
case count(Int)
|
case count(Int)
|
||||||
/// Chronologically immediately before the given ID
|
/// Chronologically immediately before the given ID
|
||||||
case before(id: String, count: Int?)
|
case before(id: String, count: Int?)
|
||||||
/// Chronologically immediately after the given ID
|
/// Chronologically immediately after the given ID
|
||||||
case after(id: String, count: Int?)
|
case after(id: String, count: Int?)
|
||||||
|
|
||||||
|
public func withCount(_ count: Int) -> Self {
|
||||||
|
switch self {
|
||||||
|
case .default, .count(_):
|
||||||
|
return .count(count)
|
||||||
|
case .before(id: let id, count: _):
|
||||||
|
return .before(id: id, count: count)
|
||||||
|
case .after(id: let id, count: _):
|
||||||
|
return .after(id: id, count: count)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension RequestRange {
|
extension RequestRange {
|
||||||
|
|
|
@ -8,6 +8,6 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public struct Empty: Decodable {
|
public struct Empty: Decodable, Sendable {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public struct Pagination {
|
public struct Pagination: Sendable {
|
||||||
public let older: RequestRange?
|
public let older: RequestRange?
|
||||||
public let newer: RequestRange?
|
public let newer: RequestRange?
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public enum Response<Result: Decodable> {
|
public enum Response<Result: Decodable & Sendable>: Sendable {
|
||||||
case success(Result, Pagination?)
|
case success(Result, Pagination?)
|
||||||
case failure(Client.Error)
|
case failure(Client.Error)
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,8 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public class CollapseState: Equatable {
|
@MainActor
|
||||||
|
public final class CollapseState: Sendable {
|
||||||
public var collapsible: Bool?
|
public var collapsible: Bool?
|
||||||
public var collapsed: Bool?
|
public var collapsed: Bool?
|
||||||
|
|
||||||
|
@ -33,8 +34,4 @@ public class CollapseState: Equatable {
|
||||||
public static var unknown: CollapseState {
|
public static var unknown: CollapseState {
|
||||||
CollapseState(collapsible: nil, collapsed: nil)
|
CollapseState(collapsible: nil, collapsed: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func == (lhs: CollapseState, rhs: CollapseState) -> Bool {
|
|
||||||
lhs.collapsible == rhs.collapsible && lhs.collapsed == rhs.collapsed
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ public struct NotificationGroup: Identifiable, Hashable {
|
||||||
public let kind: Notification.Kind
|
public let kind: Notification.Kind
|
||||||
public let statusState: CollapseState?
|
public let statusState: CollapseState?
|
||||||
|
|
||||||
|
@MainActor
|
||||||
init?(notifications: [Notification]) {
|
init?(notifications: [Notification]) {
|
||||||
guard !notifications.isEmpty else { return nil }
|
guard !notifications.isEmpty else { return nil }
|
||||||
self.notifications = notifications
|
self.notifications = notifications
|
||||||
|
@ -51,6 +52,7 @@ public struct NotificationGroup: Identifiable, Hashable {
|
||||||
notifications.append(contentsOf: group.notifications)
|
notifications.append(contentsOf: group.notifications)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
public static func createGroups(notifications: [Notification], only allowedTypes: [Notification.Kind]) -> [NotificationGroup] {
|
public static func createGroups(notifications: [Notification], only allowedTypes: [Notification.Kind]) -> [NotificationGroup] {
|
||||||
var groups = [NotificationGroup]()
|
var groups = [NotificationGroup]()
|
||||||
for notification in notifications {
|
for notification in notifications {
|
||||||
|
|
|
@ -62,7 +62,7 @@ public class GameModel: NSObject, NSCopying, GKGameModel {
|
||||||
case .playAnywhere(update.mark), .playSpecific(update.mark, column: update.subBoard.column, row: update.subBoard.row):
|
case .playAnywhere(update.mark), .playSpecific(update.mark, column: update.subBoard.column, row: update.subBoard.row):
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
fatalError()
|
return
|
||||||
}
|
}
|
||||||
controller.play(on: update.subBoard, column: update.column, row: update.row)
|
controller.play(on: update.subBoard, column: update.column, row: update.row)
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import Combine
|
||||||
|
|
||||||
public class GameController: ObservableObject {
|
public class GameController: ObservableObject {
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,9 @@
|
||||||
D60088EF2980D8B5005B4D00 /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D60088EE2980D8B5005B4D00 /* StoreKit.framework */; };
|
D60088EF2980D8B5005B4D00 /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D60088EE2980D8B5005B4D00 /* StoreKit.framework */; };
|
||||||
D60088F22980DAA0005B4D00 /* TipJarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60088F12980DAA0005B4D00 /* TipJarView.swift */; };
|
D60088F22980DAA0005B4D00 /* TipJarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60088F12980DAA0005B4D00 /* TipJarView.swift */; };
|
||||||
D60089192981FEBA005B4D00 /* ConfettiView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60089182981FEBA005B4D00 /* ConfettiView.swift */; };
|
D60089192981FEBA005B4D00 /* ConfettiView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60089182981FEBA005B4D00 /* ConfettiView.swift */; };
|
||||||
|
D600891B29848289005B4D00 /* PinnedTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = D600891A29848289005B4D00 /* PinnedTimeline.swift */; };
|
||||||
|
D600891D298482F0005B4D00 /* PinnedTimelineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D600891C298482F0005B4D00 /* PinnedTimelineTests.swift */; };
|
||||||
|
D600891F29848DE2005B4D00 /* AddInstancePinnedTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D600891E29848DE2005B4D00 /* AddInstancePinnedTimelineView.swift */; };
|
||||||
D601FA5B29787AB100A8E8B5 /* AccountFollowsListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D601FA5A29787AB100A8E8B5 /* AccountFollowsListViewController.swift */; };
|
D601FA5B29787AB100A8E8B5 /* AccountFollowsListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D601FA5A29787AB100A8E8B5 /* AccountFollowsListViewController.swift */; };
|
||||||
D601FA5D297B2E6F00A8E8B5 /* ConversationCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D601FA5C297B2E6F00A8E8B5 /* ConversationCollectionViewController.swift */; };
|
D601FA5D297B2E6F00A8E8B5 /* ConversationCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D601FA5C297B2E6F00A8E8B5 /* ConversationCollectionViewController.swift */; };
|
||||||
D601FA5F297B339100A8E8B5 /* ExpandThreadCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D601FA5E297B339100A8E8B5 /* ExpandThreadCollectionViewCell.swift */; };
|
D601FA5F297B339100A8E8B5 /* ExpandThreadCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D601FA5E297B339100A8E8B5 /* ExpandThreadCollectionViewCell.swift */; };
|
||||||
|
@ -38,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 */; };
|
||||||
|
@ -102,7 +103,6 @@
|
||||||
D627943223A5466600D38C68 /* SelectableTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D627943123A5466600D38C68 /* SelectableTableViewCell.swift */; };
|
D627943223A5466600D38C68 /* SelectableTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D627943123A5466600D38C68 /* SelectableTableViewCell.swift */; };
|
||||||
D627943B23A55BA600D38C68 /* NavigableTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D627943A23A55BA600D38C68 /* NavigableTableViewCell.swift */; };
|
D627943B23A55BA600D38C68 /* NavigableTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D627943A23A55BA600D38C68 /* NavigableTableViewCell.swift */; };
|
||||||
D627944723A6AC9300D38C68 /* BasicTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D627944623A6AC9300D38C68 /* BasicTableViewCell.xib */; };
|
D627944723A6AC9300D38C68 /* BasicTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D627944623A6AC9300D38C68 /* BasicTableViewCell.xib */; };
|
||||||
D627944A23A6AD6100D38C68 /* BookmarksTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D627944923A6AD6100D38C68 /* BookmarksTableViewController.swift */; };
|
|
||||||
D627944D23A9A03D00D38C68 /* ListTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D627944C23A9A03D00D38C68 /* ListTimelineViewController.swift */; };
|
D627944D23A9A03D00D38C68 /* ListTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D627944C23A9A03D00D38C68 /* ListTimelineViewController.swift */; };
|
||||||
D627944F23A9C99800D38C68 /* EditListAccountsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D627944E23A9C99800D38C68 /* EditListAccountsViewController.swift */; };
|
D627944F23A9C99800D38C68 /* EditListAccountsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D627944E23A9C99800D38C68 /* EditListAccountsViewController.swift */; };
|
||||||
D627FF76217E923E00CC0648 /* DraftsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D627FF75217E923E00CC0648 /* DraftsManager.swift */; };
|
D627FF76217E923E00CC0648 /* DraftsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D627FF75217E923E00CC0648 /* DraftsManager.swift */; };
|
||||||
|
@ -200,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 */; };
|
||||||
|
@ -214,11 +215,16 @@
|
||||||
D68ACE5D279B1ABA001CE8EB /* AssetPickerControlCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68ACE5C279B1ABA001CE8EB /* AssetPickerControlCollectionViewCell.swift */; };
|
D68ACE5D279B1ABA001CE8EB /* AssetPickerControlCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68ACE5C279B1ABA001CE8EB /* AssetPickerControlCollectionViewCell.swift */; };
|
||||||
D68C2AE325869BAB00548EFF /* AuxiliarySceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68C2AE225869BAB00548EFF /* AuxiliarySceneDelegate.swift */; };
|
D68C2AE325869BAB00548EFF /* AuxiliarySceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68C2AE225869BAB00548EFF /* AuxiliarySceneDelegate.swift */; };
|
||||||
D68E525B24A3D77E0054355A /* TuskerRootViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68E525A24A3D77E0054355A /* TuskerRootViewController.swift */; };
|
D68E525B24A3D77E0054355A /* TuskerRootViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68E525A24A3D77E0054355A /* TuskerRootViewController.swift */; };
|
||||||
D68E525D24A3E8F00054355A /* SearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68E525C24A3E8F00054355A /* SearchViewController.swift */; };
|
D68E525D24A3E8F00054355A /* InlineTrendsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68E525C24A3E8F00054355A /* InlineTrendsViewController.swift */; };
|
||||||
D68E6F59253C9969001A1B4C /* MultiSourceEmojiLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68E6F58253C9969001A1B4C /* MultiSourceEmojiLabel.swift */; };
|
D68E6F59253C9969001A1B4C /* MultiSourceEmojiLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68E6F58253C9969001A1B4C /* MultiSourceEmojiLabel.swift */; };
|
||||||
D68E6F5F253C9B2D001A1B4C /* BaseEmojiLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68E6F5E253C9B2D001A1B4C /* BaseEmojiLabel.swift */; };
|
D68E6F5F253C9B2D001A1B4C /* BaseEmojiLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68E6F5E253C9B2D001A1B4C /* BaseEmojiLabel.swift */; };
|
||||||
D68FEC4F232C5BC300C84F23 /* SegmentedPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68FEC4E232C5BC300C84F23 /* SegmentedPageViewController.swift */; };
|
D68FEC4F232C5BC300C84F23 /* SegmentedPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68FEC4E232C5BC300C84F23 /* SegmentedPageViewController.swift */; };
|
||||||
D690797324A4EF9700023A34 /* UIBezierPath+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D690797224A4EF9700023A34 /* UIBezierPath+Helpers.swift */; };
|
D690797324A4EF9700023A34 /* UIBezierPath+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D690797224A4EF9700023A34 /* UIBezierPath+Helpers.swift */; };
|
||||||
|
D691771129A2B76A0054D7EF /* MainActor+Unsafe.swift in Sources */ = {isa = PBXBuildFile; fileRef = D691771029A2B76A0054D7EF /* MainActor+Unsafe.swift */; };
|
||||||
|
D691771529A6FCAB0054D7EF /* StateRestorableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D691771429A6FCAB0054D7EF /* StateRestorableViewController.swift */; };
|
||||||
|
D691771729A710520054D7EF /* ProfileNoContentCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D691771629A710520054D7EF /* ProfileNoContentCollectionViewCell.swift */; };
|
||||||
|
D691771929A7B8820054D7EF /* ProfileHeaderMovedOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D691771829A7B8820054D7EF /* ProfileHeaderMovedOverlayView.swift */; };
|
||||||
|
D691772E29AA5D420054D7EF /* UserActivityHandlingContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = D691772D29AA5D420054D7EF /* UserActivityHandlingContext.swift */; };
|
||||||
D693A72825CF282E003A14E2 /* TrendingHashtagsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693A72725CF282E003A14E2 /* TrendingHashtagsViewController.swift */; };
|
D693A72825CF282E003A14E2 /* TrendingHashtagsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693A72725CF282E003A14E2 /* TrendingHashtagsViewController.swift */; };
|
||||||
D693A72A25CF8C1E003A14E2 /* ProfileDirectoryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693A72925CF8C1E003A14E2 /* ProfileDirectoryViewController.swift */; };
|
D693A72A25CF8C1E003A14E2 /* ProfileDirectoryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693A72925CF8C1E003A14E2 /* ProfileDirectoryViewController.swift */; };
|
||||||
D693A72F25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693A72D25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift */; };
|
D693A72F25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693A72D25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift */; };
|
||||||
|
@ -251,7 +257,6 @@
|
||||||
D6A5BB2D23BBA9C4003BF21D /* MyProfileTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A5BB2C23BBA9C4003BF21D /* MyProfileTests.swift */; };
|
D6A5BB2D23BBA9C4003BF21D /* MyProfileTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A5BB2C23BBA9C4003BF21D /* MyProfileTests.swift */; };
|
||||||
D6A5BB2F23BBAC97003BF21D /* DelegatingResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A5BB2E23BBAC97003BF21D /* DelegatingResponse.swift */; };
|
D6A5BB2F23BBAC97003BF21D /* DelegatingResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A5BB2E23BBAC97003BF21D /* DelegatingResponse.swift */; };
|
||||||
D6A5BB3123BBAD87003BF21D /* JSONResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A5BB3023BBAD87003BF21D /* JSONResponse.swift */; };
|
D6A5BB3123BBAD87003BF21D /* JSONResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A5BB3023BBAD87003BF21D /* JSONResponse.swift */; };
|
||||||
D6A6C10525B6138A00298D0F /* StatusTablePrefetching.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A6C10425B6138A00298D0F /* StatusTablePrefetching.swift */; };
|
|
||||||
D6A6C10F25B62D2400298D0F /* DiskCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A6C10E25B62D2400298D0F /* DiskCache.swift */; };
|
D6A6C10F25B62D2400298D0F /* DiskCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A6C10E25B62D2400298D0F /* DiskCache.swift */; };
|
||||||
D6A6C11525B62E9700298D0F /* CacheExpiry.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A6C11425B62E9700298D0F /* CacheExpiry.swift */; };
|
D6A6C11525B62E9700298D0F /* CacheExpiry.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A6C11425B62E9700298D0F /* CacheExpiry.swift */; };
|
||||||
D6A6C11B25B63CEE00298D0F /* MemoryCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A6C11A25B63CEE00298D0F /* MemoryCache.swift */; };
|
D6A6C11B25B63CEE00298D0F /* MemoryCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A6C11A25B63CEE00298D0F /* MemoryCache.swift */; };
|
||||||
|
@ -293,6 +298,12 @@
|
||||||
D6BED174212667E900F02DA0 /* TimelineStatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BED173212667E900F02DA0 /* TimelineStatusTableViewCell.swift */; };
|
D6BED174212667E900F02DA0 /* TimelineStatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BED173212667E900F02DA0 /* TimelineStatusTableViewCell.swift */; };
|
||||||
D6C143DA253510F4007DC240 /* ComposeEmojiTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C143D9253510F4007DC240 /* ComposeEmojiTextField.swift */; };
|
D6C143DA253510F4007DC240 /* ComposeEmojiTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C143D9253510F4007DC240 /* ComposeEmojiTextField.swift */; };
|
||||||
D6C1B2082545D1EC00DAAA66 /* StatusCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C1B2072545D1EC00DAAA66 /* StatusCardView.swift */; };
|
D6C1B2082545D1EC00DAAA66 /* StatusCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C1B2072545D1EC00DAAA66 /* StatusCardView.swift */; };
|
||||||
|
D6C3F4F5298ED0890009FCFF /* LocalPredicateStatusesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C3F4F4298ED0890009FCFF /* LocalPredicateStatusesViewController.swift */; };
|
||||||
|
D6C3F4F7298ED7F70009FCFF /* FavoritesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C3F4F6298ED7F70009FCFF /* FavoritesViewController.swift */; };
|
||||||
|
D6C3F4F9298EDBF20009FCFF /* ConversationTree.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C3F4F8298EDBF20009FCFF /* ConversationTree.swift */; };
|
||||||
|
D6C3F4FB299035650009FCFF /* TrendsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C3F4FA299035650009FCFF /* TrendsViewController.swift */; };
|
||||||
|
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 */; };
|
||||||
|
@ -319,10 +330,16 @@
|
||||||
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 */; };
|
||||||
D6DD2A3F273C1F4900386A6C /* ComposeAttachmentImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD2A3E273C1F4900386A6C /* ComposeAttachmentImage.swift */; };
|
D6DD2A3F273C1F4900386A6C /* ComposeAttachmentImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD2A3E273C1F4900386A6C /* ComposeAttachmentImage.swift */; };
|
||||||
D6DD2A45273D6C5700386A6C /* GIFImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD2A44273D6C5700386A6C /* GIFImageView.swift */; };
|
D6DD2A45273D6C5700386A6C /* GIFImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD2A44273D6C5700386A6C /* GIFImageView.swift */; };
|
||||||
D6DD353D22F28CD000A9563A /* ContentWarningCopyMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD353C22F28CD000A9563A /* ContentWarningCopyMode.swift */; };
|
D6DD353D22F28CD000A9563A /* ContentWarningCopyMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD353C22F28CD000A9563A /* ContentWarningCopyMode.swift */; };
|
||||||
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 */; };
|
||||||
|
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 */; };
|
||||||
|
@ -422,6 +439,9 @@
|
||||||
D60088F02980D938005B4D00 /* Tusker.storekit */ = {isa = PBXFileReference; lastKnownFileType = text; path = Tusker.storekit; sourceTree = "<group>"; };
|
D60088F02980D938005B4D00 /* Tusker.storekit */ = {isa = PBXFileReference; lastKnownFileType = text; path = Tusker.storekit; sourceTree = "<group>"; };
|
||||||
D60088F12980DAA0005B4D00 /* TipJarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TipJarView.swift; sourceTree = "<group>"; };
|
D60088F12980DAA0005B4D00 /* TipJarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TipJarView.swift; sourceTree = "<group>"; };
|
||||||
D60089182981FEBA005B4D00 /* ConfettiView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfettiView.swift; sourceTree = "<group>"; };
|
D60089182981FEBA005B4D00 /* ConfettiView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfettiView.swift; sourceTree = "<group>"; };
|
||||||
|
D600891A29848289005B4D00 /* PinnedTimeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinnedTimeline.swift; sourceTree = "<group>"; };
|
||||||
|
D600891C298482F0005B4D00 /* PinnedTimelineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinnedTimelineTests.swift; sourceTree = "<group>"; };
|
||||||
|
D600891E29848DE2005B4D00 /* AddInstancePinnedTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddInstancePinnedTimelineView.swift; sourceTree = "<group>"; };
|
||||||
D601FA5A29787AB100A8E8B5 /* AccountFollowsListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountFollowsListViewController.swift; sourceTree = "<group>"; };
|
D601FA5A29787AB100A8E8B5 /* AccountFollowsListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountFollowsListViewController.swift; sourceTree = "<group>"; };
|
||||||
D601FA5C297B2E6F00A8E8B5 /* ConversationCollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationCollectionViewController.swift; sourceTree = "<group>"; };
|
D601FA5C297B2E6F00A8E8B5 /* ConversationCollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationCollectionViewController.swift; sourceTree = "<group>"; };
|
||||||
D601FA5E297B339100A8E8B5 /* ExpandThreadCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpandThreadCollectionViewCell.swift; sourceTree = "<group>"; };
|
D601FA5E297B339100A8E8B5 /* ExpandThreadCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpandThreadCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||||
|
@ -439,8 +459,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>"; };
|
||||||
|
@ -502,7 +520,6 @@
|
||||||
D627943123A5466600D38C68 /* SelectableTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectableTableViewCell.swift; sourceTree = "<group>"; };
|
D627943123A5466600D38C68 /* SelectableTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectableTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
D627943A23A55BA600D38C68 /* NavigableTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigableTableViewCell.swift; sourceTree = "<group>"; };
|
D627943A23A55BA600D38C68 /* NavigableTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigableTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
D627944623A6AC9300D38C68 /* BasicTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = BasicTableViewCell.xib; sourceTree = "<group>"; };
|
D627944623A6AC9300D38C68 /* BasicTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = BasicTableViewCell.xib; sourceTree = "<group>"; };
|
||||||
D627944923A6AD6100D38C68 /* BookmarksTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksTableViewController.swift; sourceTree = "<group>"; };
|
|
||||||
D627944C23A9A03D00D38C68 /* ListTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListTimelineViewController.swift; sourceTree = "<group>"; };
|
D627944C23A9A03D00D38C68 /* ListTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListTimelineViewController.swift; sourceTree = "<group>"; };
|
||||||
D627944E23A9C99800D38C68 /* EditListAccountsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditListAccountsViewController.swift; sourceTree = "<group>"; };
|
D627944E23A9C99800D38C68 /* EditListAccountsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditListAccountsViewController.swift; sourceTree = "<group>"; };
|
||||||
D627FF75217E923E00CC0648 /* DraftsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraftsManager.swift; sourceTree = "<group>"; };
|
D627FF75217E923E00CC0648 /* DraftsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraftsManager.swift; sourceTree = "<group>"; };
|
||||||
|
@ -583,6 +600,7 @@
|
||||||
D667E5F72135C3040057A976 /* Mastodon+Equatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Equatable.swift"; sourceTree = "<group>"; };
|
D667E5F72135C3040057A976 /* Mastodon+Equatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Equatable.swift"; sourceTree = "<group>"; };
|
||||||
D66A77BA233838DC0058F1EC /* UIFont+Traits.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIFont+Traits.swift"; sourceTree = "<group>"; };
|
D66A77BA233838DC0058F1EC /* UIFont+Traits.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIFont+Traits.swift"; sourceTree = "<group>"; };
|
||||||
D66C900A28DAB7FD00217BF2 /* TimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineViewController.swift; sourceTree = "<group>"; };
|
D66C900A28DAB7FD00217BF2 /* TimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineViewController.swift; sourceTree = "<group>"; };
|
||||||
|
D671A6BE299DA96100A81FEA /* Tusker-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Tusker-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||||
D673ACCD2919E74200D6F8B0 /* MenuPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuPicker.swift; sourceTree = "<group>"; };
|
D673ACCD2919E74200D6F8B0 /* MenuPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuPicker.swift; sourceTree = "<group>"; };
|
||||||
D674A50727F910F300BA03AC /* Pachyderm */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Pachyderm; path = Packages/Pachyderm; sourceTree = "<group>"; };
|
D674A50727F910F300BA03AC /* Pachyderm */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Pachyderm; path = Packages/Pachyderm; sourceTree = "<group>"; };
|
||||||
D677284724ECBCB100C732D3 /* ComposeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeView.swift; sourceTree = "<group>"; };
|
D677284724ECBCB100C732D3 /* ComposeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeView.swift; sourceTree = "<group>"; };
|
||||||
|
@ -601,6 +619,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>"; };
|
||||||
|
@ -616,11 +635,16 @@
|
||||||
D68ACE5C279B1ABA001CE8EB /* AssetPickerControlCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetPickerControlCollectionViewCell.swift; sourceTree = "<group>"; };
|
D68ACE5C279B1ABA001CE8EB /* AssetPickerControlCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetPickerControlCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||||
D68C2AE225869BAB00548EFF /* AuxiliarySceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuxiliarySceneDelegate.swift; sourceTree = "<group>"; };
|
D68C2AE225869BAB00548EFF /* AuxiliarySceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuxiliarySceneDelegate.swift; sourceTree = "<group>"; };
|
||||||
D68E525A24A3D77E0054355A /* TuskerRootViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TuskerRootViewController.swift; sourceTree = "<group>"; };
|
D68E525A24A3D77E0054355A /* TuskerRootViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TuskerRootViewController.swift; sourceTree = "<group>"; };
|
||||||
D68E525C24A3E8F00054355A /* SearchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewController.swift; sourceTree = "<group>"; };
|
D68E525C24A3E8F00054355A /* InlineTrendsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlineTrendsViewController.swift; sourceTree = "<group>"; };
|
||||||
D68E6F58253C9969001A1B4C /* MultiSourceEmojiLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiSourceEmojiLabel.swift; sourceTree = "<group>"; };
|
D68E6F58253C9969001A1B4C /* MultiSourceEmojiLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiSourceEmojiLabel.swift; sourceTree = "<group>"; };
|
||||||
D68E6F5E253C9B2D001A1B4C /* BaseEmojiLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseEmojiLabel.swift; sourceTree = "<group>"; };
|
D68E6F5E253C9B2D001A1B4C /* BaseEmojiLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseEmojiLabel.swift; sourceTree = "<group>"; };
|
||||||
D68FEC4E232C5BC300C84F23 /* SegmentedPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SegmentedPageViewController.swift; sourceTree = "<group>"; };
|
D68FEC4E232C5BC300C84F23 /* SegmentedPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SegmentedPageViewController.swift; sourceTree = "<group>"; };
|
||||||
D690797224A4EF9700023A34 /* UIBezierPath+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIBezierPath+Helpers.swift"; sourceTree = "<group>"; };
|
D690797224A4EF9700023A34 /* UIBezierPath+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIBezierPath+Helpers.swift"; sourceTree = "<group>"; };
|
||||||
|
D691771029A2B76A0054D7EF /* MainActor+Unsafe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MainActor+Unsafe.swift"; sourceTree = "<group>"; };
|
||||||
|
D691771429A6FCAB0054D7EF /* StateRestorableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateRestorableViewController.swift; sourceTree = "<group>"; };
|
||||||
|
D691771629A710520054D7EF /* ProfileNoContentCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileNoContentCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||||
|
D691771829A7B8820054D7EF /* ProfileHeaderMovedOverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileHeaderMovedOverlayView.swift; sourceTree = "<group>"; };
|
||||||
|
D691772D29AA5D420054D7EF /* UserActivityHandlingContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserActivityHandlingContext.swift; sourceTree = "<group>"; };
|
||||||
D693A72725CF282E003A14E2 /* TrendingHashtagsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingHashtagsViewController.swift; sourceTree = "<group>"; };
|
D693A72725CF282E003A14E2 /* TrendingHashtagsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingHashtagsViewController.swift; sourceTree = "<group>"; };
|
||||||
D693A72925CF8C1E003A14E2 /* ProfileDirectoryViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileDirectoryViewController.swift; sourceTree = "<group>"; };
|
D693A72925CF8C1E003A14E2 /* ProfileDirectoryViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileDirectoryViewController.swift; sourceTree = "<group>"; };
|
||||||
D693A72D25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeaturedProfileCollectionViewCell.swift; sourceTree = "<group>"; };
|
D693A72D25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeaturedProfileCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||||
|
@ -653,7 +677,6 @@
|
||||||
D6A5BB2C23BBA9C4003BF21D /* MyProfileTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyProfileTests.swift; sourceTree = "<group>"; };
|
D6A5BB2C23BBA9C4003BF21D /* MyProfileTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyProfileTests.swift; sourceTree = "<group>"; };
|
||||||
D6A5BB2E23BBAC97003BF21D /* DelegatingResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DelegatingResponse.swift; sourceTree = "<group>"; };
|
D6A5BB2E23BBAC97003BF21D /* DelegatingResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DelegatingResponse.swift; sourceTree = "<group>"; };
|
||||||
D6A5BB3023BBAD87003BF21D /* JSONResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONResponse.swift; sourceTree = "<group>"; };
|
D6A5BB3023BBAD87003BF21D /* JSONResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONResponse.swift; sourceTree = "<group>"; };
|
||||||
D6A6C10425B6138A00298D0F /* StatusTablePrefetching.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusTablePrefetching.swift; sourceTree = "<group>"; };
|
|
||||||
D6A6C10E25B62D2400298D0F /* DiskCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiskCache.swift; sourceTree = "<group>"; };
|
D6A6C10E25B62D2400298D0F /* DiskCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiskCache.swift; sourceTree = "<group>"; };
|
||||||
D6A6C11425B62E9700298D0F /* CacheExpiry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheExpiry.swift; sourceTree = "<group>"; };
|
D6A6C11425B62E9700298D0F /* CacheExpiry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheExpiry.swift; sourceTree = "<group>"; };
|
||||||
D6A6C11A25B63CEE00298D0F /* MemoryCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoryCache.swift; sourceTree = "<group>"; };
|
D6A6C11A25B63CEE00298D0F /* MemoryCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoryCache.swift; sourceTree = "<group>"; };
|
||||||
|
@ -695,6 +718,12 @@
|
||||||
D6BED173212667E900F02DA0 /* TimelineStatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStatusTableViewCell.swift; sourceTree = "<group>"; };
|
D6BED173212667E900F02DA0 /* TimelineStatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStatusTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
D6C143D9253510F4007DC240 /* ComposeEmojiTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeEmojiTextField.swift; sourceTree = "<group>"; };
|
D6C143D9253510F4007DC240 /* ComposeEmojiTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeEmojiTextField.swift; sourceTree = "<group>"; };
|
||||||
D6C1B2072545D1EC00DAAA66 /* StatusCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusCardView.swift; sourceTree = "<group>"; };
|
D6C1B2072545D1EC00DAAA66 /* StatusCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusCardView.swift; sourceTree = "<group>"; };
|
||||||
|
D6C3F4F4298ED0890009FCFF /* LocalPredicateStatusesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalPredicateStatusesViewController.swift; sourceTree = "<group>"; };
|
||||||
|
D6C3F4F6298ED7F70009FCFF /* FavoritesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesViewController.swift; sourceTree = "<group>"; };
|
||||||
|
D6C3F4F8298EDBF20009FCFF /* ConversationTree.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationTree.swift; sourceTree = "<group>"; };
|
||||||
|
D6C3F4FA299035650009FCFF /* TrendsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendsViewController.swift; sourceTree = "<group>"; };
|
||||||
|
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>"; };
|
||||||
|
@ -728,10 +757,16 @@
|
||||||
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>"; };
|
||||||
D6DD2A3E273C1F4900386A6C /* ComposeAttachmentImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeAttachmentImage.swift; sourceTree = "<group>"; };
|
D6DD2A3E273C1F4900386A6C /* ComposeAttachmentImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeAttachmentImage.swift; sourceTree = "<group>"; };
|
||||||
D6DD2A44273D6C5700386A6C /* GIFImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GIFImageView.swift; sourceTree = "<group>"; };
|
D6DD2A44273D6C5700386A6C /* GIFImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GIFImageView.swift; sourceTree = "<group>"; };
|
||||||
D6DD353C22F28CD000A9563A /* ContentWarningCopyMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentWarningCopyMode.swift; sourceTree = "<group>"; };
|
D6DD353C22F28CD000A9563A /* ContentWarningCopyMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentWarningCopyMode.swift; sourceTree = "<group>"; };
|
||||||
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>"; };
|
||||||
|
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>"; };
|
||||||
|
@ -835,8 +870,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";
|
||||||
|
@ -852,6 +885,7 @@
|
||||||
D627FF75217E923E00CC0648 /* DraftsManager.swift */,
|
D627FF75217E923E00CC0648 /* DraftsManager.swift */,
|
||||||
D61F75AE293AF50C00C0B37F /* EditedFilter.swift */,
|
D61F75AE293AF50C00C0B37F /* EditedFilter.swift */,
|
||||||
D65B4B532971F71D00DABDFB /* EditedReport.swift */,
|
D65B4B532971F71D00DABDFB /* EditedReport.swift */,
|
||||||
|
D600891A29848289005B4D00 /* PinnedTimeline.swift */,
|
||||||
);
|
);
|
||||||
path = Models;
|
path = Models;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -873,6 +907,7 @@
|
||||||
D61F75A4293ABD6F00C0B37F /* EditFilterView.swift */,
|
D61F75A4293ABD6F00C0B37F /* EditFilterView.swift */,
|
||||||
D68A76E729527884001DA1B3 /* PinnedTimelinesView.swift */,
|
D68A76E729527884001DA1B3 /* PinnedTimelinesView.swift */,
|
||||||
D68A76E9295285D0001DA1B3 /* AddHashtagPinnedTimelineView.swift */,
|
D68A76E9295285D0001DA1B3 /* AddHashtagPinnedTimelineView.swift */,
|
||||||
|
D600891E29848DE2005B4D00 /* AddInstancePinnedTimelineView.swift */,
|
||||||
);
|
);
|
||||||
path = "Customize Timelines";
|
path = "Customize Timelines";
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -907,6 +942,7 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
D6C82B4025C5BB7E0017F1E6 /* ExploreViewController.swift */,
|
D6C82B4025C5BB7E0017F1E6 /* ExploreViewController.swift */,
|
||||||
|
D68E525C24A3E8F00054355A /* InlineTrendsViewController.swift */,
|
||||||
D6945C3323AC6431005C403C /* AddSavedHashtagViewController.swift */,
|
D6945C3323AC6431005C403C /* AddSavedHashtagViewController.swift */,
|
||||||
D6093F9A25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift */,
|
D6093F9A25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift */,
|
||||||
D6945C3923AC75E2005C403C /* FindInstanceViewController.swift */,
|
D6945C3923AC75E2005C403C /* FindInstanceViewController.swift */,
|
||||||
|
@ -921,16 +957,21 @@
|
||||||
D693A72925CF8C1E003A14E2 /* ProfileDirectoryViewController.swift */,
|
D693A72925CF8C1E003A14E2 /* ProfileDirectoryViewController.swift */,
|
||||||
D693A72D25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift */,
|
D693A72D25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift */,
|
||||||
D693A72E25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.xib */,
|
D693A72E25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.xib */,
|
||||||
|
D6C3F4FA299035650009FCFF /* TrendsViewController.swift */,
|
||||||
|
D68329EE299540050026EB24 /* MoreTrendsFooterCollectionViewCell.swift */,
|
||||||
|
D6DD996A2998611A0015C962 /* SuggestedProfilesViewController.swift */,
|
||||||
);
|
);
|
||||||
path = Explore;
|
path = Explore;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
D627944823A6AD5100D38C68 /* Bookmarks */ = {
|
D627944823A6AD5100D38C68 /* Local Predicate Statuses List */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
D627944923A6AD6100D38C68 /* BookmarksTableViewController.swift */,
|
D6C3F4F4298ED0890009FCFF /* LocalPredicateStatusesViewController.swift */,
|
||||||
|
D6DD8FFE2984D327002AD3FD /* BookmarksViewController.swift */,
|
||||||
|
D6C3F4F6298ED7F70009FCFF /* FavoritesViewController.swift */,
|
||||||
);
|
);
|
||||||
path = Bookmarks;
|
path = "Local Predicate Statuses List";
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
D627944B23A9A02400D38C68 /* Lists */ = {
|
D627944B23A9A02400D38C68 /* Lists */ = {
|
||||||
|
@ -947,6 +988,7 @@
|
||||||
children = (
|
children = (
|
||||||
D62D2425217ABF63005076CC /* UserActivityType.swift */,
|
D62D2425217ABF63005076CC /* UserActivityType.swift */,
|
||||||
D62D2421217AA7E1005076CC /* UserActivityManager.swift */,
|
D62D2421217AA7E1005076CC /* UserActivityManager.swift */,
|
||||||
|
D691772D29AA5D420054D7EF /* UserActivityHandlingContext.swift */,
|
||||||
D62D2423217ABF3F005076CC /* NSUserActivity+Extensions.swift */,
|
D62D2423217ABF3F005076CC /* NSUserActivity+Extensions.swift */,
|
||||||
D6674AE923341F7600E8DF94 /* AppShortcutItems.swift */,
|
D6674AE923341F7600E8DF94 /* AppShortcutItems.swift */,
|
||||||
);
|
);
|
||||||
|
@ -993,7 +1035,6 @@
|
||||||
D6A3BC822321F69400FD64D5 /* Account List */,
|
D6A3BC822321F69400FD64D5 /* Account List */,
|
||||||
D6B053A023BD2BED00A066FA /* Asset Picker */,
|
D6B053A023BD2BED00A066FA /* Asset Picker */,
|
||||||
0411610522B457290030A9B7 /* Attachment Gallery */,
|
0411610522B457290030A9B7 /* Attachment Gallery */,
|
||||||
D627944823A6AD5100D38C68 /* Bookmarks */,
|
|
||||||
D641C787213DD862004B4513 /* Compose */,
|
D641C787213DD862004B4513 /* Compose */,
|
||||||
D641C785213DD83B004B4513 /* Conversation */,
|
D641C785213DD83B004B4513 /* Conversation */,
|
||||||
D6F2E960249E772F005846BB /* Crash Reporter */,
|
D6F2E960249E772F005846BB /* Crash Reporter */,
|
||||||
|
@ -1002,6 +1043,7 @@
|
||||||
D61F759729384D4200C0B37F /* Customize Timelines */,
|
D61F759729384D4200C0B37F /* Customize Timelines */,
|
||||||
D641C788213DD86D004B4513 /* Large Image */,
|
D641C788213DD86D004B4513 /* Large Image */,
|
||||||
D627944B23A9A02400D38C68 /* Lists */,
|
D627944B23A9A02400D38C68 /* Lists */,
|
||||||
|
D627944823A6AD5100D38C68 /* Local Predicate Statuses List */,
|
||||||
D641C782213DD7F0004B4513 /* Main */,
|
D641C782213DD7F0004B4513 /* Main */,
|
||||||
D6F6A555291F4F0C00F496A8 /* Mute */,
|
D6F6A555291F4F0C00F496A8 /* Mute */,
|
||||||
D641C786213DD852004B4513 /* Notifications */,
|
D641C786213DD852004B4513 /* Notifications */,
|
||||||
|
@ -1026,6 +1068,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>";
|
||||||
|
@ -1060,6 +1103,7 @@
|
||||||
D61DC84C28F500D200B82C6E /* ProfileViewController.swift */,
|
D61DC84C28F500D200B82C6E /* ProfileViewController.swift */,
|
||||||
D61ABEF728EFC3F900B29151 /* ProfileStatusesViewController.swift */,
|
D61ABEF728EFC3F900B29151 /* ProfileStatusesViewController.swift */,
|
||||||
D61DC84A28F4FD2000B82C6E /* ProfileHeaderCollectionViewCell.swift */,
|
D61DC84A28F4FD2000B82C6E /* ProfileHeaderCollectionViewCell.swift */,
|
||||||
|
D691771629A710520054D7EF /* ProfileNoContentCollectionViewCell.swift */,
|
||||||
);
|
);
|
||||||
path = Profile;
|
path = Profile;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1070,6 +1114,7 @@
|
||||||
D65B4B6329771EFF00DABDFB /* ConversationViewController.swift */,
|
D65B4B6329771EFF00DABDFB /* ConversationViewController.swift */,
|
||||||
D601FA5C297B2E6F00A8E8B5 /* ConversationCollectionViewController.swift */,
|
D601FA5C297B2E6F00A8E8B5 /* ConversationCollectionViewController.swift */,
|
||||||
D601FA5E297B339100A8E8B5 /* ExpandThreadCollectionViewCell.swift */,
|
D601FA5E297B339100A8E8B5 /* ExpandThreadCollectionViewCell.swift */,
|
||||||
|
D6C3F4F8298EDBF20009FCFF /* ConversationTree.swift */,
|
||||||
);
|
);
|
||||||
path = Conversation;
|
path = Conversation;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1156,6 +1201,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>";
|
||||||
|
@ -1167,6 +1213,7 @@
|
||||||
D6412B0C24B0D4CF00F5412E /* ProfileHeaderView.swift */,
|
D6412B0C24B0D4CF00F5412E /* ProfileHeaderView.swift */,
|
||||||
D651C5B32915B00400236EF6 /* ProfileFieldsView.swift */,
|
D651C5B32915B00400236EF6 /* ProfileFieldsView.swift */,
|
||||||
D6A3A37F295515550036B6EF /* ProfileHeaderButton.swift */,
|
D6A3A37F295515550036B6EF /* ProfileHeaderButton.swift */,
|
||||||
|
D691771829A7B8820054D7EF /* ProfileHeaderMovedOverlayView.swift */,
|
||||||
);
|
);
|
||||||
path = "Profile Header";
|
path = "Profile Header";
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1249,6 +1296,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>";
|
||||||
|
@ -1278,6 +1326,8 @@
|
||||||
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 */,
|
||||||
|
D691771029A2B76A0054D7EF /* MainActor+Unsafe.swift */,
|
||||||
);
|
);
|
||||||
path = Extensions;
|
path = Extensions;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1389,7 +1439,6 @@
|
||||||
D6BC9DD8232D8BCA002CA326 /* Search */ = {
|
D6BC9DD8232D8BCA002CA326 /* Search */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
D68E525C24A3E8F00054355A /* SearchViewController.swift */,
|
|
||||||
D6BC9DD9232D8BE5002CA326 /* SearchResultsViewController.swift */,
|
D6BC9DD9232D8BE5002CA326 /* SearchResultsViewController.swift */,
|
||||||
);
|
);
|
||||||
path = Search;
|
path = Search;
|
||||||
|
@ -1404,6 +1453,7 @@
|
||||||
D6ADB6EF28ED1F25009924AB /* CachedImageView.swift */,
|
D6ADB6EF28ED1F25009924AB /* CachedImageView.swift */,
|
||||||
D6895DC328D65342006341DA /* ConfirmReblogStatusPreviewView.swift */,
|
D6895DC328D65342006341DA /* ConfirmReblogStatusPreviewView.swift */,
|
||||||
D620483523D38075008A63EF /* ContentTextView.swift */,
|
D620483523D38075008A63EF /* ContentTextView.swift */,
|
||||||
|
D6D9498E298EB79400C59229 /* CopyableLable.swift */,
|
||||||
D6E426B225337C7000C02E1C /* CustomEmojiImageView.swift */,
|
D6E426B225337C7000C02E1C /* CustomEmojiImageView.swift */,
|
||||||
D6969E9F240C8384002843CE /* EmojiLabel.swift */,
|
D6969E9F240C8384002843CE /* EmojiLabel.swift */,
|
||||||
D61F75B8293C15A000C0B37F /* ZeroHeightCollectionViewCell.swift */,
|
D61F75B8293C15A000C0B37F /* ZeroHeightCollectionViewCell.swift */,
|
||||||
|
@ -1456,8 +1506,8 @@
|
||||||
D6E0DC8D216EDF1E00369478 /* Previewing.swift */,
|
D6E0DC8D216EDF1E00369478 /* Previewing.swift */,
|
||||||
D6B81F3B2560365300F6E31D /* RefreshableViewController.swift */,
|
D6B81F3B2560365300F6E31D /* RefreshableViewController.swift */,
|
||||||
D68FEC4E232C5BC300C84F23 /* SegmentedPageViewController.swift */,
|
D68FEC4E232C5BC300C84F23 /* SegmentedPageViewController.swift */,
|
||||||
|
D691771429A6FCAB0054D7EF /* StateRestorableViewController.swift */,
|
||||||
D63CC7112911F57C000E19DE /* StatusBarTappableViewController.swift */,
|
D63CC7112911F57C000E19DE /* StatusBarTappableViewController.swift */,
|
||||||
D6A6C10425B6138A00298D0F /* StatusTablePrefetching.swift */,
|
|
||||||
D6412B0224AFF6A600F5412E /* TabBarScrollableViewController.swift */,
|
D6412B0224AFF6A600F5412E /* TabBarScrollableViewController.swift */,
|
||||||
D6B22A0E2560D52D004D82EF /* TabbedPageViewController.swift */,
|
D6B22A0E2560D52D004D82EF /* TabbedPageViewController.swift */,
|
||||||
D6538944214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift */,
|
D6538944214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift */,
|
||||||
|
@ -1514,6 +1564,7 @@
|
||||||
D640D76822BAF5E6004FBE69 /* DomainBlocks.plist */,
|
D640D76822BAF5E6004FBE69 /* DomainBlocks.plist */,
|
||||||
D6D4DDDB212518A200E1C4BB /* Info.plist */,
|
D6D4DDDB212518A200E1C4BB /* Info.plist */,
|
||||||
D60088F02980D938005B4D00 /* Tusker.storekit */,
|
D60088F02980D938005B4D00 /* Tusker.storekit */,
|
||||||
|
D671A6BE299DA96100A81FEA /* Tusker-Bridging-Header.h */,
|
||||||
D6D4DDCF212518A000E1C4BB /* AppDelegate.swift */,
|
D6D4DDCF212518A000E1C4BB /* AppDelegate.swift */,
|
||||||
D6CA6A91249FAD8900AD45C1 /* AudioSessionHelper.swift */,
|
D6CA6A91249FAD8900AD45C1 /* AudioSessionHelper.swift */,
|
||||||
D61F75B6293C119700C0B37F /* Filterer.swift */,
|
D61F75B6293C119700C0B37F /* Filterer.swift */,
|
||||||
|
@ -1561,6 +1612,7 @@
|
||||||
D6114E1627F8BB210080E273 /* VersionTests.swift */,
|
D6114E1627F8BB210080E273 /* VersionTests.swift */,
|
||||||
D61F75A029396DE200C0B37F /* SemiCaseSensitiveComparatorTests.swift */,
|
D61F75A029396DE200C0B37F /* SemiCaseSensitiveComparatorTests.swift */,
|
||||||
D6CA8CD92962231F0050C433 /* ArrayUniqueTests.swift */,
|
D6CA8CD92962231F0050C433 /* ArrayUniqueTests.swift */,
|
||||||
|
D600891C298482F0005B4D00 /* PinnedTimelineTests.swift */,
|
||||||
D6D4DDE6212518A200E1C4BB /* Info.plist */,
|
D6D4DDE6212518A200E1C4BB /* Info.plist */,
|
||||||
);
|
);
|
||||||
path = TuskerTests;
|
path = TuskerTests;
|
||||||
|
@ -1655,6 +1707,7 @@
|
||||||
D61F75B4293BD97400C0B37F /* DeleteFilterService.swift */,
|
D61F75B4293BD97400C0B37F /* DeleteFilterService.swift */,
|
||||||
D65B4B6129771A3F00DABDFB /* FetchStatusService.swift */,
|
D65B4B6129771A3F00DABDFB /* FetchStatusService.swift */,
|
||||||
D65B4B6529773AE600DABDFB /* DeleteStatusService.swift */,
|
D65B4B6529773AE600DABDFB /* DeleteStatusService.swift */,
|
||||||
|
D6DD8FFC298495A8002AD3FD /* LogoutService.swift */,
|
||||||
);
|
);
|
||||||
path = API;
|
path = API;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1761,7 +1814,7 @@
|
||||||
TargetAttributes = {
|
TargetAttributes = {
|
||||||
D6D4DDCB212518A000E1C4BB = {
|
D6D4DDCB212518A000E1C4BB = {
|
||||||
CreatedOnToolsVersion = 10.0;
|
CreatedOnToolsVersion = 10.0;
|
||||||
LastSwiftMigration = 1410;
|
LastSwiftMigration = 1420;
|
||||||
};
|
};
|
||||||
D6D4DDDF212518A200E1C4BB = {
|
D6D4DDDF212518A200E1C4BB = {
|
||||||
CreatedOnToolsVersion = 10.0;
|
CreatedOnToolsVersion = 10.0;
|
||||||
|
@ -1816,7 +1869,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 */,
|
||||||
|
@ -1924,7 +1976,9 @@
|
||||||
D6285B5321EA708700FE4B39 /* StatusFormat.swift in Sources */,
|
D6285B5321EA708700FE4B39 /* StatusFormat.swift in Sources */,
|
||||||
D6DD353D22F28CD000A9563A /* ContentWarningCopyMode.swift in Sources */,
|
D6DD353D22F28CD000A9563A /* ContentWarningCopyMode.swift in Sources */,
|
||||||
0427033A22B31269000D31B6 /* AdvancedPrefsView.swift in Sources */,
|
0427033A22B31269000D31B6 /* AdvancedPrefsView.swift in Sources */,
|
||||||
|
D6C3F4F5298ED0890009FCFF /* LocalPredicateStatusesViewController.swift in Sources */,
|
||||||
D662AEEF263A3B880082A153 /* PollFinishedTableViewCell.swift in Sources */,
|
D662AEEF263A3B880082A153 /* PollFinishedTableViewCell.swift in Sources */,
|
||||||
|
D691771729A710520054D7EF /* ProfileNoContentCollectionViewCell.swift in Sources */,
|
||||||
D626493C23C1000300612E6E /* AlbumTableViewCell.swift in Sources */,
|
D626493C23C1000300612E6E /* AlbumTableViewCell.swift in Sources */,
|
||||||
D62275A624F1C81800B82A16 /* ComposeReplyView.swift in Sources */,
|
D62275A624F1C81800B82A16 /* ComposeReplyView.swift in Sources */,
|
||||||
D60E2F292442372B005F8713 /* AccountMO.swift in Sources */,
|
D60E2F292442372B005F8713 /* AccountMO.swift in Sources */,
|
||||||
|
@ -1932,8 +1986,10 @@
|
||||||
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 */,
|
||||||
04DACE8C212CB14B009840C4 /* MainTabBarViewController.swift in Sources */,
|
04DACE8C212CB14B009840C4 /* MainTabBarViewController.swift in Sources */,
|
||||||
D68E525D24A3E8F00054355A /* SearchViewController.swift in Sources */,
|
D68E525D24A3E8F00054355A /* InlineTrendsViewController.swift in Sources */,
|
||||||
D61F75BB293C183100C0B37F /* HTMLConverter.swift in Sources */,
|
D61F75BB293C183100C0B37F /* HTMLConverter.swift in Sources */,
|
||||||
D61F75A5293ABD6F00C0B37F /* EditFilterView.swift in Sources */,
|
D61F75A5293ABD6F00C0B37F /* EditFilterView.swift in Sources */,
|
||||||
D6BC9DB1232C61BC002CA326 /* NotificationsPageViewController.swift in Sources */,
|
D6BC9DB1232C61BC002CA326 /* NotificationsPageViewController.swift in Sources */,
|
||||||
|
@ -1964,7 +2020,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 */,
|
||||||
|
@ -1973,6 +2028,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 */,
|
||||||
|
@ -1982,6 +2038,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 */,
|
||||||
|
@ -2002,9 +2059,11 @@
|
||||||
D6E77D0F286F773900D8B732 /* SplitNavigationController.swift in Sources */,
|
D6E77D0F286F773900D8B732 /* SplitNavigationController.swift in Sources */,
|
||||||
D6B30E09254BAF63009CAEE5 /* ImageGrayscalifier.swift in Sources */,
|
D6B30E09254BAF63009CAEE5 /* ImageGrayscalifier.swift in Sources */,
|
||||||
D6A6C10F25B62D2400298D0F /* DiskCache.swift in Sources */,
|
D6A6C10F25B62D2400298D0F /* DiskCache.swift in Sources */,
|
||||||
|
D6C3F4F9298EDBF20009FCFF /* ConversationTree.swift in Sources */,
|
||||||
D6B81F3C2560365300F6E31D /* RefreshableViewController.swift in Sources */,
|
D6B81F3C2560365300F6E31D /* RefreshableViewController.swift in Sources */,
|
||||||
D646C958213B367000269FB5 /* LargeImageShrinkAnimationController.swift in Sources */,
|
D646C958213B367000269FB5 /* LargeImageShrinkAnimationController.swift in Sources */,
|
||||||
D6674AEA23341F7600E8DF94 /* AppShortcutItems.swift in Sources */,
|
D6674AEA23341F7600E8DF94 /* AppShortcutItems.swift in Sources */,
|
||||||
|
D6DD8FFF2984D327002AD3FD /* BookmarksViewController.swift in Sources */,
|
||||||
D646C956213B365700269FB5 /* LargeImageExpandAnimationController.swift in Sources */,
|
D646C956213B365700269FB5 /* LargeImageExpandAnimationController.swift in Sources */,
|
||||||
D667E5F82135C3040057A976 /* Mastodon+Equatable.swift in Sources */,
|
D667E5F82135C3040057A976 /* Mastodon+Equatable.swift in Sources */,
|
||||||
D6B4A4FF2506B81A000C81C1 /* AccountDisplayNameLabel.swift in Sources */,
|
D6B4A4FF2506B81A000C81C1 /* AccountDisplayNameLabel.swift in Sources */,
|
||||||
|
@ -2022,6 +2081,7 @@
|
||||||
D673ACCE2919E74200D6F8B0 /* MenuPicker.swift in Sources */,
|
D673ACCE2919E74200D6F8B0 /* MenuPicker.swift in Sources */,
|
||||||
D6370B9C24421FF30092A7FF /* Tusker.xcdatamodeld in Sources */,
|
D6370B9C24421FF30092A7FF /* Tusker.xcdatamodeld in Sources */,
|
||||||
D601FA83297EEC3F00A8E8B5 /* SuggestedProfileCardCollectionViewCell.swift in Sources */,
|
D601FA83297EEC3F00A8E8B5 /* SuggestedProfileCardCollectionViewCell.swift in Sources */,
|
||||||
|
D691772E29AA5D420054D7EF /* UserActivityHandlingContext.swift in Sources */,
|
||||||
04ED00B121481ED800567C53 /* SteppedProgressView.swift in Sources */,
|
04ED00B121481ED800567C53 /* SteppedProgressView.swift in Sources */,
|
||||||
D677284C24ECBE9100C732D3 /* ComposeAvatarImageView.swift in Sources */,
|
D677284C24ECBE9100C732D3 /* ComposeAvatarImageView.swift in Sources */,
|
||||||
D6A6C11B25B63CEE00298D0F /* MemoryCache.swift in Sources */,
|
D6A6C11B25B63CEE00298D0F /* MemoryCache.swift in Sources */,
|
||||||
|
@ -2031,13 +2091,16 @@
|
||||||
D65B4B6629773AE600DABDFB /* DeleteStatusService.swift in Sources */,
|
D65B4B6629773AE600DABDFB /* DeleteStatusService.swift in Sources */,
|
||||||
D61DC84628F498F200B82C6E /* Logging.swift in Sources */,
|
D61DC84628F498F200B82C6E /* Logging.swift in Sources */,
|
||||||
D6B17255254F88B800128392 /* OppositeCollapseKeywordsView.swift in Sources */,
|
D6B17255254F88B800128392 /* OppositeCollapseKeywordsView.swift in Sources */,
|
||||||
|
D600891B29848289005B4D00 /* PinnedTimeline.swift in Sources */,
|
||||||
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 */,
|
||||||
D61DC84D28F500D200B82C6E /* ProfileViewController.swift in Sources */,
|
D61DC84D28F500D200B82C6E /* ProfileViewController.swift in Sources */,
|
||||||
|
D600891F29848DE2005B4D00 /* AddInstancePinnedTimelineView.swift in Sources */,
|
||||||
D663626421360D2300C9CBA2 /* AvatarStyle.swift in Sources */,
|
D663626421360D2300C9CBA2 /* AvatarStyle.swift in Sources */,
|
||||||
D693A72A25CF8C1E003A14E2 /* ProfileDirectoryViewController.swift in Sources */,
|
D693A72A25CF8C1E003A14E2 /* ProfileDirectoryViewController.swift in Sources */,
|
||||||
D693A72F25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift in Sources */,
|
D693A72F25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift in Sources */,
|
||||||
|
@ -2054,7 +2117,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 */,
|
||||||
D627944A23A6AD6100D38C68 /* BookmarksTableViewController.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 */,
|
||||||
|
@ -2071,6 +2134,7 @@
|
||||||
D64AAE9526C88C5000FC57FB /* ToastableViewController.swift in Sources */,
|
D64AAE9526C88C5000FC57FB /* ToastableViewController.swift in Sources */,
|
||||||
D6895DE928D962C2006341DA /* TimelineLikeController.swift in Sources */,
|
D6895DE928D962C2006341DA /* TimelineLikeController.swift in Sources */,
|
||||||
D6A3A3822956123A0036B6EF /* TimelinePosition.swift in Sources */,
|
D6A3A3822956123A0036B6EF /* TimelinePosition.swift in Sources */,
|
||||||
|
D691771529A6FCAB0054D7EF /* StateRestorableViewController.swift in Sources */,
|
||||||
D6E0DC8E216EDF1E00369478 /* Previewing.swift in Sources */,
|
D6E0DC8E216EDF1E00369478 /* Previewing.swift in Sources */,
|
||||||
D6B93667281D937300237D0E /* MainSidebarMyProfileCollectionViewCell.swift in Sources */,
|
D6B93667281D937300237D0E /* MainSidebarMyProfileCollectionViewCell.swift in Sources */,
|
||||||
D61F75BD293D099600C0B37F /* Lazy.swift in Sources */,
|
D61F75BD293D099600C0B37F /* Lazy.swift in Sources */,
|
||||||
|
@ -2102,6 +2166,7 @@
|
||||||
D663626221360B1900C9CBA2 /* Preferences.swift in Sources */,
|
D663626221360B1900C9CBA2 /* Preferences.swift in Sources */,
|
||||||
D626493823C0FD0000612E6E /* AllPhotosTableViewCell.swift in Sources */,
|
D626493823C0FD0000612E6E /* AllPhotosTableViewCell.swift in Sources */,
|
||||||
D627943B23A55BA600D38C68 /* NavigableTableViewCell.swift in Sources */,
|
D627943B23A55BA600D38C68 /* NavigableTableViewCell.swift in Sources */,
|
||||||
|
D6D9498F298EB79400C59229 /* CopyableLable.swift in Sources */,
|
||||||
D6333B792139AEFD00CE884A /* Date+TimeAgo.swift in Sources */,
|
D6333B792139AEFD00CE884A /* Date+TimeAgo.swift in Sources */,
|
||||||
D6D706A029466649000827ED /* ScrollingSegmentedControl.swift in Sources */,
|
D6D706A029466649000827ED /* ScrollingSegmentedControl.swift in Sources */,
|
||||||
D653F411267D1E32004E32B1 /* DiffableTimelineLikeTableViewController.swift in Sources */,
|
D653F411267D1E32004E32B1 /* DiffableTimelineLikeTableViewController.swift in Sources */,
|
||||||
|
@ -2132,6 +2197,7 @@
|
||||||
D6D706A72948D4D0000827ED /* TimlineState.swift in Sources */,
|
D6D706A72948D4D0000827ED /* TimlineState.swift in Sources */,
|
||||||
D66C900B28DAB7FD00217BF2 /* TimelineViewController.swift in Sources */,
|
D66C900B28DAB7FD00217BF2 /* TimelineViewController.swift in Sources */,
|
||||||
D61F758D2933C69C00C0B37F /* StatusUpdatedNotificationTableViewCell.swift in Sources */,
|
D61F758D2933C69C00C0B37F /* StatusUpdatedNotificationTableViewCell.swift in Sources */,
|
||||||
|
D6C3F4F7298ED7F70009FCFF /* FavoritesViewController.swift in Sources */,
|
||||||
D6AEBB432321685E00E5038B /* OpenInSafariActivity.swift in Sources */,
|
D6AEBB432321685E00E5038B /* OpenInSafariActivity.swift in Sources */,
|
||||||
D6C693FE2162FEEA007D6A6D /* UIViewController+Children.swift in Sources */,
|
D6C693FE2162FEEA007D6A6D /* UIViewController+Children.swift in Sources */,
|
||||||
D61A45EA28DF51EE002BE511 /* TimelineLikeCollectionViewController.swift in Sources */,
|
D61A45EA28DF51EE002BE511 /* TimelineLikeCollectionViewController.swift in Sources */,
|
||||||
|
@ -2147,7 +2213,9 @@
|
||||||
D6E77D0B286D426E00D8B732 /* TrendingLinkCardCollectionViewCell.swift in Sources */,
|
D6E77D0B286D426E00D8B732 /* TrendingLinkCardCollectionViewCell.swift in Sources */,
|
||||||
D6114E1127F899B30080E273 /* TrendingLinksViewController.swift in Sources */,
|
D6114E1127F899B30080E273 /* TrendingLinksViewController.swift in Sources */,
|
||||||
D65B4B58297203A700DABDFB /* ReportSelectRulesView.swift in Sources */,
|
D65B4B58297203A700DABDFB /* ReportSelectRulesView.swift in Sources */,
|
||||||
|
D6C3F4FB299035650009FCFF /* TrendsViewController.swift in Sources */,
|
||||||
D6B81F442560390300F6E31D /* MenuController.swift in Sources */,
|
D6B81F442560390300F6E31D /* MenuController.swift in Sources */,
|
||||||
|
D691771129A2B76A0054D7EF /* MainActor+Unsafe.swift in Sources */,
|
||||||
D65B4B542971F71D00DABDFB /* EditedReport.swift in Sources */,
|
D65B4B542971F71D00DABDFB /* EditedReport.swift in Sources */,
|
||||||
D65234E12561AA68001AF9CF /* NotificationsTableViewController.swift in Sources */,
|
D65234E12561AA68001AF9CF /* NotificationsTableViewController.swift in Sources */,
|
||||||
D6A6C11525B62E9700298D0F /* CacheExpiry.swift in Sources */,
|
D6A6C11525B62E9700298D0F /* CacheExpiry.swift in Sources */,
|
||||||
|
@ -2168,6 +2236,7 @@
|
||||||
D621733328F1D5ED004C7DB1 /* ReblogService.swift in Sources */,
|
D621733328F1D5ED004C7DB1 /* ReblogService.swift in Sources */,
|
||||||
D62275A824F1CA2800B82A16 /* ComposeReplyContentView.swift in Sources */,
|
D62275A824F1CA2800B82A16 /* ComposeReplyContentView.swift in Sources */,
|
||||||
D61F75AF293AF50C00C0B37F /* EditedFilter.swift in Sources */,
|
D61F75AF293AF50C00C0B37F /* EditedFilter.swift in Sources */,
|
||||||
|
D691771929A7B8820054D7EF /* ProfileHeaderMovedOverlayView.swift in Sources */,
|
||||||
D61F75B9293C15A000C0B37F /* ZeroHeightCollectionViewCell.swift in Sources */,
|
D61F75B9293C15A000C0B37F /* ZeroHeightCollectionViewCell.swift in Sources */,
|
||||||
D677284824ECBCB100C732D3 /* ComposeView.swift in Sources */,
|
D677284824ECBCB100C732D3 /* ComposeView.swift in Sources */,
|
||||||
D68232F72464F4FD00325FB8 /* ComposeDrawingViewController.swift in Sources */,
|
D68232F72464F4FD00325FB8 /* ComposeDrawingViewController.swift in Sources */,
|
||||||
|
@ -2203,11 +2272,11 @@
|
||||||
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 */,
|
||||||
D6D12B56292D57E800D528E1 /* AccountCollectionViewCell.swift in Sources */,
|
D6D12B56292D57E800D528E1 /* AccountCollectionViewCell.swift in Sources */,
|
||||||
D6A6C10525B6138A00298D0F /* StatusTablePrefetching.swift in Sources */,
|
|
||||||
D60088F22980DAA0005B4D00 /* TipJarView.swift in Sources */,
|
D60088F22980DAA0005B4D00 /* TipJarView.swift in Sources */,
|
||||||
D6C82B4125C5BB7E0017F1E6 /* ExploreViewController.swift in Sources */,
|
D6C82B4125C5BB7E0017F1E6 /* ExploreViewController.swift in Sources */,
|
||||||
D6B053A423BD2C8100A066FA /* AssetCollectionsListViewController.swift in Sources */,
|
D6B053A423BD2C8100A066FA /* AssetCollectionsListViewController.swift in Sources */,
|
||||||
|
@ -2228,6 +2297,7 @@
|
||||||
D61F75A129396DE200C0B37F /* SemiCaseSensitiveComparatorTests.swift in Sources */,
|
D61F75A129396DE200C0B37F /* SemiCaseSensitiveComparatorTests.swift in Sources */,
|
||||||
D62FF04823D7CDD700909D6E /* AttributedStringHelperTests.swift in Sources */,
|
D62FF04823D7CDD700909D6E /* AttributedStringHelperTests.swift in Sources */,
|
||||||
D6E426AD25334DA500C02E1C /* FuzzyMatcherTests.swift in Sources */,
|
D6E426AD25334DA500C02E1C /* FuzzyMatcherTests.swift in Sources */,
|
||||||
|
D600891D298482F0005B4D00 /* PinnedTimelineTests.swift in Sources */,
|
||||||
D6114E1727F8BB210080E273 /* VersionTests.swift in Sources */,
|
D6114E1727F8BB210080E273 /* VersionTests.swift in Sources */,
|
||||||
D6CA8CDA2962231F0050C433 /* ArrayUniqueTests.swift in Sources */,
|
D6CA8CDA2962231F0050C433 /* ArrayUniqueTests.swift in Sources */,
|
||||||
D6D4DDE5212518A200E1C4BB /* TuskerTests.swift in Sources */,
|
D6D4DDE5212518A200E1C4BB /* TuskerTests.swift in Sources */,
|
||||||
|
@ -2369,10 +2439,11 @@
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
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 = 69;
|
CURRENT_PROJECT_VERSION = 76;
|
||||||
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;
|
||||||
|
@ -2380,11 +2451,12 @@
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 2023.3;
|
MARKETING_VERSION = 2023.4;
|
||||||
OTHER_CODE_SIGN_FLAGS = "";
|
OTHER_CODE_SIGN_FLAGS = "";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).Tusker";
|
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).Tusker";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SUPPORTS_MACCATALYST = YES;
|
SUPPORTS_MACCATALYST = YES;
|
||||||
|
SWIFT_OBJC_BRIDGING_HEADER = "Tusker/Tusker-Bridging-Header.h";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = "1,2,6";
|
TARGETED_DEVICE_FAMILY = "1,2,6";
|
||||||
};
|
};
|
||||||
|
@ -2437,7 +2509,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 = 69;
|
CURRENT_PROJECT_VERSION = 76;
|
||||||
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;
|
||||||
|
@ -2446,7 +2518,7 @@
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 2023.3;
|
MARKETING_VERSION = 2023.4;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).Tusker.OpenInTusker";
|
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).Tusker.OpenInTusker";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
|
@ -2585,10 +2657,11 @@
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
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 = 69;
|
CURRENT_PROJECT_VERSION = 76;
|
||||||
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;
|
||||||
|
@ -2596,13 +2669,15 @@
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 2023.3;
|
MARKETING_VERSION = 2023.4;
|
||||||
OTHER_CODE_SIGN_FLAGS = "";
|
OTHER_CODE_SIGN_FLAGS = "";
|
||||||
OTHER_LDFLAGS = "";
|
OTHER_LDFLAGS = "";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).Tusker";
|
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).Tusker";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SUPPORTS_MACCATALYST = YES;
|
SUPPORTS_MACCATALYST = YES;
|
||||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||||
|
SWIFT_OBJC_BRIDGING_HEADER = "Tusker/Tusker-Bridging-Header.h";
|
||||||
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = "1,2,6";
|
TARGETED_DEVICE_FAMILY = "1,2,6";
|
||||||
};
|
};
|
||||||
|
@ -2613,10 +2688,11 @@
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
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 = 69;
|
CURRENT_PROJECT_VERSION = 76;
|
||||||
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;
|
||||||
|
@ -2624,11 +2700,12 @@
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 2023.3;
|
MARKETING_VERSION = 2023.4;
|
||||||
OTHER_CODE_SIGN_FLAGS = "";
|
OTHER_CODE_SIGN_FLAGS = "";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).Tusker";
|
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).Tusker";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SUPPORTS_MACCATALYST = YES;
|
SUPPORTS_MACCATALYST = YES;
|
||||||
|
SWIFT_OBJC_BRIDGING_HEADER = "Tusker/Tusker-Bridging-Header.h";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = "1,2,6";
|
TARGETED_DEVICE_FAMILY = "1,2,6";
|
||||||
};
|
};
|
||||||
|
@ -2721,7 +2798,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 = 69;
|
CURRENT_PROJECT_VERSION = 76;
|
||||||
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;
|
||||||
|
@ -2730,7 +2807,7 @@
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 2023.3;
|
MARKETING_VERSION = 2023.4;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).Tusker.OpenInTusker";
|
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).Tusker.OpenInTusker";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
|
@ -2747,7 +2824,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 = 69;
|
CURRENT_PROJECT_VERSION = 76;
|
||||||
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;
|
||||||
|
@ -2756,7 +2833,7 @@
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 2023.3;
|
MARKETING_VERSION = 2023.4;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).Tusker.OpenInTusker";
|
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).Tusker.OpenInTusker";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
|
@ -2835,7 +2912,7 @@
|
||||||
repositoryURL = "https://github.com/getsentry/sentry-cocoa.git";
|
repositoryURL = "https://github.com/getsentry/sentry-cocoa.git";
|
||||||
requirement = {
|
requirement = {
|
||||||
kind = upToNextMinorVersion;
|
kind = upToNextMinorVersion;
|
||||||
minimumVersion = 7.29.0;
|
minimumVersion = 8.0.0;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
D6552365289870790048A653 /* XCRemoteSwiftPackageReference "ScreenCorners" */ = {
|
D6552365289870790048A653 /* XCRemoteSwiftPackageReference "ScreenCorners" */ = {
|
||||||
|
|
|
@ -13,11 +13,11 @@ import Pachyderm
|
||||||
class CreateListService {
|
class CreateListService {
|
||||||
private let mastodonController: MastodonController
|
private let mastodonController: MastodonController
|
||||||
private let present: (UIViewController) -> Void
|
private let present: (UIViewController) -> Void
|
||||||
private let didCreateList: (@MainActor (List) -> Void)?
|
private let didCreateList: (@MainActor (List) async -> Void)?
|
||||||
|
|
||||||
private var createAction: UIAlertAction?
|
private var createAction: UIAlertAction?
|
||||||
|
|
||||||
init(mastodonController: MastodonController, present: @escaping (UIViewController) -> Void, didCreateList: (@MainActor (List) -> Void)?) {
|
init(mastodonController: MastodonController, present: @escaping (UIViewController) -> Void, didCreateList: (@MainActor (List) async -> Void)?) {
|
||||||
self.mastodonController = mastodonController
|
self.mastodonController = mastodonController
|
||||||
self.present = present
|
self.present = present
|
||||||
self.didCreateList = didCreateList
|
self.didCreateList = didCreateList
|
||||||
|
@ -50,7 +50,7 @@ class CreateListService {
|
||||||
let request = Client.createList(title: title)
|
let request = Client.createList(title: title)
|
||||||
let (list, _) = try await mastodonController.run(request)
|
let (list, _) = try await mastodonController.run(request)
|
||||||
mastodonController.addedList(list)
|
mastodonController.addedList(list)
|
||||||
self.didCreateList?(list)
|
await self.didCreateList?(list)
|
||||||
} catch {
|
} catch {
|
||||||
let alert = UIAlertController(title: "Error Creating List", message: error.localizedDescription, preferredStyle: .alert)
|
let alert = UIAlertController(title: "Error Creating List", message: error.localizedDescription, preferredStyle: .alert)
|
||||||
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
|
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
|
||||||
|
|
|
@ -48,7 +48,7 @@ class DeleteListService {
|
||||||
|
|
||||||
private func deleteList() async {
|
private func deleteList() async {
|
||||||
do {
|
do {
|
||||||
let request = List.delete(list)
|
let request = List.delete(list.id)
|
||||||
_ = try await mastodonController.run(request)
|
_ = try await mastodonController.run(request)
|
||||||
mastodonController.deletedList(list)
|
mastodonController.deletedList(list)
|
||||||
} catch {
|
} catch {
|
||||||
|
|
|
@ -101,6 +101,10 @@ struct InstanceFeatures {
|
||||||
hasMastodonVersion(3, 5, 0)
|
hasMastodonVersion(3, 5, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var pollVotersCount: Bool {
|
||||||
|
instanceType.isMastodon
|
||||||
|
}
|
||||||
|
|
||||||
mutating func update(instance: Instance, nodeInfo: NodeInfo?) {
|
mutating func update(instance: Instance, nodeInfo: NodeInfo?) {
|
||||||
let ver = instance.version.lowercased()
|
let ver = instance.version.lowercased()
|
||||||
if ver.contains("glitch") {
|
if ver.contains("glitch") {
|
||||||
|
@ -269,5 +273,5 @@ private func setInstanceBreadcrumb(instance: Instance, nodeInfo: NodeInfo?) {
|
||||||
"version": nodeInfo.software.version,
|
"version": nodeInfo.software.version,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
SentrySDK.addBreadcrumb(crumb: crumb)
|
SentrySDK.addBreadcrumb(crumb)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
//
|
||||||
|
// LogoutService.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 1/27/23.
|
||||||
|
// Copyright © 2023 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
class LogoutService {
|
||||||
|
let accountInfo: LocalData.UserAccountInfo
|
||||||
|
private let mastodonController: MastodonController
|
||||||
|
|
||||||
|
init(accountInfo: LocalData.UserAccountInfo) {
|
||||||
|
self.accountInfo = accountInfo
|
||||||
|
self.mastodonController = MastodonController.getForAccount(accountInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
func run() {
|
||||||
|
Task.detached {
|
||||||
|
try? await self.mastodonController.client.revokeAccessToken()
|
||||||
|
}
|
||||||
|
MastodonController.removeForAccount(accountInfo)
|
||||||
|
LocalData.shared.removeAccount(accountInfo)
|
||||||
|
let psc = mastodonController.persistentContainer.persistentStoreCoordinator
|
||||||
|
for store in psc.persistentStores {
|
||||||
|
guard let url = store.url else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
try? psc.destroyPersistentStore(at: url, type: .sqlite)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -31,12 +31,16 @@ class MastodonController: ObservableObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func removeForAccount(_ account: LocalData.UserAccountInfo) {
|
||||||
|
all.removeValue(forKey: account)
|
||||||
|
}
|
||||||
|
|
||||||
static func resetAll() {
|
static func resetAll() {
|
||||||
all = [:]
|
all = [:]
|
||||||
}
|
}
|
||||||
|
|
||||||
private let transient: Bool
|
private let transient: Bool
|
||||||
private(set) lazy var persistentContainer = MastodonCachePersistentStore(for: accountInfo, transient: transient)
|
private(set) nonisolated lazy var persistentContainer = MastodonCachePersistentStore(for: accountInfo, transient: transient)
|
||||||
|
|
||||||
let instanceURL: URL
|
let instanceURL: URL
|
||||||
var accountInfo: LocalData.UserAccountInfo?
|
var accountInfo: LocalData.UserAccountInfo?
|
||||||
|
@ -106,7 +110,7 @@ class MastodonController: ObservableObject {
|
||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
func run<Result>(_ request: Request<Result>) async throws -> (Result, Pagination?) {
|
func run<Result: Sendable>(_ request: Request<Result>) async throws -> (Result, Pagination?) {
|
||||||
let response = await runResponse(request)
|
let response = await runResponse(request)
|
||||||
try Task.checkCancellation()
|
try Task.checkCancellation()
|
||||||
switch response {
|
switch response {
|
||||||
|
@ -162,6 +166,8 @@ class MastodonController: ObservableObject {
|
||||||
|
|
||||||
loadAccountPreferences()
|
loadAccountPreferences()
|
||||||
|
|
||||||
|
lists = loadCachedLists()
|
||||||
|
|
||||||
NotificationCenter.default.publisher(for: .NSPersistentStoreRemoteChange, object: persistentContainer.persistentStoreCoordinator)
|
NotificationCenter.default.publisher(for: .NSPersistentStoreRemoteChange, object: persistentContainer.persistentStoreCoordinator)
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { [unowned self] _ in
|
.sink { [unowned self] _ in
|
||||||
|
@ -177,7 +183,7 @@ class MastodonController: ObservableObject {
|
||||||
_ = try await (ownAccount, ownInstance)
|
_ = try await (ownAccount, ownInstance)
|
||||||
|
|
||||||
loadLists()
|
loadLists()
|
||||||
async let _ = await loadFilters()
|
_ = await loadFilters()
|
||||||
} catch {
|
} catch {
|
||||||
Logging.general.error("MastodonController initialization failed: \(String(describing: error))")
|
Logging.general.error("MastodonController initialization failed: \(String(describing: error))")
|
||||||
}
|
}
|
||||||
|
@ -359,6 +365,23 @@ class MastodonController: ObservableObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func loadCachedLists() -> [List] {
|
||||||
|
let req = ListMO.fetchRequest()
|
||||||
|
guard let lists = try? persistentContainer.viewContext.fetch(req) else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return lists.map {
|
||||||
|
List(id: $0.id, title: $0.title)
|
||||||
|
}.sorted(using: SemiCaseSensitiveComparator.keyPath(\.title))
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCachedList(id: String) -> List? {
|
||||||
|
let req = ListMO.fetchRequest(id: id)
|
||||||
|
return (try? persistentContainer.viewContext.fetch(req).first).flatMap {
|
||||||
|
List(id: $0.id, title: $0.title)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
func addedList(_ list: List) {
|
func addedList(_ list: List) {
|
||||||
var new = self.lists
|
var new = self.lists
|
||||||
|
|
|
@ -11,13 +11,13 @@ import Pachyderm
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
class RenameListService {
|
class RenameListService {
|
||||||
private let list: List
|
private let list: ListProtocol
|
||||||
private let mastodonController: MastodonController
|
private let mastodonController: MastodonController
|
||||||
private let present: (UIViewController) -> Void
|
private let present: (UIViewController) -> Void
|
||||||
|
|
||||||
private var renameAction: UIAlertAction?
|
private var renameAction: UIAlertAction?
|
||||||
|
|
||||||
init(list: List, mastodonController: MastodonController, present: @escaping (UIViewController) -> Void) {
|
init(list: ListProtocol, mastodonController: MastodonController, present: @escaping (UIViewController) -> Void) {
|
||||||
self.list = list
|
self.list = list
|
||||||
self.mastodonController = mastodonController
|
self.mastodonController = mastodonController
|
||||||
self.present = present
|
self.present = present
|
||||||
|
@ -47,7 +47,7 @@ class RenameListService {
|
||||||
|
|
||||||
private func updateList(with title: String) async {
|
private func updateList(with title: String) async {
|
||||||
do {
|
do {
|
||||||
let req = List.update(list, title: title)
|
let req = List.update(list.id, title: title)
|
||||||
let (list, _) = try await mastodonController.run(req)
|
let (list, _) = try await mastodonController.run(req)
|
||||||
mastodonController.renamedList(list)
|
mastodonController.renamedList(list)
|
||||||
} catch {
|
} catch {
|
||||||
|
|
|
@ -39,9 +39,9 @@ class OpenInSafariActivity: UIActivity {
|
||||||
static func completionHandler(navigator: TuskerNavigationDelegate, url: URL) -> UIActivityViewController.CompletionWithItemsHandler {
|
static func completionHandler(navigator: TuskerNavigationDelegate, url: URL) -> UIActivityViewController.CompletionWithItemsHandler {
|
||||||
return { (activityType, _, _, _) in
|
return { (activityType, _, _, _) in
|
||||||
if activityType == .openInSafari {
|
if activityType == .openInSafari {
|
||||||
let vc = SFSafariViewController(url: url)
|
MainActor.runUnsafely {
|
||||||
vc.preferredControlTintColor = Preferences.shared.accentColor.color
|
navigator.selected(url: url, allowResolveStatuses: false, allowUniversalLinks: false)
|
||||||
navigator.show(vc)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,10 @@ class StatusActivityItemSource: NSObject, UIActivityItemSource {
|
||||||
}
|
}
|
||||||
|
|
||||||
func activityViewControllerLinkMetadata(_ activityViewController: UIActivityViewController) -> LPLinkMetadata? {
|
func activityViewControllerLinkMetadata(_ activityViewController: UIActivityViewController) -> LPLinkMetadata? {
|
||||||
|
guard #unavailable(iOS 16.4) else {
|
||||||
|
// iOS 16.4 shows the full content and attachments in the Messages preview, better than what we can generate with LPLinkMetadata
|
||||||
|
return nil
|
||||||
|
}
|
||||||
let metadata = LPLinkMetadata()
|
let metadata = LPLinkMetadata()
|
||||||
metadata.originalURL = status.url!
|
metadata.originalURL = status.url!
|
||||||
metadata.url = status.url!
|
metadata.url = status.url!
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
@ -66,13 +67,14 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||||
options.enableSwizzling = false
|
options.enableSwizzling = false
|
||||||
// required to support releases/release health
|
// required to support releases/release health
|
||||||
options.enableAutoSessionTracking = true
|
options.enableAutoSessionTracking = true
|
||||||
options.enableOutOfMemoryTracking = false
|
options.enableWatchdogTerminationTracking = false
|
||||||
options.enableAutoPerformanceTracking = false
|
options.enableAutoPerformanceTracing = false
|
||||||
options.enableNetworkTracking = false
|
options.enableNetworkTracking = false
|
||||||
options.enableAppHangTracking = false
|
options.enableAppHangTracking = false
|
||||||
options.enableCoreDataTracking = false
|
options.enableCoreDataTracing = false
|
||||||
// we don't care about events like battery, keyboard show/hide
|
// we don't care about events like battery, keyboard show/hide
|
||||||
options.enableAutoBreadcrumbTracking = false
|
options.enableAutoBreadcrumbTracking = false
|
||||||
|
options.enableUserInteractionTracing = false
|
||||||
|
|
||||||
options.beforeSend = { event in
|
options.beforeSend = { event in
|
||||||
// just no, why would anyone need this information
|
// just no, why would anyone need this information
|
||||||
|
@ -135,6 +137,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||||
var originalIMP: IMP?
|
var originalIMP: IMP?
|
||||||
let imp = imp_implementationWithBlock({ (self: UIStatusBarManager, sender: AnyObject) in
|
let imp = imp_implementationWithBlock({ (self: UIStatusBarManager, sender: AnyObject) in
|
||||||
let original = unsafeBitCast(originalIMP!, to: (@convention(c) (UIStatusBarManager, Selector, AnyObject) -> Void).self)
|
let original = unsafeBitCast(originalIMP!, to: (@convention(c) (UIStatusBarManager, Selector, AnyObject) -> Void).self)
|
||||||
|
let exception = catchNSException {
|
||||||
guard let windowScene = self.perform(Selector(("windowScene"))).takeUnretainedValue() as? UIWindowScene,
|
guard let windowScene = self.perform(Selector(("windowScene"))).takeUnretainedValue() as? UIWindowScene,
|
||||||
let xPosition = sender.value(forKey: "xPosition") as? CGFloat,
|
let xPosition = sender.value(forKey: "xPosition") as? CGFloat,
|
||||||
let delegate = windowScene.delegate as? TuskerSceneDelegate else {
|
let delegate = windowScene.delegate as? TuskerSceneDelegate else {
|
||||||
|
@ -147,10 +150,34 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||||
case .continue:
|
case .continue:
|
||||||
original(self, selector, sender)
|
original(self, selector, sender)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if let exception {
|
||||||
|
SentrySDK.capture(exception: exception)
|
||||||
|
}
|
||||||
} as @convention(block) (UIStatusBarManager, AnyObject) -> Void)
|
} as @convention(block) (UIStatusBarManager, AnyObject) -> Void)
|
||||||
originalIMP = class_replaceMethod(UIStatusBarManager.self, selector, imp, "v@:@")
|
originalIMP = class_replaceMethod(UIStatusBarManager.self, selector, imp, "v@:@")
|
||||||
if originalIMP == nil {
|
if originalIMP == nil {
|
||||||
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,46 +32,38 @@ class ImageCache {
|
||||||
}
|
}
|
||||||
|
|
||||||
func get(_ url: URL, loadOriginal: Bool = false, completion: ((Data?, UIImage?) -> Void)?) -> Request? {
|
func get(_ url: URL, loadOriginal: Bool = false, completion: ((Data?, UIImage?) -> Void)?) -> Request? {
|
||||||
let key = url.absoluteString
|
|
||||||
|
|
||||||
let wrappedCompletion: ((Data?, UIImage?) -> Void)?
|
|
||||||
if let completion = completion {
|
|
||||||
wrappedCompletion = { (data, image) in
|
|
||||||
if let image {
|
|
||||||
if !loadOriginal,
|
|
||||||
let size = self.desiredPixelSize {
|
|
||||||
image.prepareThumbnail(of: size) {
|
|
||||||
completion(data, $0)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
image.prepareForDisplay {
|
|
||||||
completion(data, $0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
completion(data, image)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
wrappedCompletion = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if !ImageCache.disableCaching,
|
if !ImageCache.disableCaching,
|
||||||
let entry = try? cache.get(key, loadOriginal: loadOriginal) {
|
let entry = try? cache.get(url.absoluteString, loadOriginal: loadOriginal) {
|
||||||
wrappedCompletion?(entry.data, entry.image)
|
completion?(entry.data, entry.image)
|
||||||
return nil
|
return nil
|
||||||
} else {
|
} else {
|
||||||
let task = dataTask(url: url, completion: wrappedCompletion)
|
return Task.detached(priority: .userInitiated) {
|
||||||
task.resume()
|
let result = await self.fetch(url: url)
|
||||||
return task
|
switch result {
|
||||||
|
case .data(let data):
|
||||||
|
completion?(data, nil)
|
||||||
|
case .dataAndImage(let data, let image):
|
||||||
|
completion?(data, image)
|
||||||
|
case .none:
|
||||||
|
completion?(nil, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func get(_ url: URL, loadOriginal: Bool = false) async -> (Data?, UIImage?) {
|
func get(_ url: URL, loadOriginal: Bool = false) async -> (Data?, UIImage?) {
|
||||||
// todo: this should integrate with the task cancellation mechanism somehow
|
if !ImageCache.disableCaching,
|
||||||
return await withCheckedContinuation { continuation in
|
let entry = try? cache.get(url.absoluteString, loadOriginal: loadOriginal) {
|
||||||
_ = get(url, loadOriginal: loadOriginal) { data, image in
|
return (entry.data, entry.image)
|
||||||
continuation.resume(returning: (data, image))
|
} else {
|
||||||
|
let result = await self.fetch(url: url)
|
||||||
|
switch result {
|
||||||
|
case .data(let data):
|
||||||
|
return (data, nil)
|
||||||
|
case .dataAndImage(let data, let image):
|
||||||
|
return (data, image)
|
||||||
|
case .none:
|
||||||
|
return (nil, nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -81,21 +73,28 @@ class ImageCache {
|
||||||
guard !ImageCache.disableCaching else { return }
|
guard !ImageCache.disableCaching else { return }
|
||||||
|
|
||||||
if !((try? cache.has(url.absoluteString)) ?? false) {
|
if !((try? cache.has(url.absoluteString)) ?? false) {
|
||||||
let task = dataTask(url: url, completion: nil)
|
Task.detached(priority: .medium) {
|
||||||
task.resume()
|
_ = await self.fetch(url: url)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func dataTask(url: URL, completion: ((Data?, UIImage?) -> Void)?) -> URLSessionDataTask {
|
private func fetch(url: URL) async -> FetchResult {
|
||||||
return URLSession.shared.dataTask(with: url) { data, response, error in
|
guard let (data, _) = try? await URLSession.shared.data(from: url) else {
|
||||||
guard error == nil,
|
return .none
|
||||||
let data else {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
let image = UIImage(data: data)
|
guard let image = UIImage(data: data) else {
|
||||||
try? self.cache.set(url.absoluteString, data: data, image: image)
|
try? cache.set(url.absoluteString, data: data, image: nil)
|
||||||
completion?(data, image)
|
return .data(data)
|
||||||
}
|
}
|
||||||
|
let preparedImage: UIImage?
|
||||||
|
if let desiredPixelSize {
|
||||||
|
preparedImage = await image.byPreparingThumbnail(ofSize: desiredPixelSize)
|
||||||
|
} else {
|
||||||
|
preparedImage = await image.byPreparingForDisplay()
|
||||||
|
}
|
||||||
|
try? cache.set(url.absoluteString, data: data, image: preparedImage ?? image)
|
||||||
|
return .dataAndImage(data, preparedImage ?? image)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getData(_ url: URL) -> Data? {
|
func getData(_ url: URL) -> Data? {
|
||||||
|
@ -114,6 +113,12 @@ class ImageCache {
|
||||||
return cache.disk?.getSizeInBytes()
|
return cache.disk?.getSizeInBytes()
|
||||||
}
|
}
|
||||||
|
|
||||||
typealias Request = URLSessionDataTask
|
typealias Request = Task<Void, Never>
|
||||||
|
|
||||||
|
enum FetchResult {
|
||||||
|
case data(Data)
|
||||||
|
case dataAndImage(Data, UIImage)
|
||||||
|
case none
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -77,6 +77,7 @@ class ImageDataCache {
|
||||||
try? disk?.removeAll()
|
try? disk?.removeAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: consider removing this and letting ImageCache just use the UIImage thumbnailing API
|
||||||
private func scaleImageIfDesired(data: Data) -> UIImage? {
|
private func scaleImageIfDesired(data: Data) -> UIImage? {
|
||||||
guard let desiredPixelSize = desiredPixelSize,
|
guard let desiredPixelSize = desiredPixelSize,
|
||||||
let source = CGImageSourceCreateWithData(data as CFData, [kCGImageSourceShouldCache: false] as CFDictionary) else {
|
let source = CGImageSourceCreateWithData(data as CFData, [kCGImageSourceShouldCache: false] as CFDictionary) else {
|
||||||
|
@ -84,14 +85,14 @@ class ImageDataCache {
|
||||||
}
|
}
|
||||||
|
|
||||||
let maxDimension = max(desiredPixelSize.width, desiredPixelSize.height)
|
let maxDimension = max(desiredPixelSize.width, desiredPixelSize.height)
|
||||||
let downsampleOptions = [
|
let downsampleOptions: [CFString: Any] = [
|
||||||
kCGImageSourceCreateThumbnailFromImageAlways: true,
|
kCGImageSourceCreateThumbnailFromImageAlways: true,
|
||||||
kCGImageSourceShouldCacheImmediately: true,
|
kCGImageSourceShouldCacheImmediately: true,
|
||||||
kCGImageSourceCreateThumbnailWithTransform: true,
|
kCGImageSourceCreateThumbnailWithTransform: true,
|
||||||
kCGImageSourceThumbnailMaxPixelSize: maxDimension
|
kCGImageSourceThumbnailMaxPixelSize: maxDimension
|
||||||
] as CFDictionary
|
]
|
||||||
|
|
||||||
if let downsampled = CGImageSourceCreateThumbnailAtIndex(source, 0, downsampleOptions) {
|
if let downsampled = CGImageSourceCreateThumbnailAtIndex(source, 0, downsampleOptions as CFDictionary) {
|
||||||
return UIImage(cgImage: downsampled)
|
return UIImage(cgImage: downsampled)
|
||||||
} else {
|
} else {
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -25,7 +25,7 @@ public final class AccountPreferences: NSManagedObject {
|
||||||
@NSManaged var pinnedTimelinesData: Data?
|
@NSManaged var pinnedTimelinesData: Data?
|
||||||
|
|
||||||
@LazilyDecoding(from: \AccountPreferences.pinnedTimelinesData, fallback: [])
|
@LazilyDecoding(from: \AccountPreferences.pinnedTimelinesData, fallback: [])
|
||||||
var pinnedTimelines: [Timeline]
|
var pinnedTimelines: [PinnedTimeline]
|
||||||
|
|
||||||
static func `default`(account: LocalData.UserAccountInfo, context: NSManagedObjectContext) -> AccountPreferences {
|
static func `default`(account: LocalData.UserAccountInfo, context: NSManagedObjectContext) -> AccountPreferences {
|
||||||
let prefs = AccountPreferences(context: context)
|
let prefs = AccountPreferences(context: context)
|
||||||
|
|
|
@ -11,7 +11,7 @@ import CoreData
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
|
|
||||||
@objc(ListMO)
|
@objc(ListMO)
|
||||||
public final class ListMO: NSManagedObject {
|
public final class ListMO: NSManagedObject, ListProtocol {
|
||||||
|
|
||||||
@nonobjc public class func fetchRequest() -> NSFetchRequest<ListMO> {
|
@nonobjc public class func fetchRequest() -> NSFetchRequest<ListMO> {
|
||||||
return NSFetchRequest(entityName: "List")
|
return NSFetchRequest(entityName: "List")
|
||||||
|
|
|
@ -211,7 +211,7 @@ class MastodonCachePersistentStore: NSPersistentCloudKitContainer {
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
SentrySDK.addBreadcrumb(crumb: crumb)
|
SentrySDK.addBreadcrumb(crumb)
|
||||||
fatalError("Unable to save managed object context: \(String(describing: error))")
|
fatalError("Unable to save managed object context: \(String(describing: error))")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -545,6 +545,8 @@ class MastodonCachePersistentStore: NSPersistentCloudKitContainer {
|
||||||
guard let timelinePosition = try? self.viewContext.existingObject(with: id) as? TimelinePosition else {
|
guard let timelinePosition = try? self.viewContext.existingObject(with: id) as? TimelinePosition else {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
// the kvo observer that clears the LazilyDecoding cache doesn't always fire on remote changes, so do it manually
|
||||||
|
timelinePosition.changedRemotely()
|
||||||
NotificationCenter.default.post(name: .timelinePositionChanged, object: timelinePosition)
|
NotificationCenter.default.post(name: .timelinePositionChanged, object: timelinePosition)
|
||||||
}
|
}
|
||||||
if changedAccountPrefs {
|
if changedAccountPrefs {
|
||||||
|
|
|
@ -41,6 +41,10 @@ public final class TimelinePosition: NSManagedObject {
|
||||||
self.createdAt = Date()
|
self.createdAt = Date()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func changedRemotely() {
|
||||||
|
_statusIDs.removeCachedValue()
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// blergh, this is the simplest way of getting the Timeline into a format that A) CoreData can handle and B) is usable in the predicate
|
// blergh, this is the simplest way of getting the Timeline into a format that A) CoreData can handle and B) is usable in the predicate
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
//
|
||||||
|
// MainActor+Unsafe.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 2/19/23.
|
||||||
|
// Copyright © 2023 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
/*
|
||||||
|
Copied from https://github.com/ChimeHQ/ConcurrencyPlus/blob/fe3b3fd5436b196d8c5211ab2cc4b69fc35524fe/Sources/ConcurrencyPlus/MainActor%2BUnsafe.swift
|
||||||
|
|
||||||
|
Copyright (c) 2022, Chime
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
1. Redistributions of source code must retain the above copyright notice, this
|
||||||
|
list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
3. Neither the name of the copyright holder nor the names of its
|
||||||
|
contributors may be used to endorse or promote products derived from
|
||||||
|
this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||||
|
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||||
|
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
public extension MainActor {
|
||||||
|
/// Execute the given body closure on the main actor without enforcing MainActor isolation.
|
||||||
|
///
|
||||||
|
/// This function exists to work around libraries with incorrect/inconsistent concurrency annotations. You should be **extremely** careful when using it, and only as a last resort.
|
||||||
|
///
|
||||||
|
/// It will crash if run on any non-main thread.
|
||||||
|
@MainActor(unsafe)
|
||||||
|
static func runUnsafely<T>(_ body: @MainActor () throws -> T) rethrows -> T {
|
||||||
|
dispatchPrecondition(condition: .onQueue(.main))
|
||||||
|
|
||||||
|
return try body()
|
||||||
|
}
|
||||||
|
}
|
|
@ -25,23 +25,4 @@ extension Timeline {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var image: UIImage {
|
|
||||||
switch self {
|
|
||||||
case .home:
|
|
||||||
return UIImage(systemName: "house.fill")!
|
|
||||||
case let .public(local):
|
|
||||||
if local {
|
|
||||||
return UIImage(systemName: "person.and.person.fill")!
|
|
||||||
} else {
|
|
||||||
return UIImage(systemName: "globe")!
|
|
||||||
}
|
|
||||||
case .list(id: _):
|
|
||||||
return UIImage(systemName: "list.bullet")!
|
|
||||||
case .tag(hashtag: _):
|
|
||||||
return UIImage(systemName: "number")!
|
|
||||||
case .direct:
|
|
||||||
return UIImage(systemName: "enveloep.fill")!
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -11,7 +11,7 @@ import Pachyderm
|
||||||
import Combine
|
import Combine
|
||||||
|
|
||||||
/// An opaque object that serves as the cache for the filtered-ness of a particular status.
|
/// An opaque object that serves as the cache for the filtered-ness of a particular status.
|
||||||
class FilterState {
|
class FilterState: @unchecked Sendable {
|
||||||
static var unknown: FilterState { FilterState(state: .unknown) }
|
static var unknown: FilterState { FilterState(state: .unknown) }
|
||||||
|
|
||||||
fileprivate var state: State
|
fileprivate var state: State
|
||||||
|
|
|
@ -55,7 +55,21 @@ struct HTMLConverter {
|
||||||
case let node as Element:
|
case let node as Element:
|
||||||
let attributed = NSMutableAttributedString(string: "", attributes: [.font: font, .foregroundColor: color])
|
let attributed = NSMutableAttributedString(string: "", attributes: [.font: font, .foregroundColor: color])
|
||||||
for child in node.getChildNodes() {
|
for child in node.getChildNodes() {
|
||||||
|
var appendEllipsis = false
|
||||||
|
if node.tagName() == "a",
|
||||||
|
let el = child as? Element {
|
||||||
|
if el.hasClass("invisible") {
|
||||||
|
continue
|
||||||
|
} else if el.hasClass("ellipsis") {
|
||||||
|
appendEllipsis = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
attributed.append(attributedTextForHTMLNode(child, usePreformattedText: usePreformattedText || node.tagName() == "pre"))
|
attributed.append(attributedTextForHTMLNode(child, usePreformattedText: usePreformattedText || node.tagName() == "pre"))
|
||||||
|
|
||||||
|
if appendEllipsis {
|
||||||
|
attributed.append(NSAttributedString("…"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
switch node.tagName() {
|
switch node.tagName() {
|
||||||
|
@ -120,6 +134,8 @@ struct HTMLConverter {
|
||||||
}
|
}
|
||||||
|
|
||||||
return attributed
|
return attributed
|
||||||
|
case is DataNode:
|
||||||
|
return NSAttributedString()
|
||||||
default:
|
default:
|
||||||
fatalError("Unexpected node type \(type(of: node))")
|
fatalError("Unexpected node type \(type(of: node))")
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ public struct LazilyDecoding<Enclosing: NSObject, Value: Codable> {
|
||||||
private let fallback: Value
|
private let fallback: Value
|
||||||
private var value: Value?
|
private var value: Value?
|
||||||
private var observation: NSKeyValueObservation?
|
private var observation: NSKeyValueObservation?
|
||||||
|
private var skipClearingOnNextUpdate = false
|
||||||
|
|
||||||
init(from keyPath: ReferenceWritableKeyPath<Enclosing, Data?>, fallback: Value) {
|
init(from keyPath: ReferenceWritableKeyPath<Enclosing, Data?>, fallback: Value) {
|
||||||
self.keyPath = keyPath
|
self.keyPath = keyPath
|
||||||
|
@ -37,13 +38,16 @@ public struct LazilyDecoding<Enclosing: NSObject, Value: Codable> {
|
||||||
} else {
|
} else {
|
||||||
guard let data = instance[keyPath: wrapper.keyPath] else { return wrapper.fallback }
|
guard let data = instance[keyPath: wrapper.keyPath] else { return wrapper.fallback }
|
||||||
do {
|
do {
|
||||||
let value = try decoder.decode(Box<Value>.self, from: data)
|
let value = try decoder.decode(Box.self, from: data)
|
||||||
wrapper.value = value.value
|
wrapper.value = value.value
|
||||||
wrapper.observation = instance.observe(wrapper.keyPath, changeHandler: { instance, _ in
|
wrapper.observation = instance.observe(wrapper.keyPath, changeHandler: { instance, _ in
|
||||||
var updated = instance[keyPath: storageKeyPath]
|
var wrapper = instance[keyPath: storageKeyPath]
|
||||||
updated.value = nil
|
if wrapper.skipClearingOnNextUpdate {
|
||||||
updated.observation = nil
|
wrapper.skipClearingOnNextUpdate = false
|
||||||
instance[keyPath: storageKeyPath] = updated
|
} else {
|
||||||
|
wrapper.removeCachedValue()
|
||||||
|
}
|
||||||
|
instance[keyPath: storageKeyPath] = wrapper
|
||||||
})
|
})
|
||||||
instance[keyPath: storageKeyPath] = wrapper
|
instance[keyPath: storageKeyPath] = wrapper
|
||||||
return value.value
|
return value.value
|
||||||
|
@ -55,12 +59,18 @@ public struct LazilyDecoding<Enclosing: NSObject, Value: Codable> {
|
||||||
set {
|
set {
|
||||||
var wrapper = instance[keyPath: storageKeyPath]
|
var wrapper = instance[keyPath: storageKeyPath]
|
||||||
wrapper.value = newValue
|
wrapper.value = newValue
|
||||||
|
wrapper.skipClearingOnNextUpdate = true
|
||||||
instance[keyPath: storageKeyPath] = wrapper
|
instance[keyPath: storageKeyPath] = wrapper
|
||||||
let newData = try! encoder.encode(Box(value: newValue))
|
let newData = try! encoder.encode(Box(value: newValue))
|
||||||
instance[keyPath: wrapper.keyPath] = newData
|
instance[keyPath: wrapper.keyPath] = newData
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mutating func removeCachedValue() {
|
||||||
|
value = nil
|
||||||
|
observation = nil
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension LazilyDecoding {
|
extension LazilyDecoding {
|
||||||
|
@ -72,7 +82,7 @@ extension LazilyDecoding {
|
||||||
extension LazilyDecoding {
|
extension LazilyDecoding {
|
||||||
// PropertyListEncoder only allows top-level types to be dicts or arrays, which breaks encoding nil-able values.
|
// PropertyListEncoder only allows top-level types to be dicts or arrays, which breaks encoding nil-able values.
|
||||||
// Wrapping everything in a Box ensures that it's always a dict.
|
// Wrapping everything in a Box ensures that it's always a dict.
|
||||||
private struct Box<T: Codable>: Codable {
|
struct Box: Codable {
|
||||||
let value: T
|
let value: Value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ import UIKit
|
||||||
struct MenuController {
|
struct MenuController {
|
||||||
|
|
||||||
static let composeCommand: UIKeyCommand = {
|
static let composeCommand: UIKeyCommand = {
|
||||||
return UIKeyCommand(title: "Compose", action: #selector(MainSplitViewController.presentCompose), input: "n", modifierFlags: .command)
|
return UIKeyCommand(title: "Compose", action: #selector(MainSplitViewController.handleComposeKeyCommand), input: "n", modifierFlags: .command)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
static func refreshCommand(discoverabilityTitle: String?) -> UIKeyCommand {
|
static func refreshCommand(discoverabilityTitle: String?) -> UIKeyCommand {
|
||||||
|
|
|
@ -0,0 +1,129 @@
|
||||||
|
//
|
||||||
|
// PinnedTimeline.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 1/27/23.
|
||||||
|
// Copyright © 2023 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import Pachyderm
|
||||||
|
|
||||||
|
enum PinnedTimeline: Codable, Equatable, Hashable {
|
||||||
|
case home
|
||||||
|
case `public`(local: Bool)
|
||||||
|
case tag(hashtag: String)
|
||||||
|
case list(id: String)
|
||||||
|
case instance(URL)
|
||||||
|
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
let type = try container.decode(String.self, forKey: .type)
|
||||||
|
switch type {
|
||||||
|
case "home":
|
||||||
|
self = .home
|
||||||
|
case "public":
|
||||||
|
self = .public(local: try container.decode(Bool.self, forKey: .local))
|
||||||
|
case "tag":
|
||||||
|
self = .tag(hashtag: try container.decode(String.self, forKey: .hashtag))
|
||||||
|
case "list":
|
||||||
|
self = .list(id: try container.decode(String.self, forKey: .listID))
|
||||||
|
case "instance":
|
||||||
|
self = .instance(try container.decode(URL.self, forKey: .instanceURL))
|
||||||
|
default:
|
||||||
|
throw DecodingError.dataCorruptedError(forKey: CodingKeys.type, in: container, debugDescription: "PinnedTimeline type must be one of 'home', 'local', 'tag', 'list', or 'instance'")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(to encoder: Encoder) throws {
|
||||||
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||||
|
switch self {
|
||||||
|
case .home:
|
||||||
|
try container.encode("home", forKey: .type)
|
||||||
|
case .public(let local):
|
||||||
|
try container.encode("public", forKey: .type)
|
||||||
|
try container.encode(local, forKey: .local)
|
||||||
|
case .tag(let hashtag):
|
||||||
|
try container.encode("tag", forKey: .type)
|
||||||
|
try container.encode(hashtag, forKey: .hashtag)
|
||||||
|
case .list(let id):
|
||||||
|
try container.encode("list", forKey: .type)
|
||||||
|
try container.encode(id, forKey: .listID)
|
||||||
|
case .instance(let url):
|
||||||
|
try container.encode("instance", forKey: .type)
|
||||||
|
try container.encode(url, forKey: .instanceURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init?(timeline: Timeline) {
|
||||||
|
switch timeline {
|
||||||
|
case .home:
|
||||||
|
self = .home
|
||||||
|
case .public(let local):
|
||||||
|
self = .public(local: local)
|
||||||
|
case .tag(let hashtag):
|
||||||
|
self = .tag(hashtag: hashtag)
|
||||||
|
case .list(let id):
|
||||||
|
self = .list(id: id)
|
||||||
|
case .direct:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var timeline: Timeline? {
|
||||||
|
switch self {
|
||||||
|
case .home:
|
||||||
|
return .home
|
||||||
|
case .public(let local):
|
||||||
|
return .public(local: local)
|
||||||
|
case .tag(let hashtag):
|
||||||
|
return .tag(hashtag: hashtag)
|
||||||
|
case .list(let id):
|
||||||
|
return .list(id: id)
|
||||||
|
case .instance(_):
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var title: String {
|
||||||
|
switch self {
|
||||||
|
case .home:
|
||||||
|
return "Home"
|
||||||
|
case let .public(local):
|
||||||
|
return local ? "Local" : "Federated"
|
||||||
|
case let .tag(hashtag):
|
||||||
|
return "#\(hashtag)"
|
||||||
|
case .list:
|
||||||
|
return "List"
|
||||||
|
case .instance(let url):
|
||||||
|
return url.host!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var image: UIImage {
|
||||||
|
switch self {
|
||||||
|
case .home:
|
||||||
|
return UIImage(systemName: "house.fill")!
|
||||||
|
case let .public(local):
|
||||||
|
if local {
|
||||||
|
return UIImage(systemName: "person.and.person.fill")!
|
||||||
|
} else {
|
||||||
|
return UIImage(systemName: "globe")!
|
||||||
|
}
|
||||||
|
case .list(id: _):
|
||||||
|
return UIImage(systemName: "list.bullet")!
|
||||||
|
case .tag(hashtag: _):
|
||||||
|
return UIImage(systemName: "number")!
|
||||||
|
case .instance(_):
|
||||||
|
return UIImage(systemName: "globe")!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case type
|
||||||
|
case local
|
||||||
|
case hashtag
|
||||||
|
case listID
|
||||||
|
case instanceURL
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,20 +13,24 @@ import os
|
||||||
// to make the lock semantics more clear
|
// to make the lock semantics more clear
|
||||||
@available(iOS, obsoleted: 16.0)
|
@available(iOS, obsoleted: 16.0)
|
||||||
class MultiThreadDictionary<Key: Hashable & Sendable, Value: Sendable> {
|
class MultiThreadDictionary<Key: Hashable & Sendable, Value: Sendable> {
|
||||||
private let lock: LockHolder<[AnyHashable: Any]>
|
private let lock: any Lock<[Key: Value]>
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
self.lock = LockHolder(initialState: [:])
|
if #available(iOS 16.0, *) {
|
||||||
|
self.lock = OSAllocatedUnfairLock(initialState: [:])
|
||||||
|
} else {
|
||||||
|
self.lock = UnfairLock(initialState: [:])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
subscript(key: Key) -> Value? {
|
subscript(key: Key) -> Value? {
|
||||||
get {
|
get {
|
||||||
return try! lock.withLock { dict in
|
return lock.withLock { dict in
|
||||||
dict[key]
|
dict[key]
|
||||||
} as! Value?
|
}
|
||||||
}
|
}
|
||||||
set(value) {
|
set(value) {
|
||||||
_ = try! lock.withLock { dict in
|
_ = lock.withLock { dict in
|
||||||
dict[key] = value
|
dict[key] = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -34,40 +38,21 @@ class MultiThreadDictionary<Key: Hashable & Sendable, Value: Sendable> {
|
||||||
|
|
||||||
/// If the result of this function is unused, it is preferable to use `removeValueWithoutReturning` as it executes asynchronously and doesn't block the calling thread.
|
/// If the result of this function is unused, it is preferable to use `removeValueWithoutReturning` as it executes asynchronously and doesn't block the calling thread.
|
||||||
func removeValue(forKey key: Key) -> Value? {
|
func removeValue(forKey key: Key) -> Value? {
|
||||||
return try! lock.withLock { dict in
|
return lock.withLock { dict in
|
||||||
dict.removeValue(forKey: key)
|
dict.removeValue(forKey: key)
|
||||||
} as! Value?
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func contains(key: Key) -> Bool {
|
func contains(key: Key) -> Bool {
|
||||||
return try! lock.withLock { dict in
|
return lock.withLock { dict in
|
||||||
dict.keys.contains(key)
|
dict.keys.contains(key)
|
||||||
} as! Bool
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: this should really be throws/rethrows but the stupid type-erased lock makes that not possible
|
// TODO: this should really be throws/rethrows but the stupid type-erased lock makes that not possible
|
||||||
func withLock<R>(_ body: @Sendable (inout [Key: Value]) -> R) -> R where R: Sendable {
|
func withLock<R>(_ body: @Sendable (inout [Key: Value]) throws -> R) rethrows -> R where R: Sendable {
|
||||||
return try! lock.withLock { dict in
|
return try lock.withLock { dict in
|
||||||
var downcasted = dict as! [Key: Value]
|
return try body(&dict)
|
||||||
defer { dict = downcasted }
|
|
||||||
return body(&downcasted)
|
|
||||||
} as! R
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// this type erased struct is necessary due to a compiler bug with stored constrained existential types
|
|
||||||
// see https://github.com/apple/swift/issues/61403
|
|
||||||
// see #178
|
|
||||||
fileprivate struct LockHolder<State> {
|
|
||||||
let withLock: (_ body: @Sendable (inout State) throws -> any Sendable) throws -> any Sendable
|
|
||||||
|
|
||||||
init(initialState: State) {
|
|
||||||
if #available(iOS 16.0, *) {
|
|
||||||
let lock = OSAllocatedUnfairLock(initialState: initialState)
|
|
||||||
self.withLock = lock.withLock(_:)
|
|
||||||
} else {
|
|
||||||
let lock = UnfairLock(initialState: initialState)
|
|
||||||
self.withLock = lock.withLock(_:)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,12 +38,14 @@ 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)
|
||||||
self.showIsStatusReplyIcon = try container.decode(Bool.self, forKey: .showIsStatusReplyIcon)
|
self.showIsStatusReplyIcon = try container.decode(Bool.self, forKey: .showIsStatusReplyIcon)
|
||||||
self.alwaysShowStatusVisibilityIcon = try container.decode(Bool.self, forKey: .alwaysShowStatusVisibilityIcon)
|
self.alwaysShowStatusVisibilityIcon = try container.decode(Bool.self, forKey: .alwaysShowStatusVisibilityIcon)
|
||||||
self.hideActionsInTimeline = try container.decodeIfPresent(Bool.self, forKey: .hideActionsInTimeline) ?? false
|
self.hideActionsInTimeline = try container.decodeIfPresent(Bool.self, forKey: .hideActionsInTimeline) ?? false
|
||||||
|
self.showLinkPreviews = try container.decodeIfPresent(Bool.self, forKey: .showLinkPreviews) ?? true
|
||||||
self.leadingStatusSwipeActions = try container.decodeIfPresent([StatusSwipeAction].self, forKey: .leadingStatusSwipeActions) ?? leadingStatusSwipeActions
|
self.leadingStatusSwipeActions = try container.decodeIfPresent([StatusSwipeAction].self, forKey: .leadingStatusSwipeActions) ?? leadingStatusSwipeActions
|
||||||
self.trailingStatusSwipeActions = try container.decodeIfPresent([StatusSwipeAction].self, forKey: .trailingStatusSwipeActions) ?? trailingStatusSwipeActions
|
self.trailingStatusSwipeActions = try container.decodeIfPresent([StatusSwipeAction].self, forKey: .trailingStatusSwipeActions) ?? trailingStatusSwipeActions
|
||||||
|
|
||||||
|
@ -63,6 +65,7 @@ class Preferences: Codable, ObservableObject {
|
||||||
self.blurMediaBehindContentWarning = try container.decodeIfPresent(Bool.self, forKey: .blurMediaBehindContentWarning) ?? true
|
self.blurMediaBehindContentWarning = try container.decodeIfPresent(Bool.self, forKey: .blurMediaBehindContentWarning) ?? true
|
||||||
self.automaticallyPlayGifs = try container.decode(Bool.self, forKey: .automaticallyPlayGifs)
|
self.automaticallyPlayGifs = try container.decode(Bool.self, forKey: .automaticallyPlayGifs)
|
||||||
self.showUncroppedMediaInline = try container.decodeIfPresent(Bool.self, forKey: .showUncroppedMediaInline) ?? true
|
self.showUncroppedMediaInline = try container.decodeIfPresent(Bool.self, forKey: .showUncroppedMediaInline) ?? true
|
||||||
|
self.showAttachmentBadges = try container.decodeIfPresent(Bool.self, forKey: .showAttachmentBadges) ?? true
|
||||||
|
|
||||||
self.openLinksInApps = try container.decode(Bool.self, forKey: .openLinksInApps)
|
self.openLinksInApps = try container.decode(Bool.self, forKey: .openLinksInApps)
|
||||||
self.useInAppSafari = try container.decode(Bool.self, forKey: .useInAppSafari)
|
self.useInAppSafari = try container.decode(Bool.self, forKey: .useInAppSafari)
|
||||||
|
@ -72,6 +75,7 @@ class Preferences: Codable, ObservableObject {
|
||||||
self.oppositeCollapseKeywords = try container.decodeIfPresent([String].self, forKey: .oppositeCollapseKeywords) ?? []
|
self.oppositeCollapseKeywords = try container.decodeIfPresent([String].self, forKey: .oppositeCollapseKeywords) ?? []
|
||||||
self.confirmBeforeReblog = try container.decodeIfPresent(Bool.self, forKey: .confirmBeforeReblog) ?? false
|
self.confirmBeforeReblog = try container.decodeIfPresent(Bool.self, forKey: .confirmBeforeReblog) ?? false
|
||||||
self.timelineStateRestoration = try container.decodeIfPresent(Bool.self, forKey: .timelineStateRestoration) ?? true
|
self.timelineStateRestoration = try container.decodeIfPresent(Bool.self, forKey: .timelineStateRestoration) ?? true
|
||||||
|
self.timelineSyncMode = try container.decodeIfPresent(TimelineSyncMode.self, forKey: .timelineSyncMode) ?? .icloud
|
||||||
self.hideReblogsInTimelines = try container.decodeIfPresent(Bool.self, forKey: .hideReblogsInTimelines) ?? false
|
self.hideReblogsInTimelines = try container.decodeIfPresent(Bool.self, forKey: .hideReblogsInTimelines) ?? false
|
||||||
self.hideRepliesInTimelines = try container.decodeIfPresent(Bool.self, forKey: .hideRepliesInTimelines) ?? false
|
self.hideRepliesInTimelines = try container.decodeIfPresent(Bool.self, forKey: .hideRepliesInTimelines) ?? false
|
||||||
|
|
||||||
|
@ -79,7 +83,7 @@ class Preferences: Codable, ObservableObject {
|
||||||
self.defaultNotificationsMode = try container.decode(NotificationsMode.self, forKey: .defaultNotificationsType)
|
self.defaultNotificationsMode = try container.decode(NotificationsMode.self, forKey: .defaultNotificationsType)
|
||||||
self.grayscaleImages = try container.decodeIfPresent(Bool.self, forKey: .grayscaleImages) ?? false
|
self.grayscaleImages = try container.decodeIfPresent(Bool.self, forKey: .grayscaleImages) ?? false
|
||||||
self.disableInfiniteScrolling = try container.decodeIfPresent(Bool.self, forKey: .disableInfiniteScrolling) ?? false
|
self.disableInfiniteScrolling = try container.decodeIfPresent(Bool.self, forKey: .disableInfiniteScrolling) ?? false
|
||||||
self.hideDiscover = try container.decodeIfPresent(Bool.self, forKey: .hideDiscover) ?? false
|
self.hideTrends = try container.decodeIfPresent(Bool.self, forKey: .hideTrends) ?? false
|
||||||
|
|
||||||
self.statusContentType = try container.decode(StatusContentType.self, forKey: .statusContentType)
|
self.statusContentType = try container.decode(StatusContentType.self, forKey: .statusContentType)
|
||||||
|
|
||||||
|
@ -91,12 +95,14 @@ 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)
|
||||||
try container.encode(showIsStatusReplyIcon, forKey: .showIsStatusReplyIcon)
|
try container.encode(showIsStatusReplyIcon, forKey: .showIsStatusReplyIcon)
|
||||||
try container.encode(alwaysShowStatusVisibilityIcon, forKey: .alwaysShowStatusVisibilityIcon)
|
try container.encode(alwaysShowStatusVisibilityIcon, forKey: .alwaysShowStatusVisibilityIcon)
|
||||||
try container.encode(hideActionsInTimeline, forKey: .hideActionsInTimeline)
|
try container.encode(hideActionsInTimeline, forKey: .hideActionsInTimeline)
|
||||||
|
try container.encode(showLinkPreviews, forKey: .showLinkPreviews)
|
||||||
try container.encode(leadingStatusSwipeActions, forKey: .leadingStatusSwipeActions)
|
try container.encode(leadingStatusSwipeActions, forKey: .leadingStatusSwipeActions)
|
||||||
try container.encode(trailingStatusSwipeActions, forKey: .trailingStatusSwipeActions)
|
try container.encode(trailingStatusSwipeActions, forKey: .trailingStatusSwipeActions)
|
||||||
|
|
||||||
|
@ -112,6 +118,7 @@ class Preferences: Codable, ObservableObject {
|
||||||
try container.encode(blurMediaBehindContentWarning, forKey: .blurMediaBehindContentWarning)
|
try container.encode(blurMediaBehindContentWarning, forKey: .blurMediaBehindContentWarning)
|
||||||
try container.encode(automaticallyPlayGifs, forKey: .automaticallyPlayGifs)
|
try container.encode(automaticallyPlayGifs, forKey: .automaticallyPlayGifs)
|
||||||
try container.encode(showUncroppedMediaInline, forKey: .showUncroppedMediaInline)
|
try container.encode(showUncroppedMediaInline, forKey: .showUncroppedMediaInline)
|
||||||
|
try container.encode(showAttachmentBadges, forKey: .showAttachmentBadges)
|
||||||
|
|
||||||
try container.encode(openLinksInApps, forKey: .openLinksInApps)
|
try container.encode(openLinksInApps, forKey: .openLinksInApps)
|
||||||
try container.encode(useInAppSafari, forKey: .useInAppSafari)
|
try container.encode(useInAppSafari, forKey: .useInAppSafari)
|
||||||
|
@ -121,6 +128,7 @@ class Preferences: Codable, ObservableObject {
|
||||||
try container.encode(oppositeCollapseKeywords, forKey: .oppositeCollapseKeywords)
|
try container.encode(oppositeCollapseKeywords, forKey: .oppositeCollapseKeywords)
|
||||||
try container.encode(confirmBeforeReblog, forKey: .confirmBeforeReblog)
|
try container.encode(confirmBeforeReblog, forKey: .confirmBeforeReblog)
|
||||||
try container.encode(timelineStateRestoration, forKey: .timelineStateRestoration)
|
try container.encode(timelineStateRestoration, forKey: .timelineStateRestoration)
|
||||||
|
try container.encode(timelineSyncMode, forKey: .timelineSyncMode)
|
||||||
try container.encode(hideReblogsInTimelines, forKey: .hideReblogsInTimelines)
|
try container.encode(hideReblogsInTimelines, forKey: .hideReblogsInTimelines)
|
||||||
try container.encode(hideRepliesInTimelines, forKey: .hideRepliesInTimelines)
|
try container.encode(hideRepliesInTimelines, forKey: .hideRepliesInTimelines)
|
||||||
|
|
||||||
|
@ -128,7 +136,7 @@ class Preferences: Codable, ObservableObject {
|
||||||
try container.encode(defaultNotificationsMode, forKey: .defaultNotificationsType)
|
try container.encode(defaultNotificationsMode, forKey: .defaultNotificationsType)
|
||||||
try container.encode(grayscaleImages, forKey: .grayscaleImages)
|
try container.encode(grayscaleImages, forKey: .grayscaleImages)
|
||||||
try container.encode(disableInfiniteScrolling, forKey: .disableInfiniteScrolling)
|
try container.encode(disableInfiniteScrolling, forKey: .disableInfiniteScrolling)
|
||||||
try container.encode(hideDiscover, forKey: .hideDiscover)
|
try container.encode(hideTrends, forKey: .hideTrends)
|
||||||
|
|
||||||
try container.encode(statusContentType, forKey: .statusContentType)
|
try container.encode(statusContentType, forKey: .statusContentType)
|
||||||
|
|
||||||
|
@ -138,12 +146,14 @@ 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
|
||||||
@Published var showIsStatusReplyIcon = false
|
@Published var showIsStatusReplyIcon = false
|
||||||
@Published var alwaysShowStatusVisibilityIcon = false
|
@Published var alwaysShowStatusVisibilityIcon = false
|
||||||
@Published var hideActionsInTimeline = false
|
@Published var hideActionsInTimeline = false
|
||||||
|
@Published var showLinkPreviews = true
|
||||||
@Published var leadingStatusSwipeActions: [StatusSwipeAction] = [.favorite, .reblog]
|
@Published var leadingStatusSwipeActions: [StatusSwipeAction] = [.favorite, .reblog]
|
||||||
@Published var trailingStatusSwipeActions: [StatusSwipeAction] = [.reply, .share]
|
@Published var trailingStatusSwipeActions: [StatusSwipeAction] = [.reply, .share]
|
||||||
|
|
||||||
|
@ -169,6 +179,7 @@ class Preferences: Codable, ObservableObject {
|
||||||
@Published var blurMediaBehindContentWarning = true
|
@Published var blurMediaBehindContentWarning = true
|
||||||
@Published var automaticallyPlayGifs = true
|
@Published var automaticallyPlayGifs = true
|
||||||
@Published var showUncroppedMediaInline = true
|
@Published var showUncroppedMediaInline = true
|
||||||
|
@Published var showAttachmentBadges = true
|
||||||
|
|
||||||
// MARK: Behavior
|
// MARK: Behavior
|
||||||
@Published var openLinksInApps = true
|
@Published var openLinksInApps = true
|
||||||
|
@ -179,6 +190,7 @@ class Preferences: Codable, ObservableObject {
|
||||||
@Published var oppositeCollapseKeywords: [String] = []
|
@Published var oppositeCollapseKeywords: [String] = []
|
||||||
@Published var confirmBeforeReblog = false
|
@Published var confirmBeforeReblog = false
|
||||||
@Published var timelineStateRestoration = true
|
@Published var timelineStateRestoration = true
|
||||||
|
@Published var timelineSyncMode = TimelineSyncMode.icloud
|
||||||
@Published var hideReblogsInTimelines = false
|
@Published var hideReblogsInTimelines = false
|
||||||
@Published var hideRepliesInTimelines = false
|
@Published var hideRepliesInTimelines = false
|
||||||
|
|
||||||
|
@ -187,7 +199,7 @@ class Preferences: Codable, ObservableObject {
|
||||||
@Published var defaultNotificationsMode = NotificationsMode.allNotifications
|
@Published var defaultNotificationsMode = NotificationsMode.allNotifications
|
||||||
@Published var grayscaleImages = false
|
@Published var grayscaleImages = false
|
||||||
@Published var disableInfiniteScrolling = false
|
@Published var disableInfiniteScrolling = false
|
||||||
@Published var hideDiscover = false
|
@Published var hideTrends = false
|
||||||
|
|
||||||
// MARK: Advanced
|
// MARK: Advanced
|
||||||
@Published var statusContentType: StatusContentType = .plain
|
@Published var statusContentType: StatusContentType = .plain
|
||||||
|
@ -199,12 +211,14 @@ 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
|
||||||
case showIsStatusReplyIcon
|
case showIsStatusReplyIcon
|
||||||
case alwaysShowStatusVisibilityIcon
|
case alwaysShowStatusVisibilityIcon
|
||||||
case hideActionsInTimeline
|
case hideActionsInTimeline
|
||||||
|
case showLinkPreviews
|
||||||
case leadingStatusSwipeActions
|
case leadingStatusSwipeActions
|
||||||
case trailingStatusSwipeActions
|
case trailingStatusSwipeActions
|
||||||
|
|
||||||
|
@ -221,6 +235,7 @@ class Preferences: Codable, ObservableObject {
|
||||||
case blurMediaBehindContentWarning
|
case blurMediaBehindContentWarning
|
||||||
case automaticallyPlayGifs
|
case automaticallyPlayGifs
|
||||||
case showUncroppedMediaInline
|
case showUncroppedMediaInline
|
||||||
|
case showAttachmentBadges
|
||||||
|
|
||||||
case openLinksInApps
|
case openLinksInApps
|
||||||
case useInAppSafari
|
case useInAppSafari
|
||||||
|
@ -230,6 +245,7 @@ class Preferences: Codable, ObservableObject {
|
||||||
case oppositeCollapseKeywords
|
case oppositeCollapseKeywords
|
||||||
case confirmBeforeReblog
|
case confirmBeforeReblog
|
||||||
case timelineStateRestoration
|
case timelineStateRestoration
|
||||||
|
case timelineSyncMode
|
||||||
case hideReblogsInTimelines
|
case hideReblogsInTimelines
|
||||||
case hideRepliesInTimelines
|
case hideRepliesInTimelines
|
||||||
|
|
||||||
|
@ -237,7 +253,7 @@ class Preferences: Codable, ObservableObject {
|
||||||
case defaultNotificationsType
|
case defaultNotificationsType
|
||||||
case grayscaleImages
|
case grayscaleImages
|
||||||
case disableInfiniteScrolling
|
case disableInfiniteScrolling
|
||||||
case hideDiscover
|
case hideTrends = "hideDiscover"
|
||||||
|
|
||||||
case statusContentType
|
case statusContentType
|
||||||
|
|
||||||
|
@ -383,3 +399,10 @@ extension Preferences {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension Preferences {
|
||||||
|
enum TimelineSyncMode: String, Codable {
|
||||||
|
case mastodon
|
||||||
|
case icloud
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -128,7 +128,9 @@ private func createReblogAction(status: StatusMO, container: StatusSwipeActionCo
|
||||||
|
|
||||||
private func createShareAction(status: StatusMO, container: StatusSwipeActionContainer) -> UIContextualAction {
|
private func createShareAction(status: StatusMO, container: StatusSwipeActionContainer) -> UIContextualAction {
|
||||||
let action = UIContextualAction(style: .normal, title: "Share") { [unowned container] _, _, completion in
|
let action = UIContextualAction(style: .normal, title: "Share") { [unowned container] _, _, completion in
|
||||||
|
MainActor.runUnsafely {
|
||||||
container.navigationDelegate.showMoreOptions(forStatus: status.id, source: .view(container))
|
container.navigationDelegate.showMoreOptions(forStatus: status.id, source: .view(container))
|
||||||
|
}
|
||||||
completion(true)
|
completion(true)
|
||||||
}
|
}
|
||||||
// bold to more closesly match other action symbols
|
// bold to more closesly match other action symbols
|
||||||
|
@ -166,7 +168,9 @@ private func createBookmarkAction(status: StatusMO, container: StatusSwipeAction
|
||||||
|
|
||||||
private func createOpenInSafariAction(status: StatusMO, container: StatusSwipeActionContainer) -> UIContextualAction {
|
private func createOpenInSafariAction(status: StatusMO, container: StatusSwipeActionContainer) -> UIContextualAction {
|
||||||
let action = UIContextualAction(style: .normal, title: "Open in Safari") { [unowned container] _, _, completion in
|
let action = UIContextualAction(style: .normal, title: "Open in Safari") { [unowned container] _, _, completion in
|
||||||
|
MainActor.runUnsafely {
|
||||||
container.navigationDelegate.selected(url: status.url!, allowUniversalLinks: false)
|
container.navigationDelegate.selected(url: status.url!, allowUniversalLinks: false)
|
||||||
|
}
|
||||||
completion(true)
|
completion(true)
|
||||||
}
|
}
|
||||||
action.image = UIImage(systemName: "safari")
|
action.image = UIImage(systemName: "safari")
|
||||||
|
|
|
@ -49,7 +49,7 @@ class SavedDataManager: Codable {
|
||||||
var changed = false
|
var changed = false
|
||||||
|
|
||||||
if let hashtags = savedHashtags[accountID] {
|
if let hashtags = savedHashtags[accountID] {
|
||||||
let objects = hashtags.map {
|
let objects: [[String: Any]] = hashtags.map {
|
||||||
["url": $0.url, "name": $0.name]
|
["url": $0.url, "name": $0.name]
|
||||||
}
|
}
|
||||||
let hashtagsReq = NSBatchInsertRequest(entity: SavedHashtag.entity(), objects: objects)
|
let hashtagsReq = NSBatchInsertRequest(entity: SavedHashtag.entity(), objects: objects)
|
||||||
|
|
|
@ -9,10 +9,14 @@
|
||||||
import UIKit
|
import UIKit
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
|
|
||||||
class AuxiliarySceneDelegate: UIResponder, UIWindowSceneDelegate {
|
class AuxiliarySceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDelegate {
|
||||||
|
|
||||||
var window: UIWindow?
|
var window: UIWindow?
|
||||||
|
|
||||||
|
var rootViewController: TuskerRootViewController? {
|
||||||
|
window?.rootViewController as? TuskerRootViewController
|
||||||
|
}
|
||||||
|
|
||||||
private var launchActivity: NSUserActivity?
|
private var launchActivity: NSUserActivity?
|
||||||
|
|
||||||
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
|
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
|
||||||
|
@ -70,7 +74,7 @@ class AuxiliarySceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||||
private func viewController(for activity: NSUserActivity, mastodonController: MastodonController) -> UIViewController? {
|
private func viewController(for activity: NSUserActivity, mastodonController: MastodonController) -> UIViewController? {
|
||||||
switch UserActivityType(rawValue: activity.activityType) {
|
switch UserActivityType(rawValue: activity.activityType) {
|
||||||
case .showTimeline:
|
case .showTimeline:
|
||||||
guard let timeline = UserActivityManager.getTimeline(from: activity) else { return nil }
|
guard let (timeline, _) = UserActivityManager.getTimeline(from: activity) else { return nil }
|
||||||
return timelineViewController(for: timeline, mastodonController: mastodonController)
|
return timelineViewController(for: timeline, mastodonController: mastodonController)
|
||||||
|
|
||||||
case .showConversation:
|
case .showConversation:
|
||||||
|
@ -82,10 +86,10 @@ class AuxiliarySceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||||
return NotificationsPageViewController(initialMode: mode, mastodonController: mastodonController)
|
return NotificationsPageViewController(initialMode: mode, mastodonController: mastodonController)
|
||||||
|
|
||||||
case .search:
|
case .search:
|
||||||
return SearchViewController(mastodonController: mastodonController)
|
return InlineTrendsViewController(mastodonController: mastodonController)
|
||||||
|
|
||||||
case .bookmarks:
|
case .bookmarks:
|
||||||
return BookmarksTableViewController(mastodonController: mastodonController)
|
return BookmarksViewController(mastodonController: mastodonController)
|
||||||
|
|
||||||
case .myProfile:
|
case .myProfile:
|
||||||
return MyProfileViewController(mastodonController: mastodonController)
|
return MyProfileViewController(mastodonController: mastodonController)
|
||||||
|
@ -112,7 +116,6 @@ class AuxiliarySceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func themePrefChanged() {
|
@objc private func themePrefChanged() {
|
||||||
window?.overrideUserInterfaceStyle = Preferences.shared.theme
|
applyAppearancePreferences()
|
||||||
window?.tintColor = Preferences.shared.accentColor.color
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,10 +9,12 @@
|
||||||
import UIKit
|
import UIKit
|
||||||
import Combine
|
import Combine
|
||||||
|
|
||||||
class ComposeSceneDelegate: UIResponder, UIWindowSceneDelegate {
|
class ComposeSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDelegate {
|
||||||
|
|
||||||
var window: UIWindow?
|
var window: UIWindow?
|
||||||
|
|
||||||
|
var rootViewController: TuskerRootViewController? { nil }
|
||||||
|
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
|
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
|
||||||
|
@ -100,8 +102,7 @@ class ComposeSceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func themePrefChanged() {
|
@objc private func themePrefChanged() {
|
||||||
window?.overrideUserInterfaceStyle = Preferences.shared.theme
|
applyAppearancePreferences()
|
||||||
window?.tintColor = Preferences.shared.accentColor.color
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,7 +64,15 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDelegate
|
||||||
|
|
||||||
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
|
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
|
||||||
stateRestorationLogger.info("MainSceneDelegate.scene(_:continue:) called with \(userActivity.activityType, privacy: .public)")
|
stateRestorationLogger.info("MainSceneDelegate.scene(_:continue:) called with \(userActivity.activityType, privacy: .public)")
|
||||||
_ = userActivity.handleResume(manager: UserActivityManager(scene: scene as! UIWindowScene))
|
let context: any UserActivityHandlingContext
|
||||||
|
if let account = UserActivityManager.getAccount(from: userActivity),
|
||||||
|
account.id != scene.session.mastodonController!.accountInfo!.id {
|
||||||
|
stateRestorationLogger.info("MainSceneDelegate cannot resume user activity for different account")
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
context = ActiveAccountUserActivityHandlingContext(isHandoff: true, root: rootViewController!)
|
||||||
|
}
|
||||||
|
_ = userActivity.handleResume(manager: UserActivityManager(scene: scene as! UIWindowScene, context: context))
|
||||||
}
|
}
|
||||||
|
|
||||||
func windowScene(_ windowScene: UIWindowScene, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) {
|
func windowScene(_ windowScene: UIWindowScene, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) {
|
||||||
|
@ -169,10 +177,16 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDelegate
|
||||||
activateAccount(account, animated: false)
|
activateAccount(account, animated: false)
|
||||||
|
|
||||||
if let activity = launchActivity {
|
if let activity = launchActivity {
|
||||||
|
func doRestoreActivity(context: UserActivityHandlingContext) {
|
||||||
|
_ = activity.handleResume(manager: UserActivityManager(scene: window!.windowScene!, context: context))
|
||||||
|
context.finalize(activity: activity)
|
||||||
|
}
|
||||||
if activity.isStateRestorationActivity {
|
if activity.isStateRestorationActivity {
|
||||||
rootViewController?.restoreActivity(activity)
|
doRestoreActivity(context: StateRestorationUserActivityHandlingContext(root: rootViewController!))
|
||||||
} else if activity.activityType != UserActivityType.mainScene.rawValue {
|
} else if activity.activityType != UserActivityType.mainScene.rawValue {
|
||||||
_ = activity.handleResume(manager: UserActivityManager(scene: window!.windowScene!))
|
doRestoreActivity(context: ActiveAccountUserActivityHandlingContext(isHandoff: false, root: rootViewController!))
|
||||||
|
} else {
|
||||||
|
stateRestorationLogger.fault("MainSceneDelegate launched with non-restorable activity \(activity.activityType, privacy: .public)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -204,9 +218,9 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDelegate
|
||||||
} else {
|
} else {
|
||||||
direction = .none
|
direction = .none
|
||||||
}
|
}
|
||||||
container.setRoot(newRoot, animating: direction)
|
container.setRoot(newRoot, for: account, animating: direction)
|
||||||
} else {
|
} else {
|
||||||
window!.rootViewController = AccountSwitchingContainerViewController(root: newRoot)
|
window!.rootViewController = AccountSwitchingContainerViewController(root: newRoot, for: account)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -214,7 +228,7 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDelegate
|
||||||
guard let account = window?.windowScene?.session.mastodonController?.accountInfo else {
|
guard let account = window?.windowScene?.session.mastodonController?.accountInfo else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
LocalData.shared.removeAccount(account)
|
LogoutService(accountInfo: account).run()
|
||||||
if LocalData.shared.onboardingComplete {
|
if LocalData.shared.onboardingComplete {
|
||||||
activateAccount(LocalData.shared.accounts.first!, animated: false)
|
activateAccount(LocalData.shared.accounts.first!, animated: false)
|
||||||
} else {
|
} else {
|
||||||
|
@ -243,8 +257,7 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDelegate
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func themePrefChanged() {
|
@objc func themePrefChanged() {
|
||||||
window?.overrideUserInterfaceStyle = Preferences.shared.theme
|
applyAppearancePreferences()
|
||||||
window?.tintColor = Preferences.shared.accentColor.color
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func showAddAccount() {
|
func showAddAccount() {
|
||||||
|
|
|
@ -7,11 +7,11 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import Sentry
|
||||||
|
|
||||||
protocol TuskerSceneDelegate: UISceneDelegate {
|
protocol TuskerSceneDelegate: UISceneDelegate {
|
||||||
|
var window: UIWindow? { get }
|
||||||
var rootViewController: TuskerRootViewController? { get }
|
var rootViewController: TuskerRootViewController? { get }
|
||||||
|
|
||||||
func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum StatusBarTapActionResult {
|
enum StatusBarTapActionResult {
|
||||||
|
@ -27,4 +27,19 @@ extension TuskerSceneDelegate {
|
||||||
}
|
}
|
||||||
return .continue
|
return .continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func applyAppearancePreferences() {
|
||||||
|
guard let window else { return }
|
||||||
|
window.overrideUserInterfaceStyle = Preferences.shared.theme
|
||||||
|
window.tintColor = Preferences.shared.accentColor.color
|
||||||
|
let exception = catchNSException {
|
||||||
|
let key = ["Controller", "Presentation", "root", "_"].reversed().joined()
|
||||||
|
if let rootPresentationController = window.value(forKey: key) as? UIPresentationController {
|
||||||
|
rootPresentationController.overrideTraitCollection = UITraitCollection(pureBlackDarkMode: Preferences.shared.pureBlackDarkMode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let exception {
|
||||||
|
SentrySDK.capture(exception: exception)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,8 @@ import Pachyderm
|
||||||
|
|
||||||
class AccountFollowsListViewController: UIViewController, CollectionViewController {
|
class AccountFollowsListViewController: UIViewController, CollectionViewController {
|
||||||
|
|
||||||
|
private static let pageSize = 40
|
||||||
|
|
||||||
let accountID: String
|
let accountID: String
|
||||||
let mastodonController: MastodonController
|
let mastodonController: MastodonController
|
||||||
let mode: AccountFollowsViewController.Mode
|
let mode: AccountFollowsViewController.Mode
|
||||||
|
@ -39,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
|
||||||
|
@ -63,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()
|
||||||
|
@ -89,12 +102,12 @@ class AccountFollowsListViewController: UIViewController, CollectionViewControll
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func request(for range: RequestRange) -> Request<[Account]> {
|
private nonisolated func request(for range: RequestRange) -> Request<[Account]> {
|
||||||
switch mode {
|
switch mode {
|
||||||
case .following:
|
case .following:
|
||||||
return Account.getFollowing(accountID, range: range)
|
return Account.getFollowing(accountID, range: range.withCount(Self.pageSize))
|
||||||
case .followers:
|
case .followers:
|
||||||
return Account.getFollowers(accountID, range: range)
|
return Account.getFollowers(accountID, range: range.withCount(Self.pageSize))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -1,196 +0,0 @@
|
||||||
//
|
|
||||||
// BookmarksTableViewController.swift
|
|
||||||
// Tusker
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 12/15/19.
|
|
||||||
// Copyright © 2019 Shadowfacts. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
import Pachyderm
|
|
||||||
|
|
||||||
class BookmarksTableViewController: EnhancedTableViewController {
|
|
||||||
|
|
||||||
private let statusCell = "statusCell"
|
|
||||||
|
|
||||||
let mastodonController: MastodonController
|
|
||||||
|
|
||||||
private var loaded = false
|
|
||||||
|
|
||||||
var statuses: [(id: String, state: CollapseState)] = []
|
|
||||||
|
|
||||||
var newer: RequestRange?
|
|
||||||
var older: RequestRange?
|
|
||||||
|
|
||||||
init(mastodonController: MastodonController) {
|
|
||||||
self.mastodonController = mastodonController
|
|
||||||
|
|
||||||
super.init(style: .plain)
|
|
||||||
|
|
||||||
dragEnabled = true
|
|
||||||
|
|
||||||
title = NSLocalizedString("Bookmarks", comment: "bookmarks screen title")
|
|
||||||
}
|
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
|
||||||
fatalError("init(coder:) has not been implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override func viewDidLoad() {
|
|
||||||
super.viewDidLoad()
|
|
||||||
|
|
||||||
tableView.rowHeight = UITableView.automaticDimension
|
|
||||||
tableView.estimatedRowHeight = 140
|
|
||||||
tableView.allowsFocus = true
|
|
||||||
|
|
||||||
tableView.register(UINib(nibName: "TimelineStatusTableViewCell", bundle: .main), forCellReuseIdentifier: statusCell)
|
|
||||||
|
|
||||||
tableView.prefetchDataSource = self
|
|
||||||
|
|
||||||
userActivity = UserActivityManager.bookmarksActivity()
|
|
||||||
|
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(handleStatusDeleted), name: .statusDeleted, object: nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
override func viewWillAppear(_ animated: Bool) {
|
|
||||||
super.viewWillAppear(animated)
|
|
||||||
|
|
||||||
if !loaded {
|
|
||||||
loaded = true
|
|
||||||
|
|
||||||
let request = Client.getBookmarks()
|
|
||||||
mastodonController.run(request) { (response) in
|
|
||||||
guard case let .success(statuses, pagination) = response else { fatalError() }
|
|
||||||
self.mastodonController.persistentContainer.addAll(statuses: statuses) {
|
|
||||||
self.statuses.append(contentsOf: statuses.map { ($0.id, .unknown) })
|
|
||||||
self.newer = pagination?.newer
|
|
||||||
self.older = pagination?.older
|
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.tableView.reloadData()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Table view data source
|
|
||||||
|
|
||||||
override func numberOfSections(in tableView: UITableView) -> Int {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
|
||||||
return statuses.count
|
|
||||||
}
|
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
|
||||||
let cell = tableView.dequeueReusableCell(withIdentifier: statusCell, for: indexPath) as! TimelineStatusTableViewCell
|
|
||||||
cell.delegate = self
|
|
||||||
let (id, state) = statuses[indexPath.row]
|
|
||||||
cell.updateUI(statusID: id, state: state)
|
|
||||||
return cell
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Table view delegate
|
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
|
||||||
guard indexPath.row == statuses.count, let older = older else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let request = Client.getBookmarks(range: older)
|
|
||||||
mastodonController.run(request) { (response) in
|
|
||||||
guard case let .success(newStatuses, pagination) = response else { fatalError() }
|
|
||||||
self.older = pagination?.older
|
|
||||||
self.mastodonController.persistentContainer.addAll(statuses: newStatuses) {
|
|
||||||
let newIndexPaths = (self.statuses.count..<(self.statuses.count + newStatuses.count)).map {
|
|
||||||
IndexPath(row: $0, section: 0)
|
|
||||||
}
|
|
||||||
self.statuses.append(contentsOf: newStatuses.map { ($0.id, .unknown) })
|
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
UIView.performWithoutAnimation {
|
|
||||||
self.tableView.insertRows(at: newIndexPaths, with: .automatic)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
|
||||||
return (tableView.cellForRow(at: indexPath) as? TableViewSwipeActionProvider)?.leadingSwipeActionsConfiguration()
|
|
||||||
}
|
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
|
||||||
let cellConfig = (tableView.cellForRow(at: indexPath) as? TableViewSwipeActionProvider)?.trailingSwipeActionsConfiguration()
|
|
||||||
|
|
||||||
guard let status = mastodonController.persistentContainer.status(for: statuses[indexPath.row].id) else {
|
|
||||||
return cellConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
let unbookmarkAction = UIContextualAction(style: .destructive, title: NSLocalizedString("Unbookmark", comment: "unbookmark action title")) { (action, view, completion) in
|
|
||||||
let request = Status.unbookmark(status.id)
|
|
||||||
self.mastodonController.run(request) { (response) in
|
|
||||||
guard case let .success(newStatus, _) = response else { fatalError() }
|
|
||||||
self.mastodonController.persistentContainer.addOrUpdate(status: newStatus)
|
|
||||||
self.statuses.remove(at: indexPath.row)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
unbookmarkAction.image = UIImage(systemName: "bookmark.fill")
|
|
||||||
|
|
||||||
let config: UISwipeActionsConfiguration
|
|
||||||
if let cellConfig = cellConfig {
|
|
||||||
config = UISwipeActionsConfiguration(actions: cellConfig.actions + [unbookmarkAction])
|
|
||||||
config.performsFirstActionWithFullSwipe = cellConfig.performsFirstActionWithFullSwipe
|
|
||||||
} else {
|
|
||||||
config = UISwipeActionsConfiguration(actions: [unbookmarkAction])
|
|
||||||
config.performsFirstActionWithFullSwipe = false
|
|
||||||
}
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc private func handleStatusDeleted(_ notification: Foundation.Notification) {
|
|
||||||
guard let userInfo = notification.userInfo,
|
|
||||||
let accountID = mastodonController.accountInfo?.id,
|
|
||||||
userInfo["accountID"] as? String == accountID,
|
|
||||||
let statusIDs = userInfo["statusIDs"] as? [String] else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let indicesToDelete = statusIDs
|
|
||||||
.compactMap { id in
|
|
||||||
self.statuses.firstIndex(where: { $0.id == id })
|
|
||||||
}
|
|
||||||
self.statuses.remove(atOffsets: IndexSet(indicesToDelete))
|
|
||||||
self.tableView.deleteRows(at: indicesToDelete.map { IndexPath(row: $0, section: 0) }, with: .automatic)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
extension BookmarksTableViewController: TuskerNavigationDelegate {
|
|
||||||
var apiController: MastodonController! { mastodonController }
|
|
||||||
}
|
|
||||||
|
|
||||||
extension BookmarksTableViewController: ToastableViewController {
|
|
||||||
}
|
|
||||||
|
|
||||||
extension BookmarksTableViewController: MenuActionProvider {
|
|
||||||
}
|
|
||||||
|
|
||||||
extension BookmarksTableViewController: StatusTableViewCellDelegate {
|
|
||||||
func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) {
|
|
||||||
tableView.beginUpdates()
|
|
||||||
tableView.endUpdates()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension BookmarksTableViewController: UITableViewDataSourcePrefetching, StatusTablePrefetching {
|
|
||||||
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
|
|
||||||
let ids = indexPaths.map { statuses[$0.row].id }
|
|
||||||
prefetchStatuses(with: ids)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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()
|
||||||
|
|
|
@ -236,6 +236,7 @@ struct ComposeAutocompleteEmojisView: View {
|
||||||
CustomEmojiImageView(emoji: emoji)
|
CustomEmojiImageView(emoji: emoji)
|
||||||
.frame(height: emojiSize)
|
.frame(height: emojiSize)
|
||||||
}
|
}
|
||||||
|
.accessibilityLabel(emoji.shortcode)
|
||||||
}
|
}
|
||||||
} header: {
|
} header: {
|
||||||
if !section.isEmpty {
|
if !section.isEmpty {
|
||||||
|
@ -271,6 +272,7 @@ struct ComposeAutocompleteEmojisView: View {
|
||||||
.foregroundColor(Color(UIColor.label))
|
.foregroundColor(Color(UIColor.label))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.accessibilityLabel(emoji.shortcode)
|
||||||
.frame(height: emojiSize)
|
.frame(height: emojiSize)
|
||||||
}
|
}
|
||||||
.animation(.linear(duration: 0.2), value: emojis)
|
.animation(.linear(duration: 0.2), value: emojis)
|
||||||
|
@ -293,6 +295,7 @@ struct ComposeAutocompleteEmojisView: View {
|
||||||
.aspectRatio(contentMode: .fit)
|
.aspectRatio(contentMode: .fit)
|
||||||
.rotationEffect(expanded ? .zero : .degrees(180))
|
.rotationEffect(expanded ? .zero : .degrees(180))
|
||||||
}
|
}
|
||||||
|
.accessibilityLabel(expanded ? "Collapse" : "Expand")
|
||||||
.frame(width: 20, height: 20)
|
.frame(width: 20, height: 20)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,15 +15,17 @@ struct ComposeEmojiTextField: UIViewRepresentable {
|
||||||
|
|
||||||
@Binding var text: String
|
@Binding var text: String
|
||||||
let placeholder: String
|
let placeholder: String
|
||||||
|
let maxLength: Int?
|
||||||
let becomeFirstResponder: Binding<Bool>?
|
let becomeFirstResponder: Binding<Bool>?
|
||||||
let focusNextView: Binding<Bool>?
|
let focusNextView: Binding<Bool>?
|
||||||
private var didChange: ((String) -> Void)? = nil
|
private var didChange: ((String) -> Void)? = nil
|
||||||
private var didEndEditing: (() -> Void)? = nil
|
private var didEndEditing: (() -> Void)? = nil
|
||||||
private var backgroundColor: UIColor? = nil
|
private var backgroundColor: UIColor? = nil
|
||||||
|
|
||||||
init(text: Binding<String>, placeholder: String, becomeFirstResponder: Binding<Bool>? = nil, focusNextView: Binding<Bool>? = nil) {
|
init(text: Binding<String>, placeholder: String, maxLength: Int? = nil, becomeFirstResponder: Binding<Bool>? = nil, focusNextView: Binding<Bool>? = nil) {
|
||||||
self._text = text
|
self._text = text
|
||||||
self.placeholder = placeholder
|
self.placeholder = placeholder
|
||||||
|
self.maxLength = maxLength
|
||||||
self.becomeFirstResponder = becomeFirstResponder
|
self.becomeFirstResponder = becomeFirstResponder
|
||||||
self.focusNextView = focusNextView
|
self.focusNextView = focusNextView
|
||||||
self.didChange = nil
|
self.didChange = nil
|
||||||
|
@ -74,6 +76,7 @@ struct ComposeEmojiTextField: UIViewRepresentable {
|
||||||
} else {
|
} else {
|
||||||
uiView.text = text
|
uiView.text = text
|
||||||
}
|
}
|
||||||
|
context.coordinator.maxLength = maxLength
|
||||||
context.coordinator.didChange = didChange
|
context.coordinator.didChange = didChange
|
||||||
context.coordinator.didEndEditing = didEndEditing
|
context.coordinator.didEndEditing = didEndEditing
|
||||||
context.coordinator.focusNextView = focusNextView
|
context.coordinator.focusNextView = focusNextView
|
||||||
|
@ -95,6 +98,7 @@ struct ComposeEmojiTextField: UIViewRepresentable {
|
||||||
var text: Binding<String>!
|
var text: Binding<String>!
|
||||||
// break retained cycle through ComposeUIState.currentInput
|
// break retained cycle through ComposeUIState.currentInput
|
||||||
unowned var uiState: ComposeUIState!
|
unowned var uiState: ComposeUIState!
|
||||||
|
var maxLength: Int?
|
||||||
var didChange: ((String) -> Void)?
|
var didChange: ((String) -> Void)?
|
||||||
var didEndEditing: (() -> Void)?
|
var didEndEditing: (() -> Void)?
|
||||||
var focusNextView: Binding<Bool>?
|
var focusNextView: Binding<Bool>?
|
||||||
|
@ -114,6 +118,14 @@ struct ComposeEmojiTextField: UIViewRepresentable {
|
||||||
focusNextView?.wrappedValue = true
|
focusNextView?.wrappedValue = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
|
||||||
|
if let maxLength {
|
||||||
|
return ((textField.text ?? "") as NSString).replacingCharacters(in: range, with: string).count <= maxLength
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func textFieldDidBeginEditing(_ textField: UITextField) {
|
func textFieldDidBeginEditing(_ textField: UITextField) {
|
||||||
uiState.currentInput = self
|
uiState.currentInput = self
|
||||||
updateAutocompleteState(textField: textField)
|
updateAutocompleteState(textField: textField)
|
||||||
|
|
|
@ -20,6 +20,7 @@ struct ComposePollView: View {
|
||||||
@ObservedObject var draft: Draft
|
@ObservedObject var draft: Draft
|
||||||
@ObservedObject var poll: Draft.Poll
|
@ObservedObject var poll: Draft.Poll
|
||||||
|
|
||||||
|
@EnvironmentObject var mastodonController: MastodonController
|
||||||
@Environment(\.colorScheme) var colorScheme: ColorScheme
|
@Environment(\.colorScheme) var colorScheme: ColorScheme
|
||||||
|
|
||||||
@State private var duration: Duration
|
@State private var duration: Duration
|
||||||
|
@ -31,6 +32,14 @@ struct ComposePollView: View {
|
||||||
self._duration = State(initialValue: .fromTimeInterval(poll.duration) ?? .oneDay)
|
self._duration = State(initialValue: .fromTimeInterval(poll.duration) ?? .oneDay)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var canAddOption: Bool {
|
||||||
|
if let pollConfig = mastodonController.instance?.pollsConfiguration {
|
||||||
|
return poll.options.count < pollConfig.maxOptions
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
VStack {
|
||||||
HStack {
|
HStack {
|
||||||
|
@ -67,9 +76,15 @@ struct ComposePollView: View {
|
||||||
.frame(height: 44 * CGFloat(poll.options.count))
|
.frame(height: 44 * CGFloat(poll.options.count))
|
||||||
|
|
||||||
Button(action: self.addOption) {
|
Button(action: self.addOption) {
|
||||||
Label("Add Option", systemImage: "plus")
|
Label {
|
||||||
|
Text("Add Option")
|
||||||
|
} icon: {
|
||||||
|
Image(systemName: "plus")
|
||||||
|
.foregroundColor(.accentColor)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.buttonStyle(.borderless)
|
.buttonStyle(.borderless)
|
||||||
|
.disabled(!canAddOption)
|
||||||
|
|
||||||
HStack {
|
HStack {
|
||||||
MenuPicker(selection: $poll.multiple, options: [
|
MenuPicker(selection: $poll.multiple, options: [
|
||||||
|
@ -96,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 {
|
||||||
|
@ -155,6 +170,8 @@ struct ComposePollOption: View {
|
||||||
@ObservedObject var option: Draft.Poll.Option
|
@ObservedObject var option: Draft.Poll.Option
|
||||||
let optionIndex: Int
|
let optionIndex: Int
|
||||||
|
|
||||||
|
@EnvironmentObject private var mastodonController: MastodonController
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack(spacing: 4) {
|
HStack(spacing: 4) {
|
||||||
Checkbox(radiusFraction: poll.multiple ? 0.1 : 0.5, borderWidth: 2)
|
Checkbox(radiusFraction: poll.multiple ? 0.1 : 0.5, borderWidth: 2)
|
||||||
|
@ -173,8 +190,8 @@ struct ComposePollOption: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private var textField: some View {
|
private var textField: some View {
|
||||||
var field = ComposeEmojiTextField(text: $option.text, placeholder: "Option \(optionIndex + 1)")
|
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() {
|
||||||
|
@ -199,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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,6 +54,7 @@ struct ComposeToolbar: View {
|
||||||
.font(.system(size: imageSize))
|
.font(.system(size: imageSize))
|
||||||
.padding(5)
|
.padding(5)
|
||||||
.hoverEffect()
|
.hoverEffect()
|
||||||
|
.transition(.opacity.animation(.linear(duration: 0.2)))
|
||||||
}
|
}
|
||||||
|
|
||||||
if let currentInput = uiState.currentInput,
|
if let currentInput = uiState.currentInput,
|
||||||
|
@ -74,16 +75,11 @@ struct ComposeToolbar: View {
|
||||||
.accessibilityLabel(format.accessibilityLabel)
|
.accessibilityLabel(format.accessibilityLabel)
|
||||||
.padding(5)
|
.padding(5)
|
||||||
.hoverEffect()
|
.hoverEffect()
|
||||||
|
.transition(.opacity.animation(.linear(duration: 0.2)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
Button(action: self.draftsButtonPressed) {
|
|
||||||
Text("Drafts")
|
|
||||||
}
|
|
||||||
.padding(5)
|
|
||||||
.hoverEffect()
|
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 16)
|
.padding(.horizontal, 16)
|
||||||
.frame(minWidth: minWidth)
|
.frame(minWidth: minWidth)
|
||||||
|
@ -119,10 +115,6 @@ struct ComposeToolbar: View {
|
||||||
uiState.currentInput?.beginAutocompletingEmoji()
|
uiState.currentInput?.beginAutocompletingEmoji()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func draftsButtonPressed() {
|
|
||||||
uiState.isShowingDraftsList = true
|
|
||||||
}
|
|
||||||
|
|
||||||
private func formatAction(_ format: StatusFormat) -> () -> Void {
|
private func formatAction(_ format: StatusFormat) -> () -> Void {
|
||||||
{
|
{
|
||||||
uiState.currentInput?.applyFormat(format)
|
uiState.currentInput?.applyFormat(format)
|
||||||
|
|
|
@ -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()
|
||||||
|
@ -239,7 +249,9 @@ struct ComposeView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
private var postButton: some View {
|
private var postButton: some View {
|
||||||
|
if draft.hasContent {
|
||||||
Button {
|
Button {
|
||||||
Task {
|
Task {
|
||||||
await self.postStatus()
|
await self.postStatus()
|
||||||
|
@ -249,6 +261,13 @@ struct ComposeView: View {
|
||||||
}
|
}
|
||||||
.keyboardShortcut(.return, modifiers: .command)
|
.keyboardShortcut(.return, modifiers: .command)
|
||||||
.disabled(!postButtonEnabled)
|
.disabled(!postButtonEnabled)
|
||||||
|
} else {
|
||||||
|
Button {
|
||||||
|
uiState.isShowingDraftsList = true
|
||||||
|
} label: {
|
||||||
|
Text("Drafts")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func cancel() {
|
private func cancel() {
|
||||||
|
@ -310,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 {
|
||||||
|
|
|
@ -9,39 +9,20 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
|
|
||||||
struct MainComposeTextView: View {
|
struct MainComposeTextView: View, PlaceholderViewProvider {
|
||||||
@ObservedObject var draft: Draft
|
@ObservedObject var draft: Draft
|
||||||
@State private var placeholder: Text = {
|
@State private var placeholder: PlaceholderView = Self.placeholderView()
|
||||||
let components = Calendar.current.dateComponents([.month, .day], from: Date())
|
|
||||||
if components.month == 3 && components.day == 14 {
|
|
||||||
if Date().formatted(date: .numeric, time: .omitted).starts(with: "3") {
|
|
||||||
return Text("Happy π day!")
|
|
||||||
}
|
|
||||||
} else if components.month == 9 && components.day == 5 {
|
|
||||||
// https://weirder.earth/@noracodes/109276419847254552
|
|
||||||
// https://retrocomputing.stackexchange.com/questions/14763/what-warning-was-given-on-attempting-to-post-to-usenet-circa-1990
|
|
||||||
return Text("This program posts news to thousands of machines throughout the entire populated world. Please be sure you know what you are doing.").italic()
|
|
||||||
} else if components.month == 9 && components.day == 21 {
|
|
||||||
return Text("Do you remember?")
|
|
||||||
} else if components.month == 10 && components.day == 31 {
|
|
||||||
if .random() {
|
|
||||||
return Text("Post something spooky!")
|
|
||||||
} else {
|
|
||||||
return Text("Any questions?")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Text("What's on your mind?")
|
|
||||||
}()
|
|
||||||
|
|
||||||
let minHeight: CGFloat = 150
|
let minHeight: CGFloat = 150
|
||||||
@State private var height: CGFloat?
|
@State private var height: CGFloat?
|
||||||
@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
|
||||||
|
@ -67,6 +48,38 @@ struct MainComposeTextView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
static func placeholderView() -> some View {
|
||||||
|
let components = Calendar.current.dateComponents([.month, .day], from: Date())
|
||||||
|
if components.month == 3 && components.day == 14,
|
||||||
|
Date().formatted(date: .numeric, time: .omitted).starts(with: "3") {
|
||||||
|
Text("Happy π day!")
|
||||||
|
} else if components.month == 4 && components.day == 1 {
|
||||||
|
Text("April Fool's!").rotationEffect(.radians(.pi), anchor: .center)
|
||||||
|
} else if components.month == 9 && components.day == 5 {
|
||||||
|
// https://weirder.earth/@noracodes/109276419847254552
|
||||||
|
// https://retrocomputing.stackexchange.com/questions/14763/what-warning-was-given-on-attempting-to-post-to-usenet-circa-1990
|
||||||
|
Text("This program posts news to thousands of machines throughout the entire populated world. Please be sure you know what you are doing.").italic()
|
||||||
|
} else if components.month == 9 && components.day == 21 {
|
||||||
|
Text("Do you remember?")
|
||||||
|
} else if components.month == 10 && components.day == 31 {
|
||||||
|
if .random() {
|
||||||
|
Text("Post something spooky!")
|
||||||
|
} else {
|
||||||
|
Text("Any questions?")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Text("What's on your mind?")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// exists to provide access to the type alias since the @State property needs it to be explicit
|
||||||
|
private protocol PlaceholderViewProvider {
|
||||||
|
associatedtype PlaceholderView: View
|
||||||
|
@ViewBuilder
|
||||||
|
static func placeholderView() -> PlaceholderView
|
||||||
}
|
}
|
||||||
|
|
||||||
struct MainComposeWrappedTextView: UIViewRepresentable {
|
struct MainComposeWrappedTextView: UIViewRepresentable {
|
||||||
|
@ -98,6 +111,7 @@ struct MainComposeWrappedTextView: UIViewRepresentable {
|
||||||
if context.coordinator.skipSettingTextOnNextUpdate {
|
if context.coordinator.skipSettingTextOnNextUpdate {
|
||||||
context.coordinator.skipSettingTextOnNextUpdate = false
|
context.coordinator.skipSettingTextOnNextUpdate = false
|
||||||
} else {
|
} else {
|
||||||
|
context.coordinator.skipNextAutocompleteUpdate = true
|
||||||
uiView.text = text
|
uiView.text = text
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -185,6 +199,7 @@ struct MainComposeWrappedTextView: UIViewRepresentable {
|
||||||
var caretScrollPositionAnimator: UIViewPropertyAnimator?
|
var caretScrollPositionAnimator: UIViewPropertyAnimator?
|
||||||
|
|
||||||
var skipSettingTextOnNextUpdate = false
|
var skipSettingTextOnNextUpdate = false
|
||||||
|
var skipNextAutocompleteUpdate = false
|
||||||
|
|
||||||
var toolbarElements: [ComposeUIState.ToolbarElement] {
|
var toolbarElements: [ComposeUIState.ToolbarElement] {
|
||||||
[.emojiPicker, .formattingButtons]
|
[.emojiPicker, .formattingButtons]
|
||||||
|
@ -324,6 +339,10 @@ struct MainComposeWrappedTextView: UIViewRepresentable {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateAutocompleteState() {
|
private func updateAutocompleteState() {
|
||||||
|
guard !skipNextAutocompleteUpdate else {
|
||||||
|
skipNextAutocompleteUpdate = false
|
||||||
|
return
|
||||||
|
}
|
||||||
guard let textView = textView,
|
guard let textView = textView,
|
||||||
let text = textView.text,
|
let text = textView.text,
|
||||||
let (lastWordStartIndex, foundFirstAtSign) = findAutocompleteLastWord() else {
|
let (lastWordStartIndex, foundFirstAtSign) = findAutocompleteLastWord() else {
|
||||||
|
|
|
@ -9,18 +9,9 @@
|
||||||
import UIKit
|
import UIKit
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
|
|
||||||
class ConversationNode {
|
class ConversationCollectionViewController: UIViewController, CollectionViewController, RefreshableViewController {
|
||||||
let status: StatusMO
|
|
||||||
var children: [ConversationNode]
|
|
||||||
|
|
||||||
init(status: StatusMO) {
|
|
||||||
self.status = status
|
|
||||||
self.children = []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ConversationCollectionViewController: UIViewController, CollectionViewController {
|
|
||||||
|
|
||||||
|
private unowned let conversationViewController: ConversationViewController
|
||||||
private let mastodonController: MastodonController
|
private let mastodonController: MastodonController
|
||||||
private let mainStatusID: String
|
private let mainStatusID: String
|
||||||
private let mainStatusState: CollapseState
|
private let mainStatusState: CollapseState
|
||||||
|
@ -32,11 +23,12 @@ class ConversationCollectionViewController: UIViewController, CollectionViewCont
|
||||||
}
|
}
|
||||||
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
||||||
|
|
||||||
init(for mainStatusID: String, state: CollapseState, mastodonController: MastodonController) {
|
init(for mainStatusID: String, state: CollapseState, conversationViewController: ConversationViewController) {
|
||||||
self.mainStatusID = mainStatusID
|
self.mainStatusID = mainStatusID
|
||||||
self.mainStatusState = state
|
self.mainStatusState = state
|
||||||
self.statusIDToScrollToOnLoad = mainStatusID
|
self.statusIDToScrollToOnLoad = mainStatusID
|
||||||
self.mastodonController = mastodonController
|
self.conversationViewController = conversationViewController
|
||||||
|
self.mastodonController = conversationViewController.mastodonController
|
||||||
|
|
||||||
super.init(nibName: nil, bundle: nil)
|
super.init(nibName: nil, bundle: nil)
|
||||||
}
|
}
|
||||||
|
@ -47,7 +39,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()
|
||||||
}
|
}
|
||||||
|
@ -55,17 +47,21 @@ class ConversationCollectionViewController: UIViewController, CollectionViewCont
|
||||||
(collectionView.cellForItem(at: $0) as? TimelineStatusCollectionViewCell)?.trailingSwipeActions()
|
(collectionView.cellForItem(at: $0) as? TimelineStatusCollectionViewCell)?.trailingSwipeActions()
|
||||||
}
|
}
|
||||||
config.itemSeparatorHandler = { [unowned self] indexPath, sectionConfig in
|
config.itemSeparatorHandler = { [unowned self] indexPath, sectionConfig in
|
||||||
let rowsInSection = self.collectionView.numberOfItems(inSection: indexPath.section)
|
|
||||||
let lastInSection = indexPath.row == rowsInSection - 1
|
|
||||||
var config = sectionConfig
|
var config = sectionConfig
|
||||||
config.topSeparatorVisibility = .hidden
|
config.topSeparatorVisibility = .hidden
|
||||||
config.bottomSeparatorVisibility = lastInSection ? .visible : .hidden
|
if case .ancestors = self.dataSource.sectionIdentifier(for: indexPath.section) {
|
||||||
|
config.bottomSeparatorVisibility = .hidden
|
||||||
|
} else if indexPath.row == self.collectionView.numberOfItems(inSection: indexPath.section) - 1 {
|
||||||
|
config.bottomSeparatorVisibility = .visible
|
||||||
|
} else {
|
||||||
|
config.bottomSeparatorVisibility = .hidden
|
||||||
|
}
|
||||||
config.bottomSeparatorInsets = TimelineStatusCollectionViewCell.separatorInsets
|
config.bottomSeparatorInsets = TimelineStatusCollectionViewCell.separatorInsets
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
// 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
|
||||||
|
@ -74,6 +70,11 @@ class ConversationCollectionViewController: UIViewController, CollectionViewCont
|
||||||
collectionView.dragDelegate = self
|
collectionView.dragDelegate = self
|
||||||
collectionView.allowsFocus = true
|
collectionView.allowsFocus = true
|
||||||
|
|
||||||
|
#if !targetEnvironment(macCatalyst)
|
||||||
|
collectionView.refreshControl = UIRefreshControl()
|
||||||
|
collectionView.refreshControl!.addTarget(self, action: #selector(refresh), for: .valueChanged)
|
||||||
|
#endif
|
||||||
|
|
||||||
dataSource = createDataSource()
|
dataSource = createDataSource()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -99,7 +100,7 @@ class ConversationCollectionViewController: UIViewController, CollectionViewCont
|
||||||
}
|
}
|
||||||
return UICollectionViewDiffableDataSource(collectionView: collectionView) { [unowned self] collectionView, indexPath, itemIdentifier in
|
return UICollectionViewDiffableDataSource(collectionView: collectionView) { [unowned self] collectionView, indexPath, itemIdentifier in
|
||||||
switch itemIdentifier {
|
switch itemIdentifier {
|
||||||
case let .status(id: id, state: state, prevLink: prevLink, nextLink: nextLink):
|
case let .status(id: id, node: _, state: state, prevLink: prevLink, nextLink: nextLink):
|
||||||
if id == self.mainStatusID {
|
if id == self.mainStatusID {
|
||||||
return collectionView.dequeueConfiguredReusableCell(using: mainStatusCell, for: indexPath, item: (id, state, prevLink))
|
return collectionView.dequeueConfiguredReusableCell(using: mainStatusCell, for: indexPath, item: (id, state, prevLink))
|
||||||
} else {
|
} else {
|
||||||
|
@ -123,45 +124,33 @@ class ConversationCollectionViewController: UIViewController, CollectionViewCont
|
||||||
loadViewIfNeeded()
|
loadViewIfNeeded()
|
||||||
|
|
||||||
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||||
snapshot.appendSections([.statuses])
|
snapshot.appendSections([.ancestors, .mainStatus])
|
||||||
|
|
||||||
if status.inReplyToID != nil {
|
if status.inReplyToID != nil {
|
||||||
snapshot.appendItems([.loadingIndicator], toSection: .statuses)
|
snapshot.appendItems([.loadingIndicator], toSection: .ancestors)
|
||||||
}
|
}
|
||||||
|
|
||||||
let mainStatusItem = Item.status(id: mainStatusID, state: mainStatusState, prevLink: status.inReplyToID != nil, nextLink: false)
|
// this will be replace with the actual node in the tree once it's loaded
|
||||||
snapshot.appendItems([mainStatusItem], toSection: .statuses)
|
let tempMainNode = ConversationNode(status: status)
|
||||||
|
let mainStatusItem = Item.status(id: mainStatusID, node: tempMainNode, state: mainStatusState, prevLink: status.inReplyToID != nil, nextLink: false)
|
||||||
|
snapshot.appendItems([mainStatusItem], toSection: .mainStatus)
|
||||||
|
|
||||||
dataSource.apply(snapshot, animatingDifferences: false)
|
dataSource.apply(snapshot, animatingDifferences: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func addContext(_ context: ConversationContext, for mainStatus: StatusMO) async {
|
func addTree(_ tree: ConversationTree, mainStatus: StatusMO) {
|
||||||
let parentIDs = getDirectParents(inReplyTo: mainStatus.inReplyToID, from: context.ancestors)
|
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||||
let parentStatuses = context.ancestors.filter { parentIDs.contains($0.id) }
|
snapshot.appendSections([.ancestors, .mainStatus])
|
||||||
|
let mainStatusItem = Item.status(id: mainStatusID, node: tree.mainStatus, state: mainStatusState, prevLink: mainStatus.inReplyToID != nil, nextLink: false)
|
||||||
await mastodonController.persistentContainer.addAll(statuses: parentStatuses + context.descendants)
|
snapshot.appendItems([mainStatusItem], toSection: .mainStatus)
|
||||||
|
let parentItems = tree.ancestors.enumerated().map { index, node in
|
||||||
var snapshot = dataSource.snapshot()
|
Item.status(id: node.status.id, node: node, state: .unknown, prevLink: index > 0, nextLink: true)
|
||||||
snapshot.deleteItems([.loadingIndicator])
|
|
||||||
let mainStatusItem = Item.status(id: mainStatusID, state: mainStatusState, prevLink: mainStatus.inReplyToID != nil, nextLink: false)
|
|
||||||
let parentItems = parentIDs.enumerated().map { index, id in
|
|
||||||
Item.status(id: id, state: .unknown, prevLink: index > 0, nextLink: true)
|
|
||||||
}
|
}
|
||||||
snapshot.insertItems(parentItems, beforeItem: mainStatusItem)
|
snapshot.appendItems(parentItems, toSection: .ancestors)
|
||||||
snapshot.reloadItems([mainStatusItem])
|
snapshot.reloadItems([mainStatusItem])
|
||||||
|
|
||||||
// fetch all descendant status managed objects
|
|
||||||
let descendantIDs = context.descendants.map(\.id)
|
|
||||||
let request = StatusMO.fetchRequest()
|
|
||||||
request.predicate = NSPredicate(format: "id IN %@", descendantIDs)
|
|
||||||
|
|
||||||
if let descendants = try? mastodonController.persistentContainer.viewContext.fetch(request) {
|
|
||||||
// convert array of descendant statuses into tree of sub-threads
|
|
||||||
let childThreads = self.getDescendantThreads(mainStatus: mainStatus, descendants: descendants)
|
|
||||||
|
|
||||||
// convert sub-threads into items for section and add to snapshot
|
// convert sub-threads into items for section and add to snapshot
|
||||||
self.addFlattenedChildThreadsToSnapshot(childThreads, mainStatus: mainStatus, snapshot: &snapshot)
|
self.addFlattenedChildThreadsToSnapshot(tree.descendants, mainStatus: mainStatus, snapshot: &snapshot)
|
||||||
}
|
|
||||||
|
|
||||||
self.dataSource.apply(snapshot, animatingDifferences: false) {
|
self.dataSource.apply(snapshot, animatingDifferences: false) {
|
||||||
let item: Item
|
let item: Item
|
||||||
|
@ -171,7 +160,7 @@ class ConversationCollectionViewController: UIViewController, CollectionViewCont
|
||||||
position = .centeredVertically
|
position = .centeredVertically
|
||||||
} else {
|
} else {
|
||||||
item = snapshot.itemIdentifiers.first {
|
item = snapshot.itemIdentifiers.first {
|
||||||
if case .status(id: self.statusIDToScrollToOnLoad, _, _, _) = $0 {
|
if case .status(id: self.statusIDToScrollToOnLoad, _, _, _, _) = $0 {
|
||||||
return true
|
return true
|
||||||
} else {
|
} else {
|
||||||
return false
|
return false
|
||||||
|
@ -187,54 +176,6 @@ class ConversationCollectionViewController: UIViewController, CollectionViewCont
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func getDirectParents(inReplyTo inReplyToID: String?, from statuses: [Status]) -> [String] {
|
|
||||||
var statuses = statuses
|
|
||||||
var parents = [String]()
|
|
||||||
|
|
||||||
var parentID: String? = inReplyToID
|
|
||||||
|
|
||||||
while parentID != nil, let parentIndex = statuses.firstIndex(where: { $0.id == parentID }) {
|
|
||||||
let parentStatus = statuses.remove(at: parentIndex)
|
|
||||||
parents.insert(parentStatus.id, at: 0)
|
|
||||||
parentID = parentStatus.inReplyToID
|
|
||||||
}
|
|
||||||
|
|
||||||
return parents
|
|
||||||
}
|
|
||||||
|
|
||||||
private func getDescendantThreads(mainStatus: StatusMO, descendants: [StatusMO]) -> [ConversationNode] {
|
|
||||||
var descendants = descendants
|
|
||||||
|
|
||||||
func removeAllInReplyTo(id: String) -> [StatusMO] {
|
|
||||||
let statuses = descendants.filter { $0.inReplyToID == id }
|
|
||||||
descendants.removeAll { $0.inReplyToID == id }
|
|
||||||
return statuses
|
|
||||||
}
|
|
||||||
|
|
||||||
var nodes: [String: ConversationNode] = [
|
|
||||||
mainStatus.id: ConversationNode(status: mainStatus)
|
|
||||||
]
|
|
||||||
|
|
||||||
var idsToCheck = [mainStatusID]
|
|
||||||
|
|
||||||
while !idsToCheck.isEmpty {
|
|
||||||
let inReplyToID = idsToCheck.removeFirst()
|
|
||||||
let nodeForID = nodes[inReplyToID]!
|
|
||||||
|
|
||||||
let inReply = removeAllInReplyTo(id: inReplyToID)
|
|
||||||
for reply in inReply {
|
|
||||||
idsToCheck.append(reply.id)
|
|
||||||
|
|
||||||
let replyNode = ConversationNode(status: reply)
|
|
||||||
nodes[reply.id] = replyNode
|
|
||||||
|
|
||||||
nodeForID.children.append(replyNode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nodes[mainStatusID]!.children
|
|
||||||
}
|
|
||||||
|
|
||||||
private func addFlattenedChildThreadsToSnapshot(_ childThreads: [ConversationNode], mainStatus: StatusMO, snapshot: inout NSDiffableDataSourceSnapshot<Section, Item>) {
|
private func addFlattenedChildThreadsToSnapshot(_ childThreads: [ConversationNode], mainStatus: StatusMO, snapshot: inout NSDiffableDataSourceSnapshot<Section, Item>) {
|
||||||
var childThreads = childThreads
|
var childThreads = childThreads
|
||||||
|
|
||||||
|
@ -248,7 +189,7 @@ class ConversationCollectionViewController: UIViewController, CollectionViewCont
|
||||||
for node in childThreads {
|
for node in childThreads {
|
||||||
let section = Section.childThread(firstStatusID: node.status.id)
|
let section = Section.childThread(firstStatusID: node.status.id)
|
||||||
snapshot.appendSections([section])
|
snapshot.appendSections([section])
|
||||||
snapshot.appendItems([.status(id: node.status.id, state: .unknown, prevLink: false, nextLink: !node.children.isEmpty)], toSection: section)
|
snapshot.appendItems([.status(id: node.status.id, node: node, state: .unknown, prevLink: false, nextLink: !node.children.isEmpty)], toSection: section)
|
||||||
|
|
||||||
var currentNode = node
|
var currentNode = node
|
||||||
while true {
|
while true {
|
||||||
|
@ -271,7 +212,7 @@ class ConversationCollectionViewController: UIViewController, CollectionViewCont
|
||||||
}
|
}
|
||||||
|
|
||||||
currentNode = next
|
currentNode = next
|
||||||
snapshot.appendItems([.status(id: next.status.id, state: .unknown, prevLink: true, nextLink: !next.children.isEmpty)], toSection: section)
|
snapshot.appendItems([.status(id: next.status.id, node: next, state: .unknown, prevLink: true, nextLink: !next.children.isEmpty)], toSection: section)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -280,7 +221,7 @@ class ConversationCollectionViewController: UIViewController, CollectionViewCont
|
||||||
var snapshot = dataSource.snapshot()
|
var snapshot = dataSource.snapshot()
|
||||||
var cellsToMask: [StatusCollectionViewCell] = []
|
var cellsToMask: [StatusCollectionViewCell] = []
|
||||||
for item in snapshot.itemIdentifiers {
|
for item in snapshot.itemIdentifiers {
|
||||||
guard case .status(id: _, state: let state, prevLink: _, nextLink: _) = item,
|
guard case .status(id: _, node: _, state: let state, prevLink: _, nextLink: _) = item,
|
||||||
state.collapsible == true else {
|
state.collapsible == true else {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -307,21 +248,31 @@ class ConversationCollectionViewController: UIViewController, CollectionViewCont
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc func refresh() {
|
||||||
|
Task {
|
||||||
|
await conversationViewController.refreshContext()
|
||||||
|
#if !targetEnvironment(macCatalyst)
|
||||||
|
self.collectionView.refreshControl!.endRefreshing()
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ConversationCollectionViewController {
|
extension ConversationCollectionViewController {
|
||||||
enum Section: Hashable {
|
enum Section: Hashable {
|
||||||
case statuses
|
case ancestors
|
||||||
|
case mainStatus
|
||||||
case childThread(firstStatusID: String)
|
case childThread(firstStatusID: String)
|
||||||
}
|
}
|
||||||
enum Item: Hashable {
|
enum Item: Hashable {
|
||||||
case status(id: String, state: CollapseState, prevLink: Bool, nextLink: Bool)
|
case status(id: String, node: ConversationNode, state: CollapseState, prevLink: Bool, nextLink: Bool)
|
||||||
case expandThread(childThreads: [ConversationNode], inline: Bool)
|
case expandThread(childThreads: [ConversationNode], inline: Bool)
|
||||||
case loadingIndicator
|
case loadingIndicator
|
||||||
|
|
||||||
static func ==(lhs: Item, rhs: Item) -> Bool {
|
static func ==(lhs: Item, rhs: Item) -> Bool {
|
||||||
switch (lhs, rhs) {
|
switch (lhs, rhs) {
|
||||||
case let (.status(id: a, state: _, prevLink: aPrev, nextLink: aNext), .status(id: b, state: _, prevLink: bPrev, nextLink: bNext)):
|
case let (.status(id: a, node: _, state: _, prevLink: aPrev, nextLink: aNext), .status(id: b, node: _, state: _, prevLink: bPrev, nextLink: bNext)):
|
||||||
return a == b && aPrev == bPrev && aNext == bNext
|
return a == b && aPrev == bPrev && aNext == bNext
|
||||||
case let (.expandThread(childThreads: a, inline: aInline), .expandThread(childThreads: b, inline: bInline)):
|
case let (.expandThread(childThreads: a, inline: aInline), .expandThread(childThreads: b, inline: bInline)):
|
||||||
return a.count == b.count && zip(a, b).allSatisfy { $0.status.id == $1.status.id } && aInline == bInline
|
return a.count == b.count && zip(a, b).allSatisfy { $0.status.id == $1.status.id } && aInline == bInline
|
||||||
|
@ -334,7 +285,7 @@ extension ConversationCollectionViewController {
|
||||||
|
|
||||||
func hash(into hasher: inout Hasher) {
|
func hash(into hasher: inout Hasher) {
|
||||||
switch self {
|
switch self {
|
||||||
case let .status(id: id, state: _, prevLink: prevLink, nextLink: nextLink):
|
case let .status(id: id, node: _, state: _, prevLink: prevLink, nextLink: nextLink):
|
||||||
hasher.combine(0)
|
hasher.combine(0)
|
||||||
hasher.combine(id)
|
hasher.combine(id)
|
||||||
hasher.combine(prevLink)
|
hasher.combine(prevLink)
|
||||||
|
@ -355,7 +306,7 @@ extension ConversationCollectionViewController {
|
||||||
extension ConversationCollectionViewController: UICollectionViewDelegate {
|
extension ConversationCollectionViewController: UICollectionViewDelegate {
|
||||||
func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool {
|
func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool {
|
||||||
switch dataSource.itemIdentifier(for: indexPath) {
|
switch dataSource.itemIdentifier(for: indexPath) {
|
||||||
case .status(id: let id, state: _, prevLink: _, nextLink: _):
|
case .status(id: let id, node: _, state: _, prevLink: _, nextLink: _):
|
||||||
return id != mainStatusID
|
return id != mainStatusID
|
||||||
case .expandThread(childThreads: _, inline: _):
|
case .expandThread(childThreads: _, inline: _):
|
||||||
return true
|
return true
|
||||||
|
@ -370,12 +321,25 @@ extension ConversationCollectionViewController: UICollectionViewDelegate {
|
||||||
break
|
break
|
||||||
case .loadingIndicator:
|
case .loadingIndicator:
|
||||||
break
|
break
|
||||||
case .status(id: let id, state: let state, _, _):
|
case .status(id: let id, node: let node, state: let state, _, _):
|
||||||
|
// we can only take the fast path if the user tapped on a descendant status.
|
||||||
|
// if the current main status is C, or one of its descendants, and the user taps A, then B won't be loaded:
|
||||||
|
// A
|
||||||
|
// / \
|
||||||
|
// B C
|
||||||
|
if case .childThread(_) = dataSource.sectionIdentifier(for: indexPath.section) {
|
||||||
|
let tree = ConversationTree(ancestors: buildNewAncestors(above: indexPath), mainStatus: node)
|
||||||
|
let conv = ConversationViewController(preloadedTree: tree, state: state.copy(), mastodonController: mastodonController)
|
||||||
|
conv.showStatusesAutomatically = showStatusesAutomatically
|
||||||
|
show(conv)
|
||||||
|
} else {
|
||||||
selected(status: id, state: state.copy())
|
selected(status: id, state: state.copy())
|
||||||
|
}
|
||||||
case .expandThread(childThreads: let childThreads, inline: _):
|
case .expandThread(childThreads: let childThreads, inline: _):
|
||||||
if case .status(id: let id, state: let state, _, _) = dataSource.itemIdentifier(for: IndexPath(row: indexPath.row - 1, section: indexPath.section)) {
|
let indexPathBeforeExpandThread = IndexPath(row: indexPath.row - 1, section: indexPath.section)
|
||||||
// todo: it would be nice to avoid re-fetching the context here, since we should have all the necessary information already
|
if case .status(id: _, node: let node, state: let state, _, _) = dataSource.itemIdentifier(for: indexPathBeforeExpandThread) {
|
||||||
let conv = ConversationViewController(for: id, state: state.copy(), mastodonController: mastodonController)
|
let tree = ConversationTree(ancestors: buildNewAncestors(above: indexPathBeforeExpandThread), mainStatus: node)
|
||||||
|
let conv = ConversationViewController(preloadedTree: tree, state: state.copy(), mastodonController: mastodonController)
|
||||||
conv.statusIDToScrollToOnLoad = childThreads.first!.status.id
|
conv.statusIDToScrollToOnLoad = childThreads.first!.status.id
|
||||||
conv.showStatusesAutomatically = showStatusesAutomatically
|
conv.showStatusesAutomatically = showStatusesAutomatically
|
||||||
show(conv)
|
show(conv)
|
||||||
|
@ -383,6 +347,34 @@ extension ConversationCollectionViewController: UICollectionViewDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ConversationNode doesn't know about its parent, so we reconstruct that info from the data source
|
||||||
|
private func buildNewAncestors(above indexPath: IndexPath) -> [ConversationNode] {
|
||||||
|
let snapshot = dataSource.snapshot()
|
||||||
|
let currentAncestors = snapshot.itemIdentifiers(inSection: .ancestors).compactMap {
|
||||||
|
if case .status(id: _, node: let node, _, _, _) = $0 {
|
||||||
|
return node
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let currentMainStatus = snapshot.itemIdentifiers(inSection: .mainStatus).compactMap {
|
||||||
|
if case .status(id: _, node: let node, _, _, _) = $0 {
|
||||||
|
return node
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let parentsInCurrentSection = snapshot.itemIdentifiers(inSection: dataSource.sectionIdentifier(for: indexPath.section)!)[0..<indexPath.row].compactMap {
|
||||||
|
if case .status(id: _, node: let node, _, _, _) = $0 {
|
||||||
|
return node
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return currentAncestors + currentMainStatus + parentsInCurrentSection
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
|
func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
|
||||||
return (collectionView.cellForItem(at: indexPath) as? TimelineStatusCollectionViewCell)?.contextMenuConfiguration()
|
return (collectionView.cellForItem(at: indexPath) as? TimelineStatusCollectionViewCell)?.contextMenuConfiguration()
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,101 @@
|
||||||
|
//
|
||||||
|
// ConversationTree.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 2/4/23.
|
||||||
|
// Copyright © 2023 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Pachyderm
|
||||||
|
|
||||||
|
class ConversationNode {
|
||||||
|
let status: StatusMO
|
||||||
|
var children: [ConversationNode]
|
||||||
|
|
||||||
|
init(status: StatusMO) {
|
||||||
|
self.status = status
|
||||||
|
self.children = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
struct ConversationTree {
|
||||||
|
let ancestors: [ConversationNode]
|
||||||
|
let mainStatus: ConversationNode
|
||||||
|
var descendants: [ConversationNode] {
|
||||||
|
mainStatus.children
|
||||||
|
}
|
||||||
|
|
||||||
|
init(ancestors: [ConversationNode], mainStatus: ConversationNode) {
|
||||||
|
self.ancestors = ancestors
|
||||||
|
self.mainStatus = mainStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
static func build(for mainStatus: StatusMO, ancestors: [StatusMO], descendants: [StatusMO]) -> ConversationTree {
|
||||||
|
let mainStatusNode = ConversationNode(status: mainStatus)
|
||||||
|
let ancestors = buildAncestorNodes(mainStatusNode: mainStatusNode, ancestors: ancestors)
|
||||||
|
buildDescendantNodes(mainStatusNode: mainStatusNode, descendants: descendants)
|
||||||
|
return ConversationTree(ancestors: ancestors, mainStatus: mainStatusNode)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func buildAncestorNodes(mainStatusNode: ConversationNode, ancestors: [StatusMO]) -> [ConversationNode] {
|
||||||
|
var statuses = ancestors
|
||||||
|
var parents = [ConversationNode]()
|
||||||
|
|
||||||
|
var parentID: String? = mainStatusNode.status.inReplyToID
|
||||||
|
|
||||||
|
while let currentParentID = parentID,
|
||||||
|
let parentIndex = statuses.firstIndex(where: { $0.id == currentParentID }) {
|
||||||
|
let parentStatus = statuses.remove(at: parentIndex)
|
||||||
|
|
||||||
|
let node = ConversationNode(status: parentStatus)
|
||||||
|
parents.insert(node, at: 0)
|
||||||
|
|
||||||
|
parentID = parentStatus.inReplyToID
|
||||||
|
}
|
||||||
|
|
||||||
|
// once the parents list is built and in-order, then we walk through and set each node's children
|
||||||
|
for (index, node) in parents.enumerated() {
|
||||||
|
if index == parents.count - 1 {
|
||||||
|
// the last parent is the direct parent of the main status
|
||||||
|
node.children = [mainStatusNode]
|
||||||
|
} else {
|
||||||
|
// otherwise, it's the parent of the status that comes immediately after it in the parents list
|
||||||
|
node.children = [parents[index + 1]]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return parents
|
||||||
|
}
|
||||||
|
|
||||||
|
// doesn't return anything, since we're modifying the main status node in-place
|
||||||
|
private static func buildDescendantNodes(mainStatusNode: ConversationNode, descendants: [StatusMO]) {
|
||||||
|
var descendants = descendants
|
||||||
|
|
||||||
|
func removeAllInReplyTo(id: String) -> [StatusMO] {
|
||||||
|
let statuses = descendants.filter { $0.inReplyToID == id }
|
||||||
|
descendants.removeAll { $0.inReplyToID == id }
|
||||||
|
return statuses
|
||||||
|
}
|
||||||
|
|
||||||
|
var nodes: [String: ConversationNode] = [
|
||||||
|
mainStatusNode.status.id: mainStatusNode
|
||||||
|
]
|
||||||
|
|
||||||
|
var idsToCheck = [mainStatusNode.status.id]
|
||||||
|
|
||||||
|
while !idsToCheck.isEmpty {
|
||||||
|
let inReplyToID = idsToCheck.removeFirst()
|
||||||
|
let nodeForID = nodes[inReplyToID]!
|
||||||
|
|
||||||
|
let inReply = removeAllInReplyTo(id: inReplyToID)
|
||||||
|
for reply in inReply {
|
||||||
|
idsToCheck.append(reply.id)
|
||||||
|
|
||||||
|
let replyNode = ConversationNode(status: reply)
|
||||||
|
nodes[reply.id] = replyNode
|
||||||
|
|
||||||
|
nodeForID.children.append(replyNode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -77,6 +77,14 @@ class ConversationViewController: UIViewController {
|
||||||
super.init(nibName: nil, bundle: nil)
|
super.init(nibName: nil, bundle: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
init(preloadedTree: ConversationTree, state mainStatusState: CollapseState, mastodonController: MastodonController) {
|
||||||
|
self.mode = .preloaded(preloadedTree)
|
||||||
|
self.mainStatusState = mainStatusState
|
||||||
|
self.mastodonController = mastodonController
|
||||||
|
|
||||||
|
super.init(nibName: nil, bundle: nil)
|
||||||
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
@ -86,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()
|
||||||
|
@ -115,12 +123,18 @@ class ConversationViewController: UIViewController {
|
||||||
override func viewWillAppear(_ animated: Bool) {
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
super.viewWillAppear(animated)
|
super.viewWillAppear(animated)
|
||||||
|
|
||||||
Task {
|
|
||||||
if case .unloaded = state {
|
if case .unloaded = state {
|
||||||
|
if case .preloaded(let tree) = mode {
|
||||||
|
// when everything is preloaded, we're on the fast path and want to avoid any async work
|
||||||
|
// just kicking off a MainActor task causes a delay before the content appears, even if the task doesn't suspend
|
||||||
|
mainStatusLoaded(tree.mainStatus.status)
|
||||||
|
} else {
|
||||||
|
Task { @MainActor in
|
||||||
await loadMainStatus()
|
await loadMainStatus()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@objc private func handleStatusDeleted(_ notification: Foundation.Notification) {
|
@objc private func handleStatusDeleted(_ notification: Foundation.Notification) {
|
||||||
guard let userInfo = notification.userInfo,
|
guard let userInfo = notification.userInfo,
|
||||||
|
@ -142,10 +156,21 @@ class ConversationViewController: UIViewController {
|
||||||
|
|
||||||
// MARK: Loading
|
// MARK: Loading
|
||||||
|
|
||||||
|
@MainActor
|
||||||
private func loadMainStatus() async {
|
private func loadMainStatus() async {
|
||||||
guard let mainStatusID = await resolveStatusIfNecessary() else {
|
let mainStatusID: String
|
||||||
|
switch mode {
|
||||||
|
case .localID(let id):
|
||||||
|
mainStatusID = id
|
||||||
|
case .resolve(let url):
|
||||||
|
if let id = await resolveStatus(url: url) {
|
||||||
|
mainStatusID = id
|
||||||
|
} else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
case .preloaded(_):
|
||||||
|
fatalError("unreachable")
|
||||||
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
func doLoadMainStatus() async -> StatusMO? {
|
func doLoadMainStatus() async -> StatusMO? {
|
||||||
|
@ -166,7 +191,7 @@ class ConversationViewController: UIViewController {
|
||||||
Task {
|
Task {
|
||||||
await doLoadMainStatus()
|
await doLoadMainStatus()
|
||||||
}
|
}
|
||||||
await mainStatusLoaded(cached)
|
mainStatusLoaded(cached)
|
||||||
} else {
|
} else {
|
||||||
// otherwise, show a loading indicator while loading the main status
|
// otherwise, show a loading indicator while loading the main status
|
||||||
let indicator = UIActivityIndicatorView(style: .medium)
|
let indicator = UIActivityIndicatorView(style: .medium)
|
||||||
|
@ -174,26 +199,22 @@ class ConversationViewController: UIViewController {
|
||||||
state = .loading(indicator)
|
state = .loading(indicator)
|
||||||
|
|
||||||
if let status = await doLoadMainStatus() {
|
if let status = await doLoadMainStatus() {
|
||||||
await mainStatusLoaded(status)
|
mainStatusLoaded(status)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
private func resolveStatusIfNecessary() async -> String? {
|
private func resolveStatus(url: URL) async -> String? {
|
||||||
switch mode {
|
|
||||||
case .localID(let id):
|
|
||||||
return id
|
|
||||||
case .resolve(let url):
|
|
||||||
let indicator = UIActivityIndicatorView(style: .medium)
|
let indicator = UIActivityIndicatorView(style: .medium)
|
||||||
indicator.startAnimating()
|
indicator.startAnimating()
|
||||||
state = .loading(indicator)
|
state = .loading(indicator)
|
||||||
|
|
||||||
let url = WebURL(url)!
|
let url = WebURL(url)!.serialized(excludingFragment: true)
|
||||||
let request = Client.search(query: url.serialized(), types: [.statuses], resolve: true)
|
let request = Client.search(query: url, types: [.statuses], resolve: true)
|
||||||
do {
|
do {
|
||||||
let (results, _) = try await mastodonController.run(request)
|
let (results, _) = try await mastodonController.run(request)
|
||||||
guard let status = results.statuses.first(where: { $0.url == url }) else {
|
guard let status = results.statuses.first(where: { $0.url?.serialized() == url }) else {
|
||||||
throw UnableToResolveError()
|
throw UnableToResolveError()
|
||||||
}
|
}
|
||||||
_ = mastodonController.persistentContainer.addOrUpdateOnViewContext(status: status)
|
_ = mastodonController.persistentContainer.addOrUpdateOnViewContext(status: status)
|
||||||
|
@ -204,47 +225,84 @@ class ConversationViewController: UIViewController {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func mainStatusLoaded(_ mainStatus: StatusMO) {
|
||||||
|
if let accountID = mastodonController.accountInfo?.id {
|
||||||
|
userActivity = UserActivityManager.showConversationActivity(mainStatusID: mainStatus.id, accountID: accountID)
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
let vc = ConversationCollectionViewController(for: mainStatus.id, state: mainStatusState, conversationViewController: self)
|
||||||
private func mainStatusLoaded(_ mainStatus: StatusMO) async {
|
|
||||||
let vc = ConversationCollectionViewController(for: mainStatus.id, state: mainStatusState, mastodonController: mastodonController)
|
|
||||||
vc.statusIDToScrollToOnLoad = statusIDToScrollToOnLoad ?? mainStatus.id
|
vc.statusIDToScrollToOnLoad = statusIDToScrollToOnLoad ?? mainStatus.id
|
||||||
vc.showStatusesAutomatically = showStatusesAutomatically
|
vc.showStatusesAutomatically = showStatusesAutomatically
|
||||||
vc.addMainStatus(mainStatus)
|
vc.addMainStatus(mainStatus)
|
||||||
state = .displaying(vc)
|
state = .displaying(vc)
|
||||||
|
|
||||||
await loadContext(for: mainStatus)
|
if case .preloaded(let tree) = mode {
|
||||||
|
vc.addTree(tree, mainStatus: mainStatus)
|
||||||
|
} else {
|
||||||
|
Task { @MainActor in
|
||||||
|
await loadTree(for: mainStatus)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
private func loadContext(for mainStatus: StatusMO) async {
|
private func loadTree(for mainStatus: StatusMO) async {
|
||||||
guard case .displaying(_) = state else {
|
guard case .displaying(_) = state,
|
||||||
|
let context = await loadContext(for: mainStatus) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let request = Status.getContext(mainStatus.id)
|
await mastodonController.persistentContainer.addAll(statuses: context.ancestors + context.descendants)
|
||||||
do {
|
|
||||||
let (context, _) = try await mastodonController.run(request)
|
let ancestorIDs = context.ancestors.map(\.id)
|
||||||
|
let ancestorsReq = StatusMO.fetchRequest()
|
||||||
|
ancestorsReq.predicate = NSPredicate(format: "id in %@", ancestorIDs)
|
||||||
|
let ancestors = try? mastodonController.persistentContainer.viewContext.fetch(ancestorsReq)
|
||||||
|
|
||||||
|
let descendantIDs = context.descendants.map(\.id)
|
||||||
|
let descendantsReq = StatusMO.fetchRequest()
|
||||||
|
descendantsReq.predicate = NSPredicate(format: "id IN %@", descendantIDs)
|
||||||
|
let descendants = try? mastodonController.persistentContainer.viewContext.fetch(descendantsReq)
|
||||||
|
|
||||||
|
let tree = ConversationTree.build(for: mainStatus, ancestors: ancestors ?? [], descendants: descendants ?? [])
|
||||||
|
|
||||||
guard case .displaying(let vc) = state else {
|
guard case .displaying(let vc) = state else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
vc.addTree(tree, mainStatus: mainStatus)
|
||||||
|
}
|
||||||
|
|
||||||
await vc.addContext(context, for: mainStatus)
|
private func loadContext(for mainStatus: StatusMO) async -> ConversationContext? {
|
||||||
|
let request = Status.getContext(mainStatus.id)
|
||||||
|
do {
|
||||||
|
let (context, _) = try await mastodonController.run(request)
|
||||||
|
return context
|
||||||
} catch {
|
} catch {
|
||||||
guard case .displaying(_) = state else {
|
guard case .displaying(_) = state else {
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
let error = error as! Client.Error
|
let error = error as! Client.Error
|
||||||
|
|
||||||
let config = ToastConfiguration(from: error, with: "Error Loading Context", in: self) { [weak self] toast in
|
let config = ToastConfiguration(from: error, with: "Error Loading Context", in: self) { [weak self] toast in
|
||||||
toast.dismissToast(animated: true)
|
toast.dismissToast(animated: true)
|
||||||
await self?.loadContext(for: mainStatus)
|
await self?.loadTree(for: mainStatus)
|
||||||
}
|
}
|
||||||
self.showToast(configuration: config, animated: true)
|
self.showToast(configuration: config, animated: true)
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func refreshContext() async {
|
||||||
|
guard case .localID(let id) = mode,
|
||||||
|
let status = mastodonController.persistentContainer.status(for: id),
|
||||||
|
case .displaying(_) = state else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await loadTree(for: status)
|
||||||
|
}
|
||||||
|
|
||||||
private func showMainStatusNotFound() {
|
private func showMainStatusNotFound() {
|
||||||
let notFoundView = StatusNotFoundView(frame: .zero)
|
let notFoundView = StatusNotFoundView(frame: .zero)
|
||||||
notFoundView.translatesAutoresizingMaskIntoConstraints = false
|
notFoundView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
@ -341,6 +399,7 @@ extension ConversationViewController {
|
||||||
enum Mode {
|
enum Mode {
|
||||||
case localID(String)
|
case localID(String)
|
||||||
case resolve(URL)
|
case resolve(URL)
|
||||||
|
case preloaded(ConversationTree)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -366,6 +425,17 @@ extension ConversationViewController: TuskerNavigationDelegate {
|
||||||
var apiController: MastodonController! { mastodonController }
|
var apiController: MastodonController! { mastodonController }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension ConversationViewController: StateRestorableViewController {
|
||||||
|
func stateRestorationActivity() -> NSUserActivity? {
|
||||||
|
if let accountID = mastodonController.accountInfo?.id,
|
||||||
|
case .localID(let id) = mode {
|
||||||
|
return UserActivityManager.showConversationActivity(mainStatusID: id, accountID: accountID)
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
extension ConversationViewController: ToastableViewController {
|
extension ConversationViewController: ToastableViewController {
|
||||||
var toastScrollView: UIScrollView? {
|
var toastScrollView: UIScrollView? {
|
||||||
if case .displaying(let vc) = state {
|
if case .displaying(let vc) = state {
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue