Compare commits

..

No commits in common. "ee630cf9df26915787c354b7cbaf8dafac3e7d7b" and "8fc915d6a0534cee63881013f5d2a7d8cf567f91" have entirely different histories.

223 changed files with 2107 additions and 6470 deletions

View File

@ -1,47 +0,0 @@
## 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

View File

@ -1,97 +1,5 @@
# 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

View File

@ -1,7 +0,0 @@
# Haptic Feedback
## Selection changed
`UISelectionFeedbackGenerator`
## Actions
On success, `UIImpactFeedbackGenerator` with the `.light` style. On error, `UINotificationFeedbackGenerator` with the `.error` type.

View File

@ -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: .light).impactOccurred() UIImpactFeedbackGenerator(style: .soft).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) {

View File

@ -155,27 +155,6 @@ 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
@ -199,10 +178,8 @@ public class Client {
return Request<Account>(method: .get, path: "/api/v1/accounts/verify_credentials") return Request<Account>(method: .get, path: "/api/v1/accounts/verify_credentials")
} }
public static func getFavourites(range: RequestRange = .default) -> Request<[Status]> { public static func getFavourites() -> Request<[Status]> {
var request = Request<[Status]>(method: .get, path: "/api/v1/favourites") return Request<[Status]>(method: .get, path: "/api/v1/favourites")
request.range = range
return request
} }
public static func getRelationships(accounts: [String]? = nil) -> Request<[Relationship]> { public static func getRelationships(accounts: [String]? = nil) -> Request<[Relationship]> {
@ -417,46 +394,32 @@ public class Client {
} }
// MARK: - Instance // MARK: - Instance
public static func getTrendingHashtagsDeprecated(limit: Int? = nil, offset: Int? = nil) -> Request<[Hashtag]> { public static func getTrendingHashtags(limit: Int? = nil) -> Request<[Hashtag]> {
var parameters: [Parameter] = [] let parameters: [Parameter]
if let limit { if let limit = limit {
parameters.append("limit" => limit) parameters = ["limit" => limit]
} } else {
if let offset { parameters = []
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 getTrendingHashtags(limit: Int? = nil, offset: Int? = nil) -> Request<[Hashtag]> { public static func getTrendingStatuses(limit: Int? = nil) -> Request<[Status]> {
var parameters: [Parameter] = [] let parameters: [Parameter]
if let limit { if let limit = limit {
parameters.append("limit" => limit) parameters = ["limit" => limit]
} } else {
if let offset { parameters = []
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, offset: Int? = nil) -> Request<[Card]> { public static func getTrendingLinks(limit: Int? = nil) -> Request<[Card]> {
var parameters: [Parameter] = [] let parameters: [Parameter]
if let limit { if let limit = limit {
parameters.append("limit" => limit) parameters = ["limit" => limit]
} } else {
if let offset { parameters = []
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)
} }
@ -484,7 +447,7 @@ public class Client {
} }
extension Client { extension Client {
public struct Error: LocalizedError, Sendable { public struct Error: LocalizedError {
public let requestMethod: Method public let requestMethod: Method
public let requestEndpoint: Endpoint public let requestEndpoint: Endpoint
public let type: ErrorType public let type: ErrorType
@ -519,7 +482,7 @@ extension Client {
} }
} }
} }
public enum ErrorType: LocalizedError, Sendable { public enum ErrorType: LocalizedError {
case networkError(Swift.Error) case networkError(Swift.Error)
case unexpectedStatus(Int) case unexpectedStatus(Int)
case invalidRequest case invalidRequest

View File

@ -8,7 +8,7 @@
import Foundation import Foundation
public final class Account: AccountProtocol, Decodable, Sendable { public final class Account: AccountProtocol, Decodable {
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, Sendable {
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 let emojis: [Emoji] public private(set) var 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,12 +109,6 @@ public final class Account: AccountProtocol, Decodable, Sendable {
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")
} }
@ -171,7 +165,7 @@ extension Account: CustomDebugStringConvertible {
} }
extension Account { extension Account {
public struct Field: Codable, Equatable, Sendable { public struct Field: Codable {
public let name: String public let name: String
public let value: String public let value: String
public let verifiedAt: Date? public let verifiedAt: Date?

View File

@ -8,11 +8,11 @@
import Foundation import Foundation
public struct Application: Decodable, Sendable { public class Application: Decodable {
public let name: String public let name: String
public let website: URL? public let website: URL?
public init(from decoder: Decoder) throws { public required 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)

View File

@ -8,7 +8,7 @@
import Foundation import Foundation
public struct Attachment: Codable, Sendable { public class Attachment: Codable {
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 struct Attachment: Codable, Sendable {
], nil)) ], nil))
} }
public init(from decoder: Decoder) throws { required 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 struct Attachment: Codable, Sendable {
} }
extension Attachment { extension Attachment {
public enum Kind: String, Codable, Sendable { public enum Kind: String, Codable {
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, Sendable { public struct Metadata: Codable {
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, Sendable { public struct ImageMetadata: Codable {
public let width: Int? public let width: Int?
public let height: Int? public let height: Int?
public let size: String? public let size: String?

View File

@ -9,7 +9,7 @@
import Foundation import Foundation
import WebURL import WebURL
public struct Card: Codable, Sendable { public class Card: Codable {
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 struct Card: Codable, Sendable {
/// 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 init(from decoder: Decoder) throws { public required 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 struct Card: Codable, Sendable {
} }
extension Card { extension Card {
public enum Kind: String, Codable, Sendable { public enum Kind: String, Codable {
case link case link
case photo case photo
case video case video

View File

@ -8,7 +8,7 @@
import Foundation import Foundation
public struct ConversationContext: Decodable, Sendable { public class ConversationContext: Decodable {
public let ancestors: [Status] public let ancestors: [Status]
public let descendants: [Status] public let descendants: [Status]

View File

@ -8,7 +8,7 @@
import Foundation import Foundation
public enum DirectoryOrder: String, CaseIterable, Sendable { public enum DirectoryOrder: String, CaseIterable {
case active case active
case new case new
} }

View File

@ -9,7 +9,7 @@
import Foundation import Foundation
import WebURL import WebURL
public struct Emoji: Codable, Sendable { public class Emoji: Codable {
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 struct Emoji: Codable, Sendable {
public let visibleInPicker: Bool public let visibleInPicker: Bool
public let category: String? public let category: String?
public init(from decoder: Decoder) throws { public required 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)

View File

@ -8,7 +8,7 @@
import Foundation import Foundation
public struct FilterV1: Decodable, Sendable { public struct FilterV1: Decodable {
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, Sendable {
} }
extension FilterV1 { extension FilterV1 {
public enum Context: String, Decodable, CaseIterable, Sendable { public enum Context: String, Decodable, CaseIterable {
case home case home
case notifications case notifications
case `public` case `public`

View File

@ -7,7 +7,7 @@
import Foundation import Foundation
public struct FilterV2: Decodable, Sendable { public struct FilterV2: Decodable {
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, Sendable {
} }
extension FilterV2 { extension FilterV2 {
public enum Action: String, Decodable, Hashable, CaseIterable, Sendable { public enum Action: String, Decodable, Hashable, CaseIterable {
case warn case warn
case hide case hide
} }
} }
extension FilterV2 { extension FilterV2 {
public struct Keyword: Decodable, Sendable { public struct Keyword: Decodable {
public let id: String public let id: String
public let keyword: String public let keyword: String
public let wholeWord: Bool public let wholeWord: Bool

View File

@ -10,7 +10,7 @@ import Foundation
import WebURL import WebURL
import WebURLFoundationExtras import WebURLFoundationExtras
public struct Hashtag: Codable, Sendable { public class Hashtag: Codable {
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 struct Hashtag: Codable, Sendable {
self.following = nil self.following = nil
} }
public init(from decoder: Decoder) throws { public required 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

View File

@ -8,12 +8,12 @@
import Foundation import Foundation
public struct History: Codable, Sendable { public class History: Codable {
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 init(from decoder: Decoder) throws { public required 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) {

View File

@ -8,7 +8,7 @@
import Foundation import Foundation
public struct Instance: Decodable, Sendable { public class Instance: Decodable {
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 struct Instance: Decodable, Sendable {
} }
// 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 init(from decoder: Decoder) throws { public required 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 struct Instance: Decodable, Sendable {
} }
extension Instance { extension Instance {
public struct Stats: Decodable, Sendable { public struct Stats: Decodable {
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, Sendable { public struct Configuration: Decodable {
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, Sendable { public struct StatusesConfiguration: Decodable {
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, Sendable { public struct MediaAttachmentsConfiguration: Decodable {
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, Sendable { public struct PollsConfiguration: Decodable {
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, Sendable { public struct Rule: Decodable, Identifiable {
public let id: String public let id: String
public let text: String public let text: String
} }

View File

@ -8,7 +8,7 @@
import Foundation import Foundation
public struct List: ListProtocol, Decodable, Equatable, Hashable, Sendable { public class List: Decodable, Equatable, Hashable {
public let id: String public let id: String
public let title: String public let title: String
@ -16,11 +16,6 @@ public struct List: ListProtocol, Decodable, Equatable, Hashable, Sendable {
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
} }
@ -30,28 +25,28 @@ public struct List: ListProtocol, Decodable, Equatable, Hashable, Sendable {
hasher.combine(title) hasher.combine(title)
} }
public static func getAccounts(_ listID: String, range: RequestRange = .default) -> Request<[Account]> { public static func getAccounts(_ list: List, range: RequestRange = .default) -> Request<[Account]> {
var request = Request<[Account]>(method: .get, path: "/api/v1/lists/\(listID)/accounts") var request = Request<[Account]>(method: .get, path: "/api/v1/lists/\(list.id)/accounts")
request.range = range request.range = range
return request return request
} }
public static func update(_ listID: String, title: String) -> Request<List> { public static func update(_ list: List, title: String) -> Request<List> {
return Request<List>(method: .put, path: "/api/v1/lists/\(listID)", body: ParametersBody(["title" => title])) return Request<List>(method: .put, path: "/api/v1/lists/\(list.id)", body: ParametersBody(["title" => title]))
} }
public static func delete(_ listID: String) -> Request<Empty> { public static func delete(_ list: List) -> Request<Empty> {
return Request<Empty>(method: .delete, path: "/api/v1/lists/\(listID)") return Request<Empty>(method: .delete, path: "/api/v1/lists/\(list.id)")
} }
public static func add(_ listID: String, accounts accountIDs: [String]) -> Request<Empty> { public static func add(_ list: List, accounts accountIDs: [String]) -> Request<Empty> {
return Request<Empty>(method: .post, path: "/api/v1/lists/\(listID)/accounts", body: ParametersBody( return Request<Empty>(method: .post, path: "/api/v1/lists/\(list.id)/accounts", body: ParametersBody(
"account_ids" => accountIDs "account_ids" => accountIDs
)) ))
} }
public static func remove(_ listID: String, accounts accountIDs: [String]) -> Request<Empty> { public static func remove(_ list: List, accounts accountIDs: [String]) -> Request<Empty> {
return Request<Empty>(method: .delete, path: "/api/v1/lists/\(listID)/accounts", body: ParametersBody( return Request<Empty>(method: .delete, path: "/api/v1/lists/\(list.id)/accounts", body: ParametersBody(
"account_ids" => accountIDs "account_ids" => accountIDs
)) ))
} }

View File

@ -8,7 +8,7 @@
import Foundation import Foundation
public struct LoginSettings: Decodable, Sendable { public class LoginSettings: Decodable {
public let accessToken: String public let accessToken: String
private let scope: String? private let scope: String?

View File

@ -9,7 +9,7 @@
import Foundation import Foundation
import WebURL import WebURL
public struct Mention: Codable, Sendable { public struct Mention: Codable {
public let url: WebURL public let url: WebURL
public let username: String public let username: String
public let acct: String public let acct: String

View File

@ -8,11 +8,11 @@
import Foundation import Foundation
public struct NodeInfo: Decodable, Sendable { public struct NodeInfo: Decodable {
public let version: String public let version: String
public let software: Software public let software: Software
public struct Software: Decodable, Sendable { public struct Software: Decodable {
public let name: String public let name: String
public let version: String public let version: String
} }

View File

@ -8,14 +8,14 @@
import Foundation import Foundation
public struct Notification: Decodable, Sendable { public class Notification: Decodable {
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 init(from decoder: Decoder) throws { public required 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 struct Notification: Decodable, Sendable {
} }
extension Notification { extension Notification {
public enum Kind: String, Decodable, CaseIterable, Sendable { public enum Kind: String, Decodable, CaseIterable {
case mention case mention
case reblog case reblog
case favourite case favourite

View File

@ -8,7 +8,7 @@
import Foundation import Foundation
public struct Poll: Codable, Sendable { public final class Poll: Codable {
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 struct Poll: Codable, Sendable {
} }
extension Poll { extension Poll {
public struct Option: Codable, Sendable { public final class Option: Codable {
public let title: String public let title: String
public let votesCount: Int? public let votesCount: Int?

View File

@ -1,13 +0,0 @@
//
// ListProtocol.swift
// Pachyderm
//
// Created by Shadowfacts on 2/25/23.
//
import Foundation
public protocol ListProtocol {
var id: String { get }
var title: String { get }
}

View File

@ -8,7 +8,7 @@
import Foundation import Foundation
public struct PushSubscription: Decodable, Sendable { public class PushSubscription: Decodable {
public let id: String public let id: String
public let endpoint: URL public let endpoint: URL
public let serverKey: String public let serverKey: String

View File

@ -8,12 +8,12 @@
import Foundation import Foundation
public struct RegisteredApplication: Decodable, Sendable { public class RegisteredApplication: Decodable {
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 init(from decoder: Decoder) throws { public required 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

View File

@ -8,7 +8,7 @@
import Foundation import Foundation
public struct Relationship: Decodable, Sendable { public class Relationship: Decodable {
public let id: String public let id: String
public let following: Bool public let following: Bool
public let followedBy: Bool public let followedBy: Bool

View File

@ -8,7 +8,7 @@
import Foundation import Foundation
public struct Report: Decodable, Sendable { public class Report: Decodable {
public let id: String public let id: String
public let actionTaken: Bool public let actionTaken: Bool

View File

@ -8,7 +8,7 @@
import Foundation import Foundation
public enum Scope: String, Sendable { public enum Scope: String {
case read case read
case write case write
case follow case follow

View File

@ -8,7 +8,7 @@
import Foundation import Foundation
public enum SearchResultType: String, Sendable { public enum SearchResultType: String {
case accounts case accounts
case hashtags case hashtags
case statuses case statuses

View File

@ -8,7 +8,7 @@
import Foundation import Foundation
public struct SearchResults: Decodable, Sendable { public class SearchResults: Decodable {
public let accounts: [Account] public let accounts: [Account]
public let statuses: [Status] public let statuses: [Status]
public let hashtags: [Hashtag] public let hashtags: [Hashtag]

View File

@ -9,7 +9,7 @@
import Foundation import Foundation
import WebURL import WebURL
public final class Status: StatusProtocol, Decodable, Sendable { public final class Status: StatusProtocol, Decodable {
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,47 +44,6 @@ public final class Status: StatusProtocol, Decodable, Sendable {
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")
} }
@ -188,7 +147,7 @@ public final class Status: StatusProtocol, Decodable, Sendable {
} }
extension Status { extension Status {
public enum Visibility: String, Codable, CaseIterable, Sendable { public enum Visibility: String, Codable, CaseIterable {
case `public` case `public`
case unlisted case unlisted
case `private` case `private`

View File

@ -8,7 +8,7 @@
import Foundation import Foundation
public enum StatusContentType: String, Codable, CaseIterable, Sendable { public enum StatusContentType: String, Codable, CaseIterable {
case plain, markdown, html case plain, markdown, html
var mimeType: String { var mimeType: String {

View File

@ -7,7 +7,7 @@
import Foundation import Foundation
public struct Suggestion: Decodable, Sendable { public struct Suggestion: Decodable {
public let source: Source public let source: Source
public let account: Account public let account: Account
@ -17,7 +17,7 @@ public struct Suggestion: Decodable, Sendable {
} }
extension Suggestion { extension Suggestion {
public enum Source: String, Decodable, Sendable { public enum Source: String, Decodable {
case staff case staff
case pastInteractions = "past_interactions" case pastInteractions = "past_interactions"
case global case global

View File

@ -8,7 +8,7 @@
import Foundation import Foundation
public enum Timeline: Equatable, Hashable, Sendable { public enum Timeline: Equatable, Hashable {
case home case home
case `public`(local: Bool) case `public`(local: Bool)
case tag(hashtag: String) case tag(hashtag: String)

View File

@ -1,40 +0,0 @@
//
// 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"
}
}
}

View File

@ -8,7 +8,7 @@
import Foundation import Foundation
struct WellKnown: Decodable, Sendable { struct WellKnown: Decodable {
let links: [Link] let links: [Link]
struct Link: Decodable { struct Link: Decodable {

View File

@ -8,7 +8,7 @@
import Foundation import Foundation
protocol Body: Sendable { protocol Body {
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 & Sendable>: Body { struct JsonBody<T: Encodable>: Body {
let value: T let value: T
init(_ value: T) { init(_ value: T) {

View File

@ -8,7 +8,7 @@
import Foundation import Foundation
public struct Endpoint: ExpressibleByStringInterpolation, CustomStringConvertible, Sendable { public struct Endpoint: ExpressibleByStringInterpolation, CustomStringConvertible {
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: Sendable { enum Component {
case literal(String) case literal(String)
case interpolated(String) case interpolated(String)
} }

View File

@ -8,7 +8,7 @@
import Foundation import Foundation
public struct FormAttachment: Sendable { public struct FormAttachment {
let mimeType: String let mimeType: String
let data: Data let data: Data
let fileName: String let fileName: String

View File

@ -8,7 +8,7 @@
import Foundation import Foundation
public enum Method: Sendable { public enum Method {
case get, post, put, patch, delete case get, post, put, patch, delete
} }

View File

@ -8,7 +8,7 @@
import Foundation import Foundation
struct Parameter: Sendable { struct Parameter {
let name: String let name: String
let value: String? let value: String?
} }

View File

@ -8,7 +8,7 @@
import Foundation import Foundation
public struct Request<ResultType: Decodable>: Sendable { public struct Request<ResultType: Decodable> {
let method: Method let method: Method
let endpoint: Endpoint let endpoint: Endpoint
let body: Body let body: Body

View File

@ -8,24 +8,13 @@
import Foundation import Foundation
public enum RequestRange: Sendable { public enum RequestRange {
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 {

View File

@ -8,6 +8,6 @@
import Foundation import Foundation
public struct Empty: Decodable, Sendable { public struct Empty: Decodable {
} }

View File

@ -8,7 +8,7 @@
import Foundation import Foundation
public struct Pagination: Sendable { public struct Pagination {
public let older: RequestRange? public let older: RequestRange?
public let newer: RequestRange? public let newer: RequestRange?
} }

View File

@ -8,7 +8,7 @@
import Foundation import Foundation
public enum Response<Result: Decodable & Sendable>: Sendable { public enum Response<Result: Decodable> {
case success(Result, Pagination?) case success(Result, Pagination?)
case failure(Client.Error) case failure(Client.Error)
} }

View File

@ -8,8 +8,7 @@
import Foundation import Foundation
@MainActor public class CollapseState: Equatable {
public final class CollapseState: Sendable {
public var collapsible: Bool? public var collapsible: Bool?
public var collapsed: Bool? public var collapsed: Bool?
@ -34,4 +33,8 @@ public final class CollapseState: Sendable {
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
}
} }

View File

@ -14,7 +14,6 @@ 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
@ -52,7 +51,6 @@ 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 {

View File

@ -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:
return fatalError()
} }
controller.play(on: update.subBoard, column: update.column, row: update.row) controller.play(on: update.subBoard, column: update.column, row: update.row)
} }

View File

@ -6,7 +6,6 @@
// //
import Foundation import Foundation
import Combine
public class GameController: ObservableObject { public class GameController: ObservableObject {

View File

@ -20,9 +20,6 @@
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 */; };
@ -41,6 +38,8 @@
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 */; };
@ -103,6 +102,7 @@
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,7 +200,6 @@
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 */; };
@ -215,16 +214,11 @@
D68ACE5D279B1ABA001CE8EB /* AssetPickerControlCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68ACE5C279B1ABA001CE8EB /* AssetPickerControlCollectionViewCell.swift */; }; D68ACE5D279B1ABA001CE8EB /* AssetPickerControlCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68ACE5C279B1ABA001CE8EB /* AssetPickerControlCollectionViewCell.swift */; };
D68C2AE325869BAB00548EFF /* AuxiliarySceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68C2AE225869BAB00548EFF /* AuxiliarySceneDelegate.swift */; }; D68C2AE325869BAB00548EFF /* AuxiliarySceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68C2AE225869BAB00548EFF /* AuxiliarySceneDelegate.swift */; };
D68E525B24A3D77E0054355A /* TuskerRootViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68E525A24A3D77E0054355A /* TuskerRootViewController.swift */; }; D68E525B24A3D77E0054355A /* TuskerRootViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68E525A24A3D77E0054355A /* TuskerRootViewController.swift */; };
D68E525D24A3E8F00054355A /* InlineTrendsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68E525C24A3E8F00054355A /* InlineTrendsViewController.swift */; }; D68E525D24A3E8F00054355A /* SearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68E525C24A3E8F00054355A /* SearchViewController.swift */; };
D68E6F59253C9969001A1B4C /* MultiSourceEmojiLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68E6F58253C9969001A1B4C /* MultiSourceEmojiLabel.swift */; }; D68E6F59253C9969001A1B4C /* MultiSourceEmojiLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68E6F58253C9969001A1B4C /* MultiSourceEmojiLabel.swift */; };
D68E6F5F253C9B2D001A1B4C /* BaseEmojiLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68E6F5E253C9B2D001A1B4C /* BaseEmojiLabel.swift */; }; D68E6F5F253C9B2D001A1B4C /* BaseEmojiLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68E6F5E253C9B2D001A1B4C /* BaseEmojiLabel.swift */; };
D68FEC4F232C5BC300C84F23 /* SegmentedPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68FEC4E232C5BC300C84F23 /* SegmentedPageViewController.swift */; }; D68FEC4F232C5BC300C84F23 /* SegmentedPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68FEC4E232C5BC300C84F23 /* SegmentedPageViewController.swift */; };
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 */; };
@ -257,6 +251,7 @@
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 */; };
@ -298,12 +293,6 @@
D6BED174212667E900F02DA0 /* TimelineStatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BED173212667E900F02DA0 /* TimelineStatusTableViewCell.swift */; }; D6BED174212667E900F02DA0 /* TimelineStatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BED173212667E900F02DA0 /* TimelineStatusTableViewCell.swift */; };
D6C143DA253510F4007DC240 /* ComposeEmojiTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C143D9253510F4007DC240 /* ComposeEmojiTextField.swift */; }; D6C143DA253510F4007DC240 /* ComposeEmojiTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C143D9253510F4007DC240 /* ComposeEmojiTextField.swift */; };
D6C1B2082545D1EC00DAAA66 /* StatusCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C1B2072545D1EC00DAAA66 /* StatusCardView.swift */; }; D6C1B2082545D1EC00DAAA66 /* StatusCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C1B2072545D1EC00DAAA66 /* StatusCardView.swift */; };
D6C3F4F5298ED0890009FCFF /* LocalPredicateStatusesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C3F4F4298ED0890009FCFF /* LocalPredicateStatusesViewController.swift */; };
D6C3F4F7298ED7F70009FCFF /* FavoritesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C3F4F6298ED7F70009FCFF /* FavoritesViewController.swift */; };
D6C3F4F9298EDBF20009FCFF /* ConversationTree.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C3F4F8298EDBF20009FCFF /* ConversationTree.swift */; };
D6C3F4FB299035650009FCFF /* TrendsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C3F4FA299035650009FCFF /* TrendsViewController.swift */; };
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 */; };
@ -330,16 +319,10 @@
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 */; };
@ -439,9 +422,6 @@
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>"; };
@ -459,6 +439,8 @@
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>"; };
@ -520,6 +502,7 @@
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>"; };
@ -600,7 +583,6 @@
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>"; };
@ -619,7 +601,6 @@
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>"; };
@ -635,16 +616,11 @@
D68ACE5C279B1ABA001CE8EB /* AssetPickerControlCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetPickerControlCollectionViewCell.swift; sourceTree = "<group>"; }; D68ACE5C279B1ABA001CE8EB /* AssetPickerControlCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetPickerControlCollectionViewCell.swift; sourceTree = "<group>"; };
D68C2AE225869BAB00548EFF /* AuxiliarySceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuxiliarySceneDelegate.swift; sourceTree = "<group>"; }; D68C2AE225869BAB00548EFF /* AuxiliarySceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuxiliarySceneDelegate.swift; sourceTree = "<group>"; };
D68E525A24A3D77E0054355A /* TuskerRootViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TuskerRootViewController.swift; sourceTree = "<group>"; }; D68E525A24A3D77E0054355A /* TuskerRootViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TuskerRootViewController.swift; sourceTree = "<group>"; };
D68E525C24A3E8F00054355A /* InlineTrendsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlineTrendsViewController.swift; sourceTree = "<group>"; }; D68E525C24A3E8F00054355A /* SearchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewController.swift; sourceTree = "<group>"; };
D68E6F58253C9969001A1B4C /* MultiSourceEmojiLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiSourceEmojiLabel.swift; sourceTree = "<group>"; }; D68E6F58253C9969001A1B4C /* MultiSourceEmojiLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiSourceEmojiLabel.swift; sourceTree = "<group>"; };
D68E6F5E253C9B2D001A1B4C /* BaseEmojiLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseEmojiLabel.swift; sourceTree = "<group>"; }; D68E6F5E253C9B2D001A1B4C /* BaseEmojiLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseEmojiLabel.swift; sourceTree = "<group>"; };
D68FEC4E232C5BC300C84F23 /* SegmentedPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SegmentedPageViewController.swift; sourceTree = "<group>"; }; D68FEC4E232C5BC300C84F23 /* SegmentedPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SegmentedPageViewController.swift; sourceTree = "<group>"; };
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>"; };
@ -677,6 +653,7 @@
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>"; };
@ -718,12 +695,6 @@
D6BED173212667E900F02DA0 /* TimelineStatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStatusTableViewCell.swift; sourceTree = "<group>"; }; D6BED173212667E900F02DA0 /* TimelineStatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStatusTableViewCell.swift; sourceTree = "<group>"; };
D6C143D9253510F4007DC240 /* ComposeEmojiTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeEmojiTextField.swift; sourceTree = "<group>"; }; D6C143D9253510F4007DC240 /* ComposeEmojiTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeEmojiTextField.swift; sourceTree = "<group>"; };
D6C1B2072545D1EC00DAAA66 /* StatusCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusCardView.swift; sourceTree = "<group>"; }; D6C1B2072545D1EC00DAAA66 /* StatusCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusCardView.swift; sourceTree = "<group>"; };
D6C3F4F4298ED0890009FCFF /* LocalPredicateStatusesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalPredicateStatusesViewController.swift; sourceTree = "<group>"; };
D6C3F4F6298ED7F70009FCFF /* FavoritesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesViewController.swift; sourceTree = "<group>"; };
D6C3F4F8298EDBF20009FCFF /* ConversationTree.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationTree.swift; sourceTree = "<group>"; };
D6C3F4FA299035650009FCFF /* TrendsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendsViewController.swift; sourceTree = "<group>"; };
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>"; };
@ -757,16 +728,10 @@
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>"; };
@ -870,6 +835,8 @@
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";
@ -885,7 +852,6 @@
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>";
@ -907,7 +873,6 @@
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>";
@ -942,7 +907,6 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
D6C82B4025C5BB7E0017F1E6 /* ExploreViewController.swift */, D6C82B4025C5BB7E0017F1E6 /* ExploreViewController.swift */,
D68E525C24A3E8F00054355A /* InlineTrendsViewController.swift */,
D6945C3323AC6431005C403C /* AddSavedHashtagViewController.swift */, D6945C3323AC6431005C403C /* AddSavedHashtagViewController.swift */,
D6093F9A25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift */, D6093F9A25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift */,
D6945C3923AC75E2005C403C /* FindInstanceViewController.swift */, D6945C3923AC75E2005C403C /* FindInstanceViewController.swift */,
@ -957,21 +921,16 @@
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 /* Local Predicate Statuses List */ = { D627944823A6AD5100D38C68 /* Bookmarks */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
D6C3F4F4298ED0890009FCFF /* LocalPredicateStatusesViewController.swift */, D627944923A6AD6100D38C68 /* BookmarksTableViewController.swift */,
D6DD8FFE2984D327002AD3FD /* BookmarksViewController.swift */,
D6C3F4F6298ED7F70009FCFF /* FavoritesViewController.swift */,
); );
path = "Local Predicate Statuses List"; path = Bookmarks;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
D627944B23A9A02400D38C68 /* Lists */ = { D627944B23A9A02400D38C68 /* Lists */ = {
@ -988,7 +947,6 @@
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 */,
); );
@ -1035,6 +993,7 @@
D6A3BC822321F69400FD64D5 /* Account List */, D6A3BC822321F69400FD64D5 /* Account List */,
D6B053A023BD2BED00A066FA /* Asset Picker */, D6B053A023BD2BED00A066FA /* Asset Picker */,
0411610522B457290030A9B7 /* Attachment Gallery */, 0411610522B457290030A9B7 /* Attachment Gallery */,
D627944823A6AD5100D38C68 /* Bookmarks */,
D641C787213DD862004B4513 /* Compose */, D641C787213DD862004B4513 /* Compose */,
D641C785213DD83B004B4513 /* Conversation */, D641C785213DD83B004B4513 /* Conversation */,
D6F2E960249E772F005846BB /* Crash Reporter */, D6F2E960249E772F005846BB /* Crash Reporter */,
@ -1043,7 +1002,6 @@
D61F759729384D4200C0B37F /* Customize Timelines */, D61F759729384D4200C0B37F /* Customize Timelines */,
D641C788213DD86D004B4513 /* Large Image */, D641C788213DD86D004B4513 /* Large Image */,
D627944B23A9A02400D38C68 /* Lists */, D627944B23A9A02400D38C68 /* Lists */,
D627944823A6AD5100D38C68 /* Local Predicate Statuses List */,
D641C782213DD7F0004B4513 /* Main */, D641C782213DD7F0004B4513 /* Main */,
D6F6A555291F4F0C00F496A8 /* Mute */, D6F6A555291F4F0C00F496A8 /* Mute */,
D641C786213DD852004B4513 /* Notifications */, D641C786213DD852004B4513 /* Notifications */,
@ -1068,7 +1026,6 @@
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>";
@ -1103,7 +1060,6 @@
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>";
@ -1114,7 +1070,6 @@
D65B4B6329771EFF00DABDFB /* ConversationViewController.swift */, D65B4B6329771EFF00DABDFB /* ConversationViewController.swift */,
D601FA5C297B2E6F00A8E8B5 /* ConversationCollectionViewController.swift */, D601FA5C297B2E6F00A8E8B5 /* ConversationCollectionViewController.swift */,
D601FA5E297B339100A8E8B5 /* ExpandThreadCollectionViewCell.swift */, D601FA5E297B339100A8E8B5 /* ExpandThreadCollectionViewCell.swift */,
D6C3F4F8298EDBF20009FCFF /* ConversationTree.swift */,
); );
path = Conversation; path = Conversation;
sourceTree = "<group>"; sourceTree = "<group>";
@ -1201,7 +1156,6 @@
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>";
@ -1213,7 +1167,6 @@
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>";
@ -1296,7 +1249,6 @@
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>";
@ -1326,8 +1278,6 @@
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>";
@ -1439,6 +1389,7 @@
D6BC9DD8232D8BCA002CA326 /* Search */ = { D6BC9DD8232D8BCA002CA326 /* Search */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
D68E525C24A3E8F00054355A /* SearchViewController.swift */,
D6BC9DD9232D8BE5002CA326 /* SearchResultsViewController.swift */, D6BC9DD9232D8BE5002CA326 /* SearchResultsViewController.swift */,
); );
path = Search; path = Search;
@ -1453,7 +1404,6 @@
D6ADB6EF28ED1F25009924AB /* CachedImageView.swift */, D6ADB6EF28ED1F25009924AB /* CachedImageView.swift */,
D6895DC328D65342006341DA /* ConfirmReblogStatusPreviewView.swift */, D6895DC328D65342006341DA /* ConfirmReblogStatusPreviewView.swift */,
D620483523D38075008A63EF /* ContentTextView.swift */, D620483523D38075008A63EF /* ContentTextView.swift */,
D6D9498E298EB79400C59229 /* CopyableLable.swift */,
D6E426B225337C7000C02E1C /* CustomEmojiImageView.swift */, D6E426B225337C7000C02E1C /* CustomEmojiImageView.swift */,
D6969E9F240C8384002843CE /* EmojiLabel.swift */, D6969E9F240C8384002843CE /* EmojiLabel.swift */,
D61F75B8293C15A000C0B37F /* ZeroHeightCollectionViewCell.swift */, D61F75B8293C15A000C0B37F /* ZeroHeightCollectionViewCell.swift */,
@ -1506,8 +1456,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 */,
@ -1564,7 +1514,6 @@
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 */,
@ -1612,7 +1561,6 @@
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;
@ -1707,7 +1655,6 @@
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>";
@ -1814,7 +1761,7 @@
TargetAttributes = { TargetAttributes = {
D6D4DDCB212518A000E1C4BB = { D6D4DDCB212518A000E1C4BB = {
CreatedOnToolsVersion = 10.0; CreatedOnToolsVersion = 10.0;
LastSwiftMigration = 1420; LastSwiftMigration = 1410;
}; };
D6D4DDDF212518A200E1C4BB = { D6D4DDDF212518A200E1C4BB = {
CreatedOnToolsVersion = 10.0; CreatedOnToolsVersion = 10.0;
@ -1869,6 +1816,7 @@
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 */,
@ -1976,9 +1924,7 @@
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 */,
@ -1986,10 +1932,8 @@
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 /* InlineTrendsViewController.swift in Sources */, D68E525D24A3E8F00054355A /* SearchViewController.swift in Sources */,
D61F75BB293C183100C0B37F /* HTMLConverter.swift in Sources */, D61F75BB293C183100C0B37F /* HTMLConverter.swift in Sources */,
D61F75A5293ABD6F00C0B37F /* EditFilterView.swift in Sources */, D61F75A5293ABD6F00C0B37F /* EditFilterView.swift in Sources */,
D6BC9DB1232C61BC002CA326 /* NotificationsPageViewController.swift in Sources */, D6BC9DB1232C61BC002CA326 /* NotificationsPageViewController.swift in Sources */,
@ -2020,6 +1964,7 @@
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 */,
@ -2028,7 +1973,6 @@
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 */,
@ -2038,7 +1982,6 @@
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 */,
@ -2059,11 +2002,9 @@
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 */,
@ -2081,7 +2022,6 @@
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 */,
@ -2091,16 +2031,13 @@
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 */,
@ -2117,7 +2054,7 @@
D6CA8CDE296387310050C433 /* SaveToPhotosActivity.swift in Sources */, D6CA8CDE296387310050C433 /* SaveToPhotosActivity.swift in Sources */,
D6B936712829F72900237D0E /* NSManagedObjectContext+Helpers.swift in Sources */, D6B936712829F72900237D0E /* NSManagedObjectContext+Helpers.swift in Sources */,
D68A76EE295369C7001DA1B3 /* AcknowledgementsView.swift in Sources */, D68A76EE295369C7001DA1B3 /* AcknowledgementsView.swift in Sources */,
D6C3F5192991F5D60009FCFF /* TimelineJumpButton.swift in Sources */, D627944A23A6AD6100D38C68 /* BookmarksTableViewController.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 */,
@ -2134,7 +2071,6 @@
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 */,
@ -2166,7 +2102,6 @@
D663626221360B1900C9CBA2 /* Preferences.swift in Sources */, D663626221360B1900C9CBA2 /* Preferences.swift in Sources */,
D626493823C0FD0000612E6E /* AllPhotosTableViewCell.swift in Sources */, D626493823C0FD0000612E6E /* AllPhotosTableViewCell.swift in Sources */,
D627943B23A55BA600D38C68 /* NavigableTableViewCell.swift in Sources */, D627943B23A55BA600D38C68 /* NavigableTableViewCell.swift in Sources */,
D6D9498F298EB79400C59229 /* CopyableLable.swift in Sources */,
D6333B792139AEFD00CE884A /* Date+TimeAgo.swift in Sources */, D6333B792139AEFD00CE884A /* Date+TimeAgo.swift in Sources */,
D6D706A029466649000827ED /* ScrollingSegmentedControl.swift in Sources */, D6D706A029466649000827ED /* ScrollingSegmentedControl.swift in Sources */,
D653F411267D1E32004E32B1 /* DiffableTimelineLikeTableViewController.swift in Sources */, D653F411267D1E32004E32B1 /* DiffableTimelineLikeTableViewController.swift in Sources */,
@ -2197,7 +2132,6 @@
D6D706A72948D4D0000827ED /* TimlineState.swift in Sources */, D6D706A72948D4D0000827ED /* TimlineState.swift in Sources */,
D66C900B28DAB7FD00217BF2 /* TimelineViewController.swift in Sources */, D66C900B28DAB7FD00217BF2 /* TimelineViewController.swift in Sources */,
D61F758D2933C69C00C0B37F /* StatusUpdatedNotificationTableViewCell.swift in Sources */, D61F758D2933C69C00C0B37F /* StatusUpdatedNotificationTableViewCell.swift in Sources */,
D6C3F4F7298ED7F70009FCFF /* FavoritesViewController.swift in Sources */,
D6AEBB432321685E00E5038B /* OpenInSafariActivity.swift in Sources */, D6AEBB432321685E00E5038B /* OpenInSafariActivity.swift in Sources */,
D6C693FE2162FEEA007D6A6D /* UIViewController+Children.swift in Sources */, D6C693FE2162FEEA007D6A6D /* UIViewController+Children.swift in Sources */,
D61A45EA28DF51EE002BE511 /* TimelineLikeCollectionViewController.swift in Sources */, D61A45EA28DF51EE002BE511 /* TimelineLikeCollectionViewController.swift in Sources */,
@ -2213,9 +2147,7 @@
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 */,
@ -2236,7 +2168,6 @@
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 */,
@ -2272,11 +2203,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 */,
@ -2297,7 +2228,6 @@
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 */,
@ -2439,11 +2369,10 @@
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 = 76; CURRENT_PROJECT_VERSION = 69;
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;
@ -2451,12 +2380,11 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 2023.4; MARKETING_VERSION = 2023.3;
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";
}; };
@ -2509,7 +2437,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 = 76; CURRENT_PROJECT_VERSION = 69;
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;
@ -2518,7 +2446,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 2023.4; MARKETING_VERSION = 2023.3;
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;
@ -2657,11 +2585,10 @@
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 = 76; CURRENT_PROJECT_VERSION = 69;
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;
@ -2669,15 +2596,13 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 2023.4; MARKETING_VERSION = 2023.3;
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";
}; };
@ -2688,11 +2613,10 @@
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 = 76; CURRENT_PROJECT_VERSION = 69;
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;
@ -2700,12 +2624,11 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 2023.4; MARKETING_VERSION = 2023.3;
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";
}; };
@ -2798,7 +2721,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 = 76; CURRENT_PROJECT_VERSION = 69;
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;
@ -2807,7 +2730,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 2023.4; MARKETING_VERSION = 2023.3;
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;
@ -2824,7 +2747,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 = 76; CURRENT_PROJECT_VERSION = 69;
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;
@ -2833,7 +2756,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 2023.4; MARKETING_VERSION = 2023.3;
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;
@ -2912,7 +2835,7 @@
repositoryURL = "https://github.com/getsentry/sentry-cocoa.git"; repositoryURL = "https://github.com/getsentry/sentry-cocoa.git";
requirement = { requirement = {
kind = upToNextMinorVersion; kind = upToNextMinorVersion;
minimumVersion = 8.0.0; minimumVersion = 7.29.0;
}; };
}; };
D6552365289870790048A653 /* XCRemoteSwiftPackageReference "ScreenCorners" */ = { D6552365289870790048A653 /* XCRemoteSwiftPackageReference "ScreenCorners" */ = {

View File

@ -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) async -> Void)? private let didCreateList: (@MainActor (List) -> Void)?
private var createAction: UIAlertAction? private var createAction: UIAlertAction?
init(mastodonController: MastodonController, present: @escaping (UIViewController) -> Void, didCreateList: (@MainActor (List) async -> Void)?) { init(mastodonController: MastodonController, present: @escaping (UIViewController) -> Void, didCreateList: (@MainActor (List) -> 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)
await self.didCreateList?(list) 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))

View File

@ -48,7 +48,7 @@ class DeleteListService {
private func deleteList() async { private func deleteList() async {
do { do {
let request = List.delete(list.id) let request = List.delete(list)
_ = try await mastodonController.run(request) _ = try await mastodonController.run(request)
mastodonController.deletedList(list) mastodonController.deletedList(list)
} catch { } catch {

View File

@ -101,10 +101,6 @@ 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") {
@ -273,5 +269,5 @@ private func setInstanceBreadcrumb(instance: Instance, nodeInfo: NodeInfo?) {
"version": nodeInfo.software.version, "version": nodeInfo.software.version,
] ]
} }
SentrySDK.addBreadcrumb(crumb) SentrySDK.addBreadcrumb(crumb: crumb)
} }

View File

@ -1,35 +0,0 @@
//
// 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)
}
}
}

View File

@ -31,16 +31,12 @@ 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) nonisolated lazy var persistentContainer = MastodonCachePersistentStore(for: accountInfo, transient: transient) private(set) lazy var persistentContainer = MastodonCachePersistentStore(for: accountInfo, transient: transient)
let instanceURL: URL let instanceURL: URL
var accountInfo: LocalData.UserAccountInfo? var accountInfo: LocalData.UserAccountInfo?
@ -110,7 +106,7 @@ class MastodonController: ObservableObject {
return response return response
} }
func run<Result: Sendable>(_ request: Request<Result>) async throws -> (Result, Pagination?) { func run<Result>(_ 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 {
@ -166,8 +162,6 @@ 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
@ -183,7 +177,7 @@ class MastodonController: ObservableObject {
_ = try await (ownAccount, ownInstance) _ = try await (ownAccount, ownInstance)
loadLists() loadLists()
_ = await loadFilters() async let _ = await loadFilters()
} catch { } catch {
Logging.general.error("MastodonController initialization failed: \(String(describing: error))") Logging.general.error("MastodonController initialization failed: \(String(describing: error))")
} }
@ -365,23 +359,6 @@ 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

View File

@ -11,13 +11,13 @@ import Pachyderm
@MainActor @MainActor
class RenameListService { class RenameListService {
private let list: ListProtocol private let list: List
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: ListProtocol, mastodonController: MastodonController, present: @escaping (UIViewController) -> Void) { init(list: List, 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.id, title: title) let req = List.update(list, title: title)
let (list, _) = try await mastodonController.run(req) let (list, _) = try await mastodonController.run(req)
mastodonController.renamedList(list) mastodonController.renamedList(list)
} catch { } catch {

View File

@ -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 {
MainActor.runUnsafely { let vc = SFSafariViewController(url: url)
navigator.selected(url: url, allowResolveStatuses: false, allowUniversalLinks: false) vc.preferredControlTintColor = Preferences.shared.accentColor.color
} navigator.show(vc)
} }
} }
} }

View File

@ -26,10 +26,6 @@ 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!

View File

@ -20,7 +20,6 @@ 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)
@ -67,14 +66,13 @@ 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.enableWatchdogTerminationTracking = false options.enableOutOfMemoryTracking = false
options.enableAutoPerformanceTracing = false options.enableAutoPerformanceTracking = false
options.enableNetworkTracking = false options.enableNetworkTracking = false
options.enableAppHangTracking = false options.enableAppHangTracking = false
options.enableCoreDataTracing = false options.enableCoreDataTracking = 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
@ -137,22 +135,17 @@ 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 { original(self, selector, sender)
original(self, selector, sender) return
return
}
switch delegate.handleStatusBarTapped(xPosition: xPosition) {
case .stop:
return
case .continue:
original(self, selector, sender)
}
} }
if let exception { switch delegate.handleStatusBarTapped(xPosition: xPosition) {
SentrySDK.capture(exception: exception) case .stop:
return
case .continue:
original(self, selector, sender)
} }
} 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@:@")
@ -160,24 +153,4 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
Logging.general.error("Unable to swizzle status bar manager") Logging.general.error("Unable to swizzle status bar manager")
} }
} }
private func swizzlePresentationController() {
var originalIMP: IMP?
let imp = imp_implementationWithBlock({ (self: UIPresentationController) in
let new = UITraitCollection(pureBlackDarkMode: self.presentingViewController.traitCollection.pureBlackDarkMode)
if let existing = self.overrideTraitCollection {
self.overrideTraitCollection = UITraitCollection(traitsFrom: [existing, new])
} else {
self.overrideTraitCollection = new
}
let original = unsafeBitCast(originalIMP!, to: (@convention(c) (UIPresentationController) -> Void).self)
original(self)
} as (@convention(block) (UIPresentationController) -> Void))
let sel = Selector(["Necessary", "If", "Traits", "update", "_"].reversed().joined())
originalIMP = class_replaceMethod(UIPresentationController.self, sel, imp, "v@:")
if originalIMP == nil {
Logging.general.error("Unable to swizzle presentation controller")
}
}
} }

View File

@ -32,38 +32,46 @@ class ImageCache {
} }
func get(_ url: URL, loadOriginal: Bool = false, completion: ((Data?, UIImage?) -> Void)?) -> Request? { func get(_ url: URL, loadOriginal: Bool = false, completion: ((Data?, UIImage?) -> Void)?) -> Request? {
if !ImageCache.disableCaching, let key = url.absoluteString
let entry = try? cache.get(url.absoluteString, loadOriginal: loadOriginal) {
completion?(entry.data, entry.image) let wrappedCompletion: ((Data?, UIImage?) -> Void)?
return nil if let completion = completion {
} else { wrappedCompletion = { (data, image) in
return Task.detached(priority: .userInitiated) { if let image {
let result = await self.fetch(url: url) if !loadOriginal,
switch result { let size = self.desiredPixelSize {
case .data(let data): image.prepareThumbnail(of: size) {
completion?(data, nil) completion(data, $0)
case .dataAndImage(let data, let image): }
completion?(data, image) } else {
case .none: image.prepareForDisplay {
completion?(nil, nil) completion(data, $0)
}
}
} else {
completion(data, image)
} }
} }
} else {
wrappedCompletion = nil
}
if !ImageCache.disableCaching,
let entry = try? cache.get(key, loadOriginal: loadOriginal) {
wrappedCompletion?(entry.data, entry.image)
return nil
} else {
let task = dataTask(url: url, completion: wrappedCompletion)
task.resume()
return task
} }
} }
func get(_ url: URL, loadOriginal: Bool = false) async -> (Data?, UIImage?) { func get(_ url: URL, loadOriginal: Bool = false) async -> (Data?, UIImage?) {
if !ImageCache.disableCaching, // todo: this should integrate with the task cancellation mechanism somehow
let entry = try? cache.get(url.absoluteString, loadOriginal: loadOriginal) { return await withCheckedContinuation { continuation in
return (entry.data, entry.image) _ = get(url, loadOriginal: loadOriginal) { data, image in
} else { continuation.resume(returning: (data, image))
let result = await self.fetch(url: url)
switch result {
case .data(let data):
return (data, nil)
case .dataAndImage(let data, let image):
return (data, image)
case .none:
return (nil, nil)
} }
} }
} }
@ -73,28 +81,21 @@ class ImageCache {
guard !ImageCache.disableCaching else { return } guard !ImageCache.disableCaching else { return }
if !((try? cache.has(url.absoluteString)) ?? false) { if !((try? cache.has(url.absoluteString)) ?? false) {
Task.detached(priority: .medium) { let task = dataTask(url: url, completion: nil)
_ = await self.fetch(url: url) task.resume()
}
} }
} }
private func fetch(url: URL) async -> FetchResult { private func dataTask(url: URL, completion: ((Data?, UIImage?) -> Void)?) -> URLSessionDataTask {
guard let (data, _) = try? await URLSession.shared.data(from: url) else { return URLSession.shared.dataTask(with: url) { data, response, error in
return .none guard error == nil,
let data else {
return
}
let image = UIImage(data: data)
try? self.cache.set(url.absoluteString, data: data, image: image)
completion?(data, image)
} }
guard let image = UIImage(data: data) else {
try? cache.set(url.absoluteString, data: data, image: nil)
return .data(data)
}
let preparedImage: UIImage?
if let desiredPixelSize {
preparedImage = await image.byPreparingThumbnail(ofSize: desiredPixelSize)
} else {
preparedImage = await image.byPreparingForDisplay()
}
try? cache.set(url.absoluteString, data: data, image: preparedImage ?? image)
return .dataAndImage(data, preparedImage ?? image)
} }
func getData(_ url: URL) -> Data? { func getData(_ url: URL) -> Data? {
@ -113,12 +114,6 @@ class ImageCache {
return cache.disk?.getSizeInBytes() return cache.disk?.getSizeInBytes()
} }
typealias Request = Task<Void, Never> typealias Request = URLSessionDataTask
enum FetchResult {
case data(Data)
case dataAndImage(Data, UIImage)
case none
}
} }

View File

@ -77,7 +77,6 @@ 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 {
@ -85,14 +84,14 @@ class ImageDataCache {
} }
let maxDimension = max(desiredPixelSize.width, desiredPixelSize.height) let maxDimension = max(desiredPixelSize.width, desiredPixelSize.height)
let downsampleOptions: [CFString: Any] = [ let downsampleOptions = [
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 as CFDictionary) { if let downsampled = CGImageSourceCreateThumbnailAtIndex(source, 0, downsampleOptions) {
return UIImage(cgImage: downsampled) return UIImage(cgImage: downsampled)
} else { } else {
return nil return nil

View File

@ -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: [PinnedTimeline] var pinnedTimelines: [Timeline]
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)

View File

@ -11,7 +11,7 @@ import CoreData
import Pachyderm import Pachyderm
@objc(ListMO) @objc(ListMO)
public final class ListMO: NSManagedObject, ListProtocol { public final class ListMO: NSManagedObject {
@nonobjc public class func fetchRequest() -> NSFetchRequest<ListMO> { @nonobjc public class func fetchRequest() -> NSFetchRequest<ListMO> {
return NSFetchRequest(entityName: "List") return NSFetchRequest(entityName: "List")

View File

@ -211,7 +211,7 @@ class MastodonCachePersistentStore: NSPersistentCloudKitContainer {
} }
] ]
} }
SentrySDK.addBreadcrumb(crumb) SentrySDK.addBreadcrumb(crumb: crumb)
fatalError("Unable to save managed object context: \(String(describing: error))") fatalError("Unable to save managed object context: \(String(describing: error))")
} }
} }
@ -545,8 +545,6 @@ 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 {

View File

@ -41,10 +41,6 @@ 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

View File

@ -1,55 +0,0 @@
//
// 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()
}
}

View File

@ -25,4 +25,23 @@ 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")!
}
}
} }

View File

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

View File

@ -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: @unchecked Sendable { class FilterState {
static var unknown: FilterState { FilterState(state: .unknown) } static var unknown: FilterState { FilterState(state: .unknown) }
fileprivate var state: State fileprivate var state: State

View File

@ -55,21 +55,7 @@ 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() {
@ -134,8 +120,6 @@ 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))")
} }

View File

@ -18,7 +18,6 @@ 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
@ -38,16 +37,13 @@ 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.self, from: data) let value = try decoder.decode(Box<Value>.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 wrapper = instance[keyPath: storageKeyPath] var updated = instance[keyPath: storageKeyPath]
if wrapper.skipClearingOnNextUpdate { updated.value = nil
wrapper.skipClearingOnNextUpdate = false updated.observation = nil
} else { instance[keyPath: storageKeyPath] = updated
wrapper.removeCachedValue()
}
instance[keyPath: storageKeyPath] = wrapper
}) })
instance[keyPath: storageKeyPath] = wrapper instance[keyPath: storageKeyPath] = wrapper
return value.value return value.value
@ -59,18 +55,12 @@ 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 {
@ -82,7 +72,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.
struct Box: Codable { private struct Box<T: Codable>: Codable {
let value: Value let value: T
} }
} }

View File

@ -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.handleComposeKeyCommand), input: "n", modifierFlags: .command) return UIKeyCommand(title: "Compose", action: #selector(MainSplitViewController.presentCompose), input: "n", modifierFlags: .command)
}() }()
static func refreshCommand(discoverabilityTitle: String?) -> UIKeyCommand { static func refreshCommand(discoverabilityTitle: String?) -> UIKeyCommand {

View File

@ -1,129 +0,0 @@
//
// 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
}
}

View File

@ -13,24 +13,20 @@ import os
// to make the lock semantics more clear // to make the lock semantics more clear
@available(iOS, obsoleted: 16.0) @available(iOS, obsoleted: 16.0)
class MultiThreadDictionary<Key: Hashable & Sendable, Value: Sendable> { class MultiThreadDictionary<Key: Hashable & Sendable, Value: Sendable> {
private let lock: any Lock<[Key: Value]> private let lock: LockHolder<[AnyHashable: Any]>
init() { init() {
if #available(iOS 16.0, *) { self.lock = LockHolder(initialState: [:])
self.lock = OSAllocatedUnfairLock(initialState: [:])
} else {
self.lock = UnfairLock(initialState: [:])
}
} }
subscript(key: Key) -> Value? { subscript(key: Key) -> Value? {
get { get {
return lock.withLock { dict in return try! lock.withLock { dict in
dict[key] dict[key]
} } as! Value?
} }
set(value) { set(value) {
_ = lock.withLock { dict in _ = try! lock.withLock { dict in
dict[key] = value dict[key] = value
} }
} }
@ -38,21 +34,40 @@ class MultiThreadDictionary<Key: Hashable & Sendable, Value: Sendable> {
/// If the result of this function is unused, it is preferable to use `removeValueWithoutReturning` as it executes asynchronously and doesn't block the calling thread. /// If the result of this function is unused, it is preferable to use `removeValueWithoutReturning` as it executes asynchronously and doesn't block the calling thread.
func removeValue(forKey key: Key) -> Value? { func removeValue(forKey key: Key) -> Value? {
return lock.withLock { dict in return try! lock.withLock { dict in
dict.removeValue(forKey: key) dict.removeValue(forKey: key)
} } as! Value?
} }
func contains(key: Key) -> Bool { func contains(key: Key) -> Bool {
return lock.withLock { dict in return try! lock.withLock { dict in
dict.keys.contains(key) dict.keys.contains(key)
} } as! Bool
} }
// TODO: this should really be throws/rethrows but the stupid type-erased lock makes that not possible // TODO: this should really be throws/rethrows but the stupid type-erased lock makes that not possible
func withLock<R>(_ body: @Sendable (inout [Key: Value]) throws -> R) rethrows -> R where R: Sendable { func withLock<R>(_ body: @Sendable (inout [Key: Value]) -> R) -> R where R: Sendable {
return try lock.withLock { dict in return try! lock.withLock { dict in
return try body(&dict) var downcasted = dict as! [Key: Value]
defer { dict = downcasted }
return body(&downcasted)
} as! R
}
}
// this type erased struct is necessary due to a compiler bug with stored constrained existential types
// see https://github.com/apple/swift/issues/61403
// see #178
fileprivate struct LockHolder<State> {
let withLock: (_ body: @Sendable (inout State) throws -> any Sendable) throws -> any Sendable
init(initialState: State) {
if #available(iOS 16.0, *) {
let lock = OSAllocatedUnfairLock(initialState: initialState)
self.withLock = lock.withLock(_:)
} else {
let lock = UnfairLock(initialState: initialState)
self.withLock = lock.withLock(_:)
} }
} }
} }

View File

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

View File

@ -38,14 +38,12 @@ 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
@ -65,7 +63,6 @@ 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)
@ -75,7 +72,6 @@ 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
@ -83,7 +79,7 @@ class Preferences: Codable, ObservableObject {
self.defaultNotificationsMode = try container.decode(NotificationsMode.self, forKey: .defaultNotificationsType) self.defaultNotificationsMode = try container.decode(NotificationsMode.self, forKey: .defaultNotificationsType)
self.grayscaleImages = try container.decodeIfPresent(Bool.self, forKey: .grayscaleImages) ?? false self.grayscaleImages = try container.decodeIfPresent(Bool.self, forKey: .grayscaleImages) ?? false
self.disableInfiniteScrolling = try container.decodeIfPresent(Bool.self, forKey: .disableInfiniteScrolling) ?? false self.disableInfiniteScrolling = try container.decodeIfPresent(Bool.self, forKey: .disableInfiniteScrolling) ?? false
self.hideTrends = try container.decodeIfPresent(Bool.self, forKey: .hideTrends) ?? false self.hideDiscover = try container.decodeIfPresent(Bool.self, forKey: .hideDiscover) ?? false
self.statusContentType = try container.decode(StatusContentType.self, forKey: .statusContentType) self.statusContentType = try container.decode(StatusContentType.self, forKey: .statusContentType)
@ -95,14 +91,12 @@ 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)
@ -118,7 +112,6 @@ 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)
@ -128,7 +121,6 @@ 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)
@ -136,7 +128,7 @@ class Preferences: Codable, ObservableObject {
try container.encode(defaultNotificationsMode, forKey: .defaultNotificationsType) try container.encode(defaultNotificationsMode, forKey: .defaultNotificationsType)
try container.encode(grayscaleImages, forKey: .grayscaleImages) try container.encode(grayscaleImages, forKey: .grayscaleImages)
try container.encode(disableInfiniteScrolling, forKey: .disableInfiniteScrolling) try container.encode(disableInfiniteScrolling, forKey: .disableInfiniteScrolling)
try container.encode(hideTrends, forKey: .hideTrends) try container.encode(hideDiscover, forKey: .hideDiscover)
try container.encode(statusContentType, forKey: .statusContentType) try container.encode(statusContentType, forKey: .statusContentType)
@ -146,14 +138,12 @@ 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]
@ -179,7 +169,6 @@ 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
@ -190,7 +179,6 @@ 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
@ -199,7 +187,7 @@ class Preferences: Codable, ObservableObject {
@Published var defaultNotificationsMode = NotificationsMode.allNotifications @Published var defaultNotificationsMode = NotificationsMode.allNotifications
@Published var grayscaleImages = false @Published var grayscaleImages = false
@Published var disableInfiniteScrolling = false @Published var disableInfiniteScrolling = false
@Published var hideTrends = false @Published var hideDiscover = false
// MARK: Advanced // MARK: Advanced
@Published var statusContentType: StatusContentType = .plain @Published var statusContentType: StatusContentType = .plain
@ -211,14 +199,12 @@ 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
@ -235,7 +221,6 @@ 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
@ -245,7 +230,6 @@ 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
@ -253,7 +237,7 @@ class Preferences: Codable, ObservableObject {
case defaultNotificationsType case defaultNotificationsType
case grayscaleImages case grayscaleImages
case disableInfiniteScrolling case disableInfiniteScrolling
case hideTrends = "hideDiscover" case hideDiscover
case statusContentType case statusContentType
@ -399,10 +383,3 @@ extension Preferences {
} }
} }
} }
extension Preferences {
enum TimelineSyncMode: String, Codable {
case mastodon
case icloud
}
}

View File

@ -128,9 +128,7 @@ 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
@ -168,9 +166,7 @@ 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")

View File

@ -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: [[String: Any]] = hashtags.map { let objects = 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)

View File

@ -9,14 +9,10 @@
import UIKit import UIKit
import Pachyderm import Pachyderm
class AuxiliarySceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDelegate { class AuxiliarySceneDelegate: UIResponder, UIWindowSceneDelegate {
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) {
@ -74,7 +70,7 @@ class AuxiliarySceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDel
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:
@ -86,10 +82,10 @@ class AuxiliarySceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDel
return NotificationsPageViewController(initialMode: mode, mastodonController: mastodonController) return NotificationsPageViewController(initialMode: mode, mastodonController: mastodonController)
case .search: case .search:
return InlineTrendsViewController(mastodonController: mastodonController) return SearchViewController(mastodonController: mastodonController)
case .bookmarks: case .bookmarks:
return BookmarksViewController(mastodonController: mastodonController) return BookmarksTableViewController(mastodonController: mastodonController)
case .myProfile: case .myProfile:
return MyProfileViewController(mastodonController: mastodonController) return MyProfileViewController(mastodonController: mastodonController)
@ -116,6 +112,7 @@ class AuxiliarySceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDel
} }
@objc private func themePrefChanged() { @objc private func themePrefChanged() {
applyAppearancePreferences() window?.overrideUserInterfaceStyle = Preferences.shared.theme
window?.tintColor = Preferences.shared.accentColor.color
} }
} }

View File

@ -9,12 +9,10 @@
import UIKit import UIKit
import Combine import Combine
class ComposeSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDelegate { class ComposeSceneDelegate: UIResponder, UIWindowSceneDelegate {
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) {
@ -102,7 +100,8 @@ class ComposeSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDeleg
} }
@objc private func themePrefChanged() { @objc private func themePrefChanged() {
applyAppearancePreferences() window?.overrideUserInterfaceStyle = Preferences.shared.theme
window?.tintColor = Preferences.shared.accentColor.color
} }
} }

View File

@ -64,15 +64,7 @@ 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)")
let context: any UserActivityHandlingContext _ = userActivity.handleResume(manager: UserActivityManager(scene: scene as! UIWindowScene))
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) {
@ -177,16 +169,10 @@ 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 {
doRestoreActivity(context: StateRestorationUserActivityHandlingContext(root: rootViewController!)) rootViewController?.restoreActivity(activity)
} else if activity.activityType != UserActivityType.mainScene.rawValue { } else if activity.activityType != UserActivityType.mainScene.rawValue {
doRestoreActivity(context: ActiveAccountUserActivityHandlingContext(isHandoff: false, root: rootViewController!)) _ = activity.handleResume(manager: UserActivityManager(scene: window!.windowScene!))
} else {
stateRestorationLogger.fault("MainSceneDelegate launched with non-restorable activity \(activity.activityType, privacy: .public)")
} }
} }
} else { } else {
@ -218,9 +204,9 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDelegate
} else { } else {
direction = .none direction = .none
} }
container.setRoot(newRoot, for: account, animating: direction) container.setRoot(newRoot, animating: direction)
} else { } else {
window!.rootViewController = AccountSwitchingContainerViewController(root: newRoot, for: account) window!.rootViewController = AccountSwitchingContainerViewController(root: newRoot)
} }
} }
@ -228,7 +214,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
} }
LogoutService(accountInfo: account).run() LocalData.shared.removeAccount(account)
if LocalData.shared.onboardingComplete { if LocalData.shared.onboardingComplete {
activateAccount(LocalData.shared.accounts.first!, animated: false) activateAccount(LocalData.shared.accounts.first!, animated: false)
} else { } else {
@ -257,7 +243,8 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDelegate
} }
@objc func themePrefChanged() { @objc func themePrefChanged() {
applyAppearancePreferences() window?.overrideUserInterfaceStyle = Preferences.shared.theme
window?.tintColor = Preferences.shared.accentColor.color
} }
func showAddAccount() { func showAddAccount() {

View File

@ -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,19 +27,4 @@ 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)
}
}
} }

View File

@ -11,8 +11,6 @@ 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
@ -41,8 +39,7 @@ class AccountFollowsListViewController: UIViewController, CollectionViewControll
} }
override func loadView() { override func loadView() {
var config = UICollectionLayoutListConfiguration(appearance: .plain) var config = UICollectionLayoutListConfiguration(appearance: .grouped)
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
@ -66,16 +63,6 @@ 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()
@ -102,12 +89,12 @@ class AccountFollowsListViewController: UIViewController, CollectionViewControll
} }
} }
private nonisolated func request(for range: RequestRange) -> Request<[Account]> { private func request(for range: RequestRange) -> Request<[Account]> {
switch mode { switch mode {
case .following: case .following:
return Account.getFollowing(accountID, range: range.withCount(Self.pageSize)) return Account.getFollowing(accountID, range: range)
case .followers: case .followers:
return Account.getFollowers(accountID, range: range.withCount(Self.pageSize)) return Account.getFollowers(accountID, range: range)
} }
} }

View File

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

View File

@ -72,8 +72,7 @@ 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 = .appBackground view.backgroundColor = .systemBackground
collectionView.backgroundColor = .appBackground
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(donePressed)) navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(donePressed))

View File

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

View File

@ -0,0 +1,196 @@
//
// 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)
}
}

View File

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

View File

@ -236,7 +236,6 @@ struct ComposeAutocompleteEmojisView: View {
CustomEmojiImageView(emoji: emoji) CustomEmojiImageView(emoji: emoji)
.frame(height: emojiSize) .frame(height: emojiSize)
} }
.accessibilityLabel(emoji.shortcode)
} }
} header: { } header: {
if !section.isEmpty { if !section.isEmpty {
@ -272,7 +271,6 @@ struct ComposeAutocompleteEmojisView: View {
.foregroundColor(Color(UIColor.label)) .foregroundColor(Color(UIColor.label))
} }
} }
.accessibilityLabel(emoji.shortcode)
.frame(height: emojiSize) .frame(height: emojiSize)
} }
.animation(.linear(duration: 0.2), value: emojis) .animation(.linear(duration: 0.2), value: emojis)
@ -295,7 +293,6 @@ struct ComposeAutocompleteEmojisView: View {
.aspectRatio(contentMode: .fit) .aspectRatio(contentMode: .fit)
.rotationEffect(expanded ? .zero : .degrees(180)) .rotationEffect(expanded ? .zero : .degrees(180))
} }
.accessibilityLabel(expanded ? "Collapse" : "Expand")
.frame(width: 20, height: 20) .frame(width: 20, height: 20)
} }

View File

@ -15,17 +15,15 @@ struct ComposeEmojiTextField: UIViewRepresentable {
@Binding var text: String @Binding var text: String
let placeholder: String let placeholder: String
let maxLength: Int?
let becomeFirstResponder: Binding<Bool>? let becomeFirstResponder: Binding<Bool>?
let focusNextView: Binding<Bool>? let focusNextView: Binding<Bool>?
private var didChange: ((String) -> Void)? = nil private var didChange: ((String) -> Void)? = nil
private var didEndEditing: (() -> Void)? = nil private var didEndEditing: (() -> Void)? = nil
private var backgroundColor: UIColor? = nil private var backgroundColor: UIColor? = nil
init(text: Binding<String>, placeholder: String, maxLength: Int? = nil, becomeFirstResponder: Binding<Bool>? = nil, focusNextView: Binding<Bool>? = nil) { init(text: Binding<String>, placeholder: String, becomeFirstResponder: Binding<Bool>? = nil, focusNextView: Binding<Bool>? = nil) {
self._text = text self._text = text
self.placeholder = placeholder self.placeholder = placeholder
self.maxLength = maxLength
self.becomeFirstResponder = becomeFirstResponder self.becomeFirstResponder = becomeFirstResponder
self.focusNextView = focusNextView self.focusNextView = focusNextView
self.didChange = nil self.didChange = nil
@ -76,7 +74,6 @@ struct ComposeEmojiTextField: UIViewRepresentable {
} else { } else {
uiView.text = text uiView.text = text
} }
context.coordinator.maxLength = maxLength
context.coordinator.didChange = didChange context.coordinator.didChange = didChange
context.coordinator.didEndEditing = didEndEditing context.coordinator.didEndEditing = didEndEditing
context.coordinator.focusNextView = focusNextView context.coordinator.focusNextView = focusNextView
@ -98,7 +95,6 @@ struct ComposeEmojiTextField: UIViewRepresentable {
var text: Binding<String>! var text: Binding<String>!
// break retained cycle through ComposeUIState.currentInput // break retained cycle through ComposeUIState.currentInput
unowned var uiState: ComposeUIState! unowned var uiState: ComposeUIState!
var maxLength: Int?
var didChange: ((String) -> Void)? var didChange: ((String) -> Void)?
var didEndEditing: (() -> Void)? var didEndEditing: (() -> Void)?
var focusNextView: Binding<Bool>? var focusNextView: Binding<Bool>?
@ -118,14 +114,6 @@ struct ComposeEmojiTextField: UIViewRepresentable {
focusNextView?.wrappedValue = true focusNextView?.wrappedValue = true
} }
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if let maxLength {
return ((textField.text ?? "") as NSString).replacingCharacters(in: range, with: string).count <= maxLength
} else {
return true
}
}
func textFieldDidBeginEditing(_ textField: UITextField) { func textFieldDidBeginEditing(_ textField: UITextField) {
uiState.currentInput = self uiState.currentInput = self
updateAutocompleteState(textField: textField) updateAutocompleteState(textField: textField)

View File

@ -20,7 +20,6 @@ struct ComposePollView: View {
@ObservedObject var draft: Draft @ObservedObject var draft: Draft
@ObservedObject var poll: Draft.Poll @ObservedObject var poll: Draft.Poll
@EnvironmentObject var mastodonController: MastodonController
@Environment(\.colorScheme) var colorScheme: ColorScheme @Environment(\.colorScheme) var colorScheme: ColorScheme
@State private var duration: Duration @State private var duration: Duration
@ -32,14 +31,6 @@ struct ComposePollView: View {
self._duration = State(initialValue: .fromTimeInterval(poll.duration) ?? .oneDay) self._duration = State(initialValue: .fromTimeInterval(poll.duration) ?? .oneDay)
} }
private var canAddOption: Bool {
if let pollConfig = mastodonController.instance?.pollsConfiguration {
return poll.options.count < pollConfig.maxOptions
} else {
return true
}
}
var body: some View { var body: some View {
VStack { VStack {
HStack { HStack {
@ -76,15 +67,9 @@ struct ComposePollView: View {
.frame(height: 44 * CGFloat(poll.options.count)) .frame(height: 44 * CGFloat(poll.options.count))
Button(action: self.addOption) { Button(action: self.addOption) {
Label { Label("Add Option", systemImage: "plus")
Text("Add Option")
} icon: {
Image(systemName: "plus")
.foregroundColor(.accentColor)
}
} }
.buttonStyle(.borderless) .buttonStyle(.borderless)
.disabled(!canAddOption)
HStack { HStack {
MenuPicker(selection: $poll.multiple, options: [ MenuPicker(selection: $poll.multiple, options: [
@ -111,7 +96,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.appFill : Color(white: 0.95) colorScheme == .dark ? Color(UIColor.secondarySystemBackground) : Color(white: 0.95)
} }
private var buttonBackgroundColor: Color { private var buttonBackgroundColor: Color {
@ -170,8 +155,6 @@ struct ComposePollOption: View {
@ObservedObject var option: Draft.Poll.Option @ObservedObject var option: Draft.Poll.Option
let optionIndex: Int let optionIndex: Int
@EnvironmentObject private var mastodonController: MastodonController
var body: some View { var body: some View {
HStack(spacing: 4) { HStack(spacing: 4) {
Checkbox(radiusFraction: poll.multiple ? 0.1 : 0.5, borderWidth: 2) Checkbox(radiusFraction: poll.multiple ? 0.1 : 0.5, borderWidth: 2)
@ -190,8 +173,8 @@ struct ComposePollOption: View {
} }
private var textField: some View { private var textField: some View {
var field = ComposeEmojiTextField(text: $option.text, placeholder: "Option \(optionIndex + 1)", maxLength: mastodonController.instance?.pollsConfiguration?.maxCharactersPerOption) var field = ComposeEmojiTextField(text: $option.text, placeholder: "Option \(optionIndex + 1)")
return field.backgroundColor(.appBackground) return field.backgroundColor(.systemBackground)
} }
private func removeOption() { private func removeOption() {
@ -216,7 +199,7 @@ struct ComposePollOption: View {
.cornerRadius(radiusFraction * size) .cornerRadius(radiusFraction * size)
Rectangle() Rectangle()
.foregroundColor(Color(UIColor.appBackground)) .foregroundColor(Color(UIColor.systemBackground))
.frame(width: innerSize, height: innerSize) .frame(width: innerSize, height: innerSize)
.cornerRadius(radiusFraction * innerSize) .cornerRadius(radiusFraction * innerSize)
} }

View File

@ -54,7 +54,6 @@ 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,
@ -75,11 +74,16 @@ 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)
@ -115,6 +119,10 @@ 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)

View File

@ -94,10 +94,6 @@ 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()
@ -128,7 +124,7 @@ struct ComposeView: View {
} }
}) })
.sheet(isPresented: $uiState.isShowingDraftsList) { .sheet(isPresented: $uiState.isShowingDraftsList) {
DraftsRepresentable(currentDraft: draft, mastodonController: mastodonController) DraftsView(currentDraft: draft, mastodonController: mastodonController)
} }
.actionSheet(isPresented: $uiState.isShowingSaveDraftSheet, content: self.saveAndCloseSheet) .actionSheet(isPresented: $uiState.isShowingSaveDraftSheet, content: self.saveAndCloseSheet)
.alert(isPresented: $isShowingPostErrorAlert) { .alert(isPresented: $isShowingPostErrorAlert) {
@ -173,13 +169,11 @@ 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(
@ -190,7 +184,6 @@ 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(
@ -199,20 +192,17 @@ 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()
@ -249,25 +239,16 @@ 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()
}
} label: {
Text("Post")
}
.keyboardShortcut(.return, modifiers: .command)
.disabled(!postButtonEnabled)
} else {
Button {
uiState.isShowingDraftsList = true
} label: {
Text("Drafts")
} }
} label: {
Text("Post")
} }
.keyboardShortcut(.return, modifiers: .command)
.disabled(!postButtonEnabled)
} }
private func cancel() { private func cancel() {
@ -329,7 +310,7 @@ struct ComposeView: View {
} }
} }
extension View { private extension View {
@available(iOS, obsoleted: 16.0) @available(iOS, obsoleted: 16.0)
@ViewBuilder @ViewBuilder
func scrollDismissesKeyboardInteractivelyIfAvailable() -> some View { func scrollDismissesKeyboardInteractivelyIfAvailable() -> some View {

View File

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

View File

@ -9,20 +9,39 @@
import SwiftUI import SwiftUI
import Pachyderm import Pachyderm
struct MainComposeTextView: View, PlaceholderViewProvider { struct MainComposeTextView: View {
@ObservedObject var draft: Draft @ObservedObject var draft: Draft
@State private var placeholder: PlaceholderView = Self.placeholderView() @State private var placeholder: Text = {
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) {
colorScheme == .dark ? Color.appFill : Color(uiColor: .secondarySystemBackground) Color(UIColor.secondarySystemBackground)
if draft.text.isEmpty { if draft.text.isEmpty {
placeholder placeholder
@ -48,38 +67,6 @@ struct MainComposeTextView: View, PlaceholderViewProvider {
} }
} }
} }
@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 {
@ -111,7 +98,6 @@ 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
} }
@ -199,7 +185,6 @@ 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]
@ -339,10 +324,6 @@ 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 {

View File

@ -9,9 +9,18 @@
import UIKit import UIKit
import Pachyderm import Pachyderm
class ConversationCollectionViewController: UIViewController, CollectionViewController, RefreshableViewController { class ConversationNode {
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
@ -23,12 +32,11 @@ class ConversationCollectionViewController: UIViewController, CollectionViewCont
} }
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>! private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
init(for mainStatusID: String, state: CollapseState, conversationViewController: ConversationViewController) { init(for mainStatusID: String, state: CollapseState, mastodonController: MastodonController) {
self.mainStatusID = mainStatusID self.mainStatusID = mainStatusID
self.mainStatusState = state self.mainStatusState = state
self.statusIDToScrollToOnLoad = mainStatusID self.statusIDToScrollToOnLoad = mainStatusID
self.conversationViewController = conversationViewController self.mastodonController = mastodonController
self.mastodonController = conversationViewController.mastodonController
super.init(nibName: nil, bundle: nil) super.init(nibName: nil, bundle: nil)
} }
@ -39,7 +47,7 @@ class ConversationCollectionViewController: UIViewController, CollectionViewCont
override func loadView() { override func loadView() {
var config = UICollectionLayoutListConfiguration(appearance: .plain) var config = UICollectionLayoutListConfiguration(appearance: .plain)
config.backgroundColor = .appSecondaryBackground config.backgroundColor = .secondarySystemBackground
config.leadingSwipeActionsConfigurationProvider = { [unowned self] in config.leadingSwipeActionsConfigurationProvider = { [unowned self] in
(collectionView.cellForItem(at: $0) as? TimelineStatusCollectionViewCell)?.leadingSwipeActions() (collectionView.cellForItem(at: $0) as? TimelineStatusCollectionViewCell)?.leadingSwipeActions()
} }
@ -47,21 +55,17 @@ class ConversationCollectionViewController: UIViewController, CollectionViewCont
(collectionView.cellForItem(at: $0) as? TimelineStatusCollectionViewCell)?.trailingSwipeActions() (collectionView.cellForItem(at: $0) as? TimelineStatusCollectionViewCell)?.trailingSwipeActions()
} }
config.itemSeparatorHandler = { [unowned self] indexPath, sectionConfig in config.itemSeparatorHandler = { [unowned self] indexPath, sectionConfig in
let rowsInSection = self.collectionView.numberOfItems(inSection: indexPath.section)
let lastInSection = indexPath.row == rowsInSection - 1
var config = sectionConfig var config = sectionConfig
config.topSeparatorVisibility = .hidden config.topSeparatorVisibility = .hidden
if case .ancestors = self.dataSource.sectionIdentifier(for: indexPath.section) { config.bottomSeparatorVisibility = lastInSection ? .visible : .hidden
config.bottomSeparatorVisibility = .hidden
} else if indexPath.row == self.collectionView.numberOfItems(inSection: indexPath.section) - 1 {
config.bottomSeparatorVisibility = .visible
} else {
config.bottomSeparatorVisibility = .hidden
}
config.bottomSeparatorInsets = TimelineStatusCollectionViewCell.separatorInsets config.bottomSeparatorInsets = TimelineStatusCollectionViewCell.separatorInsets
return config return config
} }
// 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 peeking through the edges // background color always peaking 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
@ -70,11 +74,6 @@ 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()
} }
@ -100,7 +99,7 @@ class ConversationCollectionViewController: UIViewController, CollectionViewCont
} }
return UICollectionViewDiffableDataSource(collectionView: collectionView) { [unowned self] collectionView, indexPath, itemIdentifier in return UICollectionViewDiffableDataSource(collectionView: collectionView) { [unowned self] collectionView, indexPath, itemIdentifier in
switch itemIdentifier { switch itemIdentifier {
case let .status(id: id, node: _, state: state, prevLink: prevLink, nextLink: nextLink): case let .status(id: id, state: state, prevLink: prevLink, nextLink: nextLink):
if id == self.mainStatusID { if id == self.mainStatusID {
return collectionView.dequeueConfiguredReusableCell(using: mainStatusCell, for: indexPath, item: (id, state, prevLink)) return collectionView.dequeueConfiguredReusableCell(using: mainStatusCell, for: indexPath, item: (id, state, prevLink))
} else { } else {
@ -124,33 +123,45 @@ class ConversationCollectionViewController: UIViewController, CollectionViewCont
loadViewIfNeeded() loadViewIfNeeded()
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>() var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.ancestors, .mainStatus]) snapshot.appendSections([.statuses])
if status.inReplyToID != nil { if status.inReplyToID != nil {
snapshot.appendItems([.loadingIndicator], toSection: .ancestors) snapshot.appendItems([.loadingIndicator], toSection: .statuses)
} }
// this will be replace with the actual node in the tree once it's loaded let mainStatusItem = Item.status(id: mainStatusID, state: mainStatusState, prevLink: status.inReplyToID != nil, nextLink: false)
let tempMainNode = ConversationNode(status: status) snapshot.appendItems([mainStatusItem], toSection: .statuses)
let mainStatusItem = Item.status(id: mainStatusID, node: tempMainNode, state: mainStatusState, prevLink: status.inReplyToID != nil, nextLink: false)
snapshot.appendItems([mainStatusItem], toSection: .mainStatus)
dataSource.apply(snapshot, animatingDifferences: false) dataSource.apply(snapshot, animatingDifferences: false)
} }
func addTree(_ tree: ConversationTree, mainStatus: StatusMO) { func addContext(_ context: ConversationContext, for mainStatus: StatusMO) async {
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>() let parentIDs = getDirectParents(inReplyTo: mainStatus.inReplyToID, from: context.ancestors)
snapshot.appendSections([.ancestors, .mainStatus]) let parentStatuses = context.ancestors.filter { parentIDs.contains($0.id) }
let mainStatusItem = Item.status(id: mainStatusID, node: tree.mainStatus, state: mainStatusState, prevLink: mainStatus.inReplyToID != nil, nextLink: false)
snapshot.appendItems([mainStatusItem], toSection: .mainStatus) await mastodonController.persistentContainer.addAll(statuses: parentStatuses + context.descendants)
let parentItems = tree.ancestors.enumerated().map { index, node in
Item.status(id: node.status.id, node: node, state: .unknown, prevLink: index > 0, nextLink: true) var snapshot = dataSource.snapshot()
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.appendItems(parentItems, toSection: .ancestors) snapshot.insertItems(parentItems, beforeItem: mainStatusItem)
snapshot.reloadItems([mainStatusItem]) snapshot.reloadItems([mainStatusItem])
// convert sub-threads into items for section and add to snapshot // fetch all descendant status managed objects
self.addFlattenedChildThreadsToSnapshot(tree.descendants, mainStatus: mainStatus, snapshot: &snapshot) let descendantIDs = context.descendants.map(\.id)
let request = StatusMO.fetchRequest()
request.predicate = NSPredicate(format: "id IN %@", descendantIDs)
if let descendants = try? mastodonController.persistentContainer.viewContext.fetch(request) {
// convert array of descendant statuses into tree of sub-threads
let childThreads = self.getDescendantThreads(mainStatus: mainStatus, descendants: descendants)
// convert sub-threads into items for section and add to snapshot
self.addFlattenedChildThreadsToSnapshot(childThreads, mainStatus: mainStatus, snapshot: &snapshot)
}
self.dataSource.apply(snapshot, animatingDifferences: false) { self.dataSource.apply(snapshot, animatingDifferences: false) {
let item: Item let item: Item
@ -160,7 +171,7 @@ class ConversationCollectionViewController: UIViewController, CollectionViewCont
position = .centeredVertically position = .centeredVertically
} else { } else {
item = snapshot.itemIdentifiers.first { item = snapshot.itemIdentifiers.first {
if case .status(id: self.statusIDToScrollToOnLoad, _, _, _, _) = $0 { if case .status(id: self.statusIDToScrollToOnLoad, _, _, _) = $0 {
return true return true
} else { } else {
return false return false
@ -176,6 +187,54 @@ class ConversationCollectionViewController: UIViewController, CollectionViewCont
} }
} }
private func getDirectParents(inReplyTo inReplyToID: String?, from statuses: [Status]) -> [String] {
var statuses = statuses
var parents = [String]()
var parentID: String? = inReplyToID
while parentID != nil, let parentIndex = statuses.firstIndex(where: { $0.id == parentID }) {
let parentStatus = statuses.remove(at: parentIndex)
parents.insert(parentStatus.id, at: 0)
parentID = parentStatus.inReplyToID
}
return parents
}
private func getDescendantThreads(mainStatus: StatusMO, descendants: [StatusMO]) -> [ConversationNode] {
var descendants = descendants
func removeAllInReplyTo(id: String) -> [StatusMO] {
let statuses = descendants.filter { $0.inReplyToID == id }
descendants.removeAll { $0.inReplyToID == id }
return statuses
}
var nodes: [String: ConversationNode] = [
mainStatus.id: ConversationNode(status: mainStatus)
]
var idsToCheck = [mainStatusID]
while !idsToCheck.isEmpty {
let inReplyToID = idsToCheck.removeFirst()
let nodeForID = nodes[inReplyToID]!
let inReply = removeAllInReplyTo(id: inReplyToID)
for reply in inReply {
idsToCheck.append(reply.id)
let replyNode = ConversationNode(status: reply)
nodes[reply.id] = replyNode
nodeForID.children.append(replyNode)
}
}
return nodes[mainStatusID]!.children
}
private func addFlattenedChildThreadsToSnapshot(_ childThreads: [ConversationNode], mainStatus: StatusMO, snapshot: inout NSDiffableDataSourceSnapshot<Section, Item>) { private func addFlattenedChildThreadsToSnapshot(_ childThreads: [ConversationNode], mainStatus: StatusMO, snapshot: inout NSDiffableDataSourceSnapshot<Section, Item>) {
var childThreads = childThreads var childThreads = childThreads
@ -189,7 +248,7 @@ class ConversationCollectionViewController: UIViewController, CollectionViewCont
for node in childThreads { for node in childThreads {
let section = Section.childThread(firstStatusID: node.status.id) let section = Section.childThread(firstStatusID: node.status.id)
snapshot.appendSections([section]) snapshot.appendSections([section])
snapshot.appendItems([.status(id: node.status.id, node: node, state: .unknown, prevLink: false, nextLink: !node.children.isEmpty)], toSection: section) snapshot.appendItems([.status(id: node.status.id, state: .unknown, prevLink: false, nextLink: !node.children.isEmpty)], toSection: section)
var currentNode = node var currentNode = node
while true { while true {
@ -212,7 +271,7 @@ class ConversationCollectionViewController: UIViewController, CollectionViewCont
} }
currentNode = next currentNode = next
snapshot.appendItems([.status(id: next.status.id, node: next, state: .unknown, prevLink: true, nextLink: !next.children.isEmpty)], toSection: section) snapshot.appendItems([.status(id: next.status.id, state: .unknown, prevLink: true, nextLink: !next.children.isEmpty)], toSection: section)
} }
} }
} }
@ -221,7 +280,7 @@ class ConversationCollectionViewController: UIViewController, CollectionViewCont
var snapshot = dataSource.snapshot() var snapshot = dataSource.snapshot()
var cellsToMask: [StatusCollectionViewCell] = [] var cellsToMask: [StatusCollectionViewCell] = []
for item in snapshot.itemIdentifiers { for item in snapshot.itemIdentifiers {
guard case .status(id: _, node: _, state: let state, prevLink: _, nextLink: _) = item, guard case .status(id: _, state: let state, prevLink: _, nextLink: _) = item,
state.collapsible == true else { state.collapsible == true else {
continue continue
} }
@ -248,31 +307,21 @@ 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 ancestors case statuses
case mainStatus
case childThread(firstStatusID: String) case childThread(firstStatusID: String)
} }
enum Item: Hashable { enum Item: Hashable {
case status(id: String, node: ConversationNode, state: CollapseState, prevLink: Bool, nextLink: Bool) case status(id: String, state: CollapseState, prevLink: Bool, nextLink: Bool)
case expandThread(childThreads: [ConversationNode], inline: Bool) case expandThread(childThreads: [ConversationNode], inline: Bool)
case loadingIndicator case loadingIndicator
static func ==(lhs: Item, rhs: Item) -> Bool { static func ==(lhs: Item, rhs: Item) -> Bool {
switch (lhs, rhs) { switch (lhs, rhs) {
case let (.status(id: a, node: _, state: _, prevLink: aPrev, nextLink: aNext), .status(id: b, node: _, state: _, prevLink: bPrev, nextLink: bNext)): case let (.status(id: a, state: _, prevLink: aPrev, nextLink: aNext), .status(id: b, state: _, prevLink: bPrev, nextLink: bNext)):
return a == b && aPrev == bPrev && aNext == bNext return a == b && aPrev == bPrev && aNext == bNext
case let (.expandThread(childThreads: a, inline: aInline), .expandThread(childThreads: b, inline: bInline)): case let (.expandThread(childThreads: a, inline: aInline), .expandThread(childThreads: b, inline: bInline)):
return a.count == b.count && zip(a, b).allSatisfy { $0.status.id == $1.status.id } && aInline == bInline return a.count == b.count && zip(a, b).allSatisfy { $0.status.id == $1.status.id } && aInline == bInline
@ -285,7 +334,7 @@ extension ConversationCollectionViewController {
func hash(into hasher: inout Hasher) { func hash(into hasher: inout Hasher) {
switch self { switch self {
case let .status(id: id, node: _, state: _, prevLink: prevLink, nextLink: nextLink): case let .status(id: id, state: _, prevLink: prevLink, nextLink: nextLink):
hasher.combine(0) hasher.combine(0)
hasher.combine(id) hasher.combine(id)
hasher.combine(prevLink) hasher.combine(prevLink)
@ -306,7 +355,7 @@ extension ConversationCollectionViewController {
extension ConversationCollectionViewController: UICollectionViewDelegate { extension ConversationCollectionViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool { func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool {
switch dataSource.itemIdentifier(for: indexPath) { switch dataSource.itemIdentifier(for: indexPath) {
case .status(id: let id, node: _, state: _, prevLink: _, nextLink: _): case .status(id: let id, state: _, prevLink: _, nextLink: _):
return id != mainStatusID return id != mainStatusID
case .expandThread(childThreads: _, inline: _): case .expandThread(childThreads: _, inline: _):
return true return true
@ -321,25 +370,12 @@ extension ConversationCollectionViewController: UICollectionViewDelegate {
break break
case .loadingIndicator: case .loadingIndicator:
break break
case .status(id: let id, node: let node, state: let state, _, _): case .status(id: let id, state: let state, _, _):
// we can only take the fast path if the user tapped on a descendant status. selected(status: id, state: state.copy())
// if the current main status is C, or one of its descendants, and the user taps A, then B won't be loaded:
// A
// / \
// B C
if case .childThread(_) = dataSource.sectionIdentifier(for: indexPath.section) {
let tree = ConversationTree(ancestors: buildNewAncestors(above: indexPath), mainStatus: node)
let conv = ConversationViewController(preloadedTree: tree, state: state.copy(), mastodonController: mastodonController)
conv.showStatusesAutomatically = showStatusesAutomatically
show(conv)
} else {
selected(status: id, state: state.copy())
}
case .expandThread(childThreads: let childThreads, inline: _): case .expandThread(childThreads: let childThreads, inline: _):
let indexPathBeforeExpandThread = IndexPath(row: indexPath.row - 1, section: indexPath.section) if case .status(id: let id, state: let state, _, _) = dataSource.itemIdentifier(for: IndexPath(row: indexPath.row - 1, section: indexPath.section)) {
if case .status(id: _, node: let node, state: let state, _, _) = dataSource.itemIdentifier(for: indexPathBeforeExpandThread) { // todo: it would be nice to avoid re-fetching the context here, since we should have all the necessary information already
let tree = ConversationTree(ancestors: buildNewAncestors(above: indexPathBeforeExpandThread), mainStatus: node) let conv = ConversationViewController(for: id, state: state.copy(), mastodonController: mastodonController)
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)
@ -347,34 +383,6 @@ extension ConversationCollectionViewController: UICollectionViewDelegate {
} }
} }
// ConversationNode doesn't know about its parent, so we reconstruct that info from the data source
private func buildNewAncestors(above indexPath: IndexPath) -> [ConversationNode] {
let snapshot = dataSource.snapshot()
let currentAncestors = snapshot.itemIdentifiers(inSection: .ancestors).compactMap {
if case .status(id: _, node: let node, _, _, _) = $0 {
return node
} else {
return nil
}
}
let currentMainStatus = snapshot.itemIdentifiers(inSection: .mainStatus).compactMap {
if case .status(id: _, node: let node, _, _, _) = $0 {
return node
} else {
return nil
}
}
let parentsInCurrentSection = snapshot.itemIdentifiers(inSection: dataSource.sectionIdentifier(for: indexPath.section)!)[0..<indexPath.row].compactMap {
if case .status(id: _, node: let node, _, _, _) = $0 {
return node
} else {
return nil
}
}
return currentAncestors + currentMainStatus + parentsInCurrentSection
}
func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? { func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
return (collectionView.cellForItem(at: indexPath) as? TimelineStatusCollectionViewCell)?.contextMenuConfiguration() return (collectionView.cellForItem(at: indexPath) as? TimelineStatusCollectionViewCell)?.contextMenuConfiguration()
} }

View File

@ -1,101 +0,0 @@
//
// ConversationTree.swift
// Tusker
//
// Created by Shadowfacts on 2/4/23.
// Copyright © 2023 Shadowfacts. All rights reserved.
//
import Foundation
import Pachyderm
class ConversationNode {
let status: StatusMO
var children: [ConversationNode]
init(status: StatusMO) {
self.status = status
self.children = []
}
}
struct ConversationTree {
let ancestors: [ConversationNode]
let mainStatus: ConversationNode
var descendants: [ConversationNode] {
mainStatus.children
}
init(ancestors: [ConversationNode], mainStatus: ConversationNode) {
self.ancestors = ancestors
self.mainStatus = mainStatus
}
static func build(for mainStatus: StatusMO, ancestors: [StatusMO], descendants: [StatusMO]) -> ConversationTree {
let mainStatusNode = ConversationNode(status: mainStatus)
let ancestors = buildAncestorNodes(mainStatusNode: mainStatusNode, ancestors: ancestors)
buildDescendantNodes(mainStatusNode: mainStatusNode, descendants: descendants)
return ConversationTree(ancestors: ancestors, mainStatus: mainStatusNode)
}
private static func buildAncestorNodes(mainStatusNode: ConversationNode, ancestors: [StatusMO]) -> [ConversationNode] {
var statuses = ancestors
var parents = [ConversationNode]()
var parentID: String? = mainStatusNode.status.inReplyToID
while let currentParentID = parentID,
let parentIndex = statuses.firstIndex(where: { $0.id == currentParentID }) {
let parentStatus = statuses.remove(at: parentIndex)
let node = ConversationNode(status: parentStatus)
parents.insert(node, at: 0)
parentID = parentStatus.inReplyToID
}
// once the parents list is built and in-order, then we walk through and set each node's children
for (index, node) in parents.enumerated() {
if index == parents.count - 1 {
// the last parent is the direct parent of the main status
node.children = [mainStatusNode]
} else {
// otherwise, it's the parent of the status that comes immediately after it in the parents list
node.children = [parents[index + 1]]
}
}
return parents
}
// doesn't return anything, since we're modifying the main status node in-place
private static func buildDescendantNodes(mainStatusNode: ConversationNode, descendants: [StatusMO]) {
var descendants = descendants
func removeAllInReplyTo(id: String) -> [StatusMO] {
let statuses = descendants.filter { $0.inReplyToID == id }
descendants.removeAll { $0.inReplyToID == id }
return statuses
}
var nodes: [String: ConversationNode] = [
mainStatusNode.status.id: mainStatusNode
]
var idsToCheck = [mainStatusNode.status.id]
while !idsToCheck.isEmpty {
let inReplyToID = idsToCheck.removeFirst()
let nodeForID = nodes[inReplyToID]!
let inReply = removeAllInReplyTo(id: inReplyToID)
for reply in inReply {
idsToCheck.append(reply.id)
let replyNode = ConversationNode(status: reply)
nodes[reply.id] = replyNode
nodeForID.children.append(replyNode)
}
}
}
}

View File

@ -77,14 +77,6 @@ class ConversationViewController: UIViewController {
super.init(nibName: nil, bundle: nil) super.init(nibName: nil, bundle: nil)
} }
init(preloadedTree: ConversationTree, state mainStatusState: CollapseState, mastodonController: MastodonController) {
self.mode = .preloaded(preloadedTree)
self.mainStatusState = mainStatusState
self.mastodonController = mastodonController
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
@ -94,7 +86,7 @@ class ConversationViewController: UIViewController {
title = NSLocalizedString("Conversation", comment: "conversation screen title") title = NSLocalizedString("Conversation", comment: "conversation screen title")
view.backgroundColor = .appSecondaryBackground view.backgroundColor = .secondarySystemBackground
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()
@ -123,15 +115,9 @@ class ConversationViewController: UIViewController {
override func viewWillAppear(_ animated: Bool) { override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated) super.viewWillAppear(animated)
if case .unloaded = state { Task {
if case .preloaded(let tree) = mode { if case .unloaded = state {
// when everything is preloaded, we're on the fast path and want to avoid any async work await loadMainStatus()
// just kicking off a MainActor task causes a delay before the content appears, even if the task doesn't suspend
mainStatusLoaded(tree.mainStatus.status)
} else {
Task { @MainActor in
await loadMainStatus()
}
} }
} }
} }
@ -156,20 +142,9 @@ class ConversationViewController: UIViewController {
// MARK: Loading // MARK: Loading
@MainActor
private func loadMainStatus() async { private func loadMainStatus() async {
let mainStatusID: String guard let mainStatusID = await resolveStatusIfNecessary() else {
switch mode { return
case .localID(let id):
mainStatusID = id
case .resolve(let url):
if let id = await resolveStatus(url: url) {
mainStatusID = id
} else {
return
}
case .preloaded(_):
fatalError("unreachable")
} }
@MainActor @MainActor
@ -191,7 +166,7 @@ class ConversationViewController: UIViewController {
Task { Task {
await doLoadMainStatus() await doLoadMainStatus()
} }
mainStatusLoaded(cached) await mainStatusLoaded(cached)
} else { } else {
// otherwise, show a loading indicator while loading the main status // otherwise, show a loading indicator while loading the main status
let indicator = UIActivityIndicatorView(style: .medium) let indicator = UIActivityIndicatorView(style: .medium)
@ -199,110 +174,77 @@ class ConversationViewController: UIViewController {
state = .loading(indicator) state = .loading(indicator)
if let status = await doLoadMainStatus() { if let status = await doLoadMainStatus() {
mainStatusLoaded(status) await mainStatusLoaded(status)
} }
} }
} }
@MainActor @MainActor
private func resolveStatus(url: URL) async -> String? { private func resolveStatusIfNecessary() async -> String? {
let indicator = UIActivityIndicatorView(style: .medium) switch mode {
indicator.startAnimating() case .localID(let id):
state = .loading(indicator) return id
case .resolve(let url):
let indicator = UIActivityIndicatorView(style: .medium)
indicator.startAnimating()
state = .loading(indicator)
let url = WebURL(url)!.serialized(excludingFragment: true) let url = WebURL(url)!
let request = Client.search(query: url, types: [.statuses], resolve: true) let request = Client.search(query: url.serialized(), types: [.statuses], resolve: true)
do { do {
let (results, _) = try await mastodonController.run(request) let (results, _) = try await mastodonController.run(request)
guard let status = results.statuses.first(where: { $0.url?.serialized() == url }) else { guard let status = results.statuses.first(where: { $0.url == url }) else {
throw UnableToResolveError() throw UnableToResolveError()
}
_ = mastodonController.persistentContainer.addOrUpdateOnViewContext(status: status)
mode = .localID(status.id)
return status.id
} catch {
state = .unableToResolve(error)
return nil
} }
_ = mastodonController.persistentContainer.addOrUpdateOnViewContext(status: status)
mode = .localID(status.id)
return status.id
} catch {
state = .unableToResolve(error)
return nil
} }
} }
private func mainStatusLoaded(_ mainStatus: StatusMO) { @MainActor
if let accountID = mastodonController.accountInfo?.id { private func mainStatusLoaded(_ mainStatus: StatusMO) async {
userActivity = UserActivityManager.showConversationActivity(mainStatusID: mainStatus.id, accountID: accountID) let vc = ConversationCollectionViewController(for: mainStatus.id, state: mainStatusState, mastodonController: mastodonController)
}
let vc = ConversationCollectionViewController(for: mainStatus.id, state: mainStatusState, conversationViewController: self)
vc.statusIDToScrollToOnLoad = statusIDToScrollToOnLoad ?? mainStatus.id vc.statusIDToScrollToOnLoad = statusIDToScrollToOnLoad ?? mainStatus.id
vc.showStatusesAutomatically = showStatusesAutomatically vc.showStatusesAutomatically = showStatusesAutomatically
vc.addMainStatus(mainStatus) vc.addMainStatus(mainStatus)
state = .displaying(vc) state = .displaying(vc)
if case .preloaded(let tree) = mode { await loadContext(for: mainStatus)
vc.addTree(tree, mainStatus: mainStatus)
} else {
Task { @MainActor in
await loadTree(for: mainStatus)
}
}
} }
@MainActor @MainActor
private func loadTree(for mainStatus: StatusMO) async { private func loadContext(for mainStatus: StatusMO) async {
guard case .displaying(_) = state, guard case .displaying(_) = state else {
let context = await loadContext(for: mainStatus) else {
return return
} }
await mastodonController.persistentContainer.addAll(statuses: context.ancestors + context.descendants)
let ancestorIDs = context.ancestors.map(\.id)
let ancestorsReq = StatusMO.fetchRequest()
ancestorsReq.predicate = NSPredicate(format: "id in %@", ancestorIDs)
let ancestors = try? mastodonController.persistentContainer.viewContext.fetch(ancestorsReq)
let descendantIDs = context.descendants.map(\.id)
let descendantsReq = StatusMO.fetchRequest()
descendantsReq.predicate = NSPredicate(format: "id IN %@", descendantIDs)
let descendants = try? mastodonController.persistentContainer.viewContext.fetch(descendantsReq)
let tree = ConversationTree.build(for: mainStatus, ancestors: ancestors ?? [], descendants: descendants ?? [])
guard case .displaying(let vc) = state else {
return
}
vc.addTree(tree, mainStatus: mainStatus)
}
private func loadContext(for mainStatus: StatusMO) async -> ConversationContext? {
let request = Status.getContext(mainStatus.id) let request = Status.getContext(mainStatus.id)
do { do {
let (context, _) = try await mastodonController.run(request) let (context, _) = try await mastodonController.run(request)
return context guard case .displaying(let vc) = state else {
return
}
await vc.addContext(context, for: mainStatus)
} catch { } catch {
guard case .displaying(_) = state else { guard case .displaying(_) = state else {
return nil return
} }
let error = error as! Client.Error let error = error as! Client.Error
let config = ToastConfiguration(from: error, with: "Error Loading Context", in: self) { [weak self] toast in let config = ToastConfiguration(from: error, with: "Error Loading Context", in: self) { [weak self] toast in
toast.dismissToast(animated: true) toast.dismissToast(animated: true)
await self?.loadTree(for: mainStatus) await self?.loadContext(for: mainStatus)
} }
self.showToast(configuration: config, animated: true) self.showToast(configuration: config, animated: true)
return nil
} }
} }
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
@ -399,7 +341,6 @@ extension ConversationViewController {
enum Mode { enum Mode {
case localID(String) case localID(String)
case resolve(URL) case resolve(URL)
case preloaded(ConversationTree)
} }
} }
@ -425,17 +366,6 @@ 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