From aaa031f2122b6f705bb0696473d09ec5d9148f5d Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sun, 19 Feb 2023 15:23:25 -0500 Subject: [PATCH] First pass at strict sendability checking --- .../Pachyderm/Sources/Pachyderm/Client.swift | 4 +- .../Sources/Pachyderm/Model/Account.swift | 6 +- .../Sources/Pachyderm/Model/Application.swift | 4 +- .../Sources/Pachyderm/Model/Attachment.swift | 10 ++-- .../Sources/Pachyderm/Model/Card.swift | 6 +- .../Pachyderm/Model/ConversationContext.swift | 2 +- .../Pachyderm/Model/DirectoryOrder.swift | 2 +- .../Sources/Pachyderm/Model/Emoji.swift | 4 +- .../Sources/Pachyderm/Model/FilterV1.swift | 4 +- .../Sources/Pachyderm/Model/FilterV2.swift | 6 +- .../Sources/Pachyderm/Model/Hashtag.swift | 4 +- .../Sources/Pachyderm/Model/History.swift | 4 +- .../Sources/Pachyderm/Model/Instance.swift | 16 +++--- .../Sources/Pachyderm/Model/List.swift | 2 +- .../Pachyderm/Model/LoginSettings.swift | 2 +- .../Sources/Pachyderm/Model/Mention.swift | 2 +- .../Sources/Pachyderm/Model/NodeInfo.swift | 4 +- .../Pachyderm/Model/Notification.swift | 6 +- .../Sources/Pachyderm/Model/Poll.swift | 4 +- .../Pachyderm/Model/PushSubscription.swift | 2 +- .../Model/RegisteredApplication.swift | 4 +- .../Pachyderm/Model/Relationship.swift | 2 +- .../Sources/Pachyderm/Model/Report.swift | 2 +- .../Sources/Pachyderm/Model/Scope.swift | 2 +- .../Pachyderm/Model/SearchResultType.swift | 2 +- .../Pachyderm/Model/SearchResults.swift | 2 +- .../Sources/Pachyderm/Model/Status.swift | 4 +- .../Pachyderm/Model/StatusContentType.swift | 2 +- .../Sources/Pachyderm/Model/Suggestion.swift | 4 +- .../Sources/Pachyderm/Model/Timeline.swift | 2 +- .../Pachyderm/Model/TimelineMarkers.swift | 4 +- .../Sources/Pachyderm/Model/WellKnown.swift | 2 +- .../Sources/Pachyderm/Request/Body.swift | 4 +- .../Sources/Pachyderm/Request/Endpoint.swift | 4 +- .../Pachyderm/Request/FormAttachment.swift | 2 +- .../Sources/Pachyderm/Request/Method.swift | 2 +- .../Sources/Pachyderm/Request/Parameter.swift | 2 +- .../Sources/Pachyderm/Request/Request.swift | 2 +- .../Pachyderm/Request/RequestRange.swift | 2 +- .../Sources/Pachyderm/Response/Empty.swift | 2 +- .../Pachyderm/Response/Pagination.swift | 2 +- .../Sources/Pachyderm/Response/Response.swift | 2 +- .../Pachyderm/Utilities/CollapseState.swift | 7 +-- .../Utilities/NotificationGroup.swift | 2 + .../Sources/TTTKit/Logic/GameController.swift | 1 + Tusker.xcodeproj/project.pbxproj | 8 +-- Tusker/API/CreateListService.swift | 6 +- Tusker/API/MastodonController.swift | 6 +- Tusker/Activities/OpenInSafariActivity.swift | 6 +- Tusker/Caching/ImageDataCache.swift | 7 ++- Tusker/Extensions/MainActor+Unsafe.swift | 55 +++++++++++++++++++ Tusker/Filterer.swift | 2 +- Tusker/Preferences/StatusSwipeAction.swift | 8 ++- Tusker/SavedDataManager.swift | 2 +- .../AccountFollowsListViewController.swift | 2 +- .../Large Image/LargeImageContentView.swift | 4 +- .../Preferences/AcknowledgementsView.swift | 31 +++++++++++ .../Preferences/AdvancedPrefsView.swift | 11 ++-- Tusker/Screens/Utilities/Previewing.swift | 8 +-- .../Utilities/StatusTablePrefetching.swift | 39 ------------- ...TimelineLikeCollectionViewController.swift | 5 +- Tusker/TimelineLikeController.swift | 4 +- Tusker/TuskerNavigationDelegate.swift | 1 + Tusker/Views/BaseEmojiLabel.swift | 6 +- .../Profile Header/ProfileFieldsView.swift | 1 + 65 files changed, 214 insertions(+), 158 deletions(-) create mode 100644 Tusker/Extensions/MainActor+Unsafe.swift delete mode 100644 Tusker/Screens/Utilities/StatusTablePrefetching.swift diff --git a/Packages/Pachyderm/Sources/Pachyderm/Client.swift b/Packages/Pachyderm/Sources/Pachyderm/Client.swift index b78f35db..02bd0955 100644 --- a/Packages/Pachyderm/Sources/Pachyderm/Client.swift +++ b/Packages/Pachyderm/Sources/Pachyderm/Client.swift @@ -484,7 +484,7 @@ public class Client { } extension Client { - public struct Error: LocalizedError { + public struct Error: LocalizedError, Sendable { public let requestMethod: Method public let requestEndpoint: Endpoint public let type: ErrorType @@ -519,7 +519,7 @@ extension Client { } } } - public enum ErrorType: LocalizedError { + public enum ErrorType: LocalizedError, Sendable { case networkError(Swift.Error) case unexpectedStatus(Int) case invalidRequest diff --git a/Packages/Pachyderm/Sources/Pachyderm/Model/Account.swift b/Packages/Pachyderm/Sources/Pachyderm/Model/Account.swift index 90f316f9..47bee445 100644 --- a/Packages/Pachyderm/Sources/Pachyderm/Model/Account.swift +++ b/Packages/Pachyderm/Sources/Pachyderm/Model/Account.swift @@ -8,7 +8,7 @@ import Foundation -public final class Account: AccountProtocol, Decodable { +public final class Account: AccountProtocol, Decodable, Sendable { public let id: String public let username: String public let acct: String @@ -25,7 +25,7 @@ public final class Account: AccountProtocol, Decodable { public let avatarStatic: URL? public let header: URL? public let headerStatic: URL? - public private(set) var emojis: [Emoji] + public let emojis: [Emoji] public let moved: Bool? public let movedTo: Account? public let fields: [Field] @@ -171,7 +171,7 @@ extension Account: CustomDebugStringConvertible { } extension Account { - public struct Field: Codable, Equatable { + public struct Field: Codable, Equatable, Sendable { public let name: String public let value: String public let verifiedAt: Date? diff --git a/Packages/Pachyderm/Sources/Pachyderm/Model/Application.swift b/Packages/Pachyderm/Sources/Pachyderm/Model/Application.swift index 4a762e05..287deaf9 100644 --- a/Packages/Pachyderm/Sources/Pachyderm/Model/Application.swift +++ b/Packages/Pachyderm/Sources/Pachyderm/Model/Application.swift @@ -8,11 +8,11 @@ import Foundation -public class Application: Decodable { +public struct Application: Decodable, Sendable { public let name: String public let website: URL? - public required init(from decoder: Decoder) throws { + public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.name = try container.decode(String.self, forKey: .name) diff --git a/Packages/Pachyderm/Sources/Pachyderm/Model/Attachment.swift b/Packages/Pachyderm/Sources/Pachyderm/Model/Attachment.swift index 309e9fcf..4de49d66 100644 --- a/Packages/Pachyderm/Sources/Pachyderm/Model/Attachment.swift +++ b/Packages/Pachyderm/Sources/Pachyderm/Model/Attachment.swift @@ -8,7 +8,7 @@ import Foundation -public class Attachment: Codable { +public struct Attachment: Codable, Sendable { public let id: String public let kind: Kind public let url: URL @@ -25,7 +25,7 @@ public class Attachment: Codable { ], nil)) } - required public init(from decoder: Decoder) throws { + public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.id = try container.decode(String.self, forKey: .id) self.kind = try container.decode(Kind.self, forKey: .kind) @@ -50,7 +50,7 @@ public class Attachment: Codable { } extension Attachment { - public enum Kind: String, Codable { + public enum Kind: String, Codable, Sendable { case image case video case gifv @@ -77,7 +77,7 @@ extension Attachment { } extension Attachment { - public struct Metadata: Codable { + public struct Metadata: Codable, Sendable { public let length: String? public let duration: Float? public let audioEncoding: String? @@ -108,7 +108,7 @@ extension Attachment { } } - public struct ImageMetadata: Codable { + public struct ImageMetadata: Codable, Sendable { public let width: Int? public let height: Int? public let size: String? diff --git a/Packages/Pachyderm/Sources/Pachyderm/Model/Card.swift b/Packages/Pachyderm/Sources/Pachyderm/Model/Card.swift index 5882b04f..f7396268 100644 --- a/Packages/Pachyderm/Sources/Pachyderm/Model/Card.swift +++ b/Packages/Pachyderm/Sources/Pachyderm/Model/Card.swift @@ -9,7 +9,7 @@ import Foundation import WebURL -public class Card: Codable { +public struct Card: Codable, Sendable { public let url: WebURL public let title: String public let description: String @@ -26,7 +26,7 @@ public class Card: Codable { /// Only present when returned from the trending links endpoint public let history: [History]? - public required init(from decoder: Decoder) throws { + public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.url = try container.decode(WebURL.self, forKey: .url) @@ -75,7 +75,7 @@ public class Card: Codable { } extension Card { - public enum Kind: String, Codable { + public enum Kind: String, Codable, Sendable { case link case photo case video diff --git a/Packages/Pachyderm/Sources/Pachyderm/Model/ConversationContext.swift b/Packages/Pachyderm/Sources/Pachyderm/Model/ConversationContext.swift index 014a1292..7658c923 100644 --- a/Packages/Pachyderm/Sources/Pachyderm/Model/ConversationContext.swift +++ b/Packages/Pachyderm/Sources/Pachyderm/Model/ConversationContext.swift @@ -8,7 +8,7 @@ import Foundation -public class ConversationContext: Decodable { +public struct ConversationContext: Decodable, Sendable { public let ancestors: [Status] public let descendants: [Status] diff --git a/Packages/Pachyderm/Sources/Pachyderm/Model/DirectoryOrder.swift b/Packages/Pachyderm/Sources/Pachyderm/Model/DirectoryOrder.swift index bb25aebb..95dc446d 100644 --- a/Packages/Pachyderm/Sources/Pachyderm/Model/DirectoryOrder.swift +++ b/Packages/Pachyderm/Sources/Pachyderm/Model/DirectoryOrder.swift @@ -8,7 +8,7 @@ import Foundation -public enum DirectoryOrder: String, CaseIterable { +public enum DirectoryOrder: String, CaseIterable, Sendable { case active case new } diff --git a/Packages/Pachyderm/Sources/Pachyderm/Model/Emoji.swift b/Packages/Pachyderm/Sources/Pachyderm/Model/Emoji.swift index 505fafb8..31163451 100644 --- a/Packages/Pachyderm/Sources/Pachyderm/Model/Emoji.swift +++ b/Packages/Pachyderm/Sources/Pachyderm/Model/Emoji.swift @@ -9,7 +9,7 @@ import Foundation import WebURL -public class Emoji: Codable { +public struct Emoji: Codable, Sendable { public let shortcode: String // 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 @@ -18,7 +18,7 @@ public class Emoji: Codable { public let visibleInPicker: Bool public let category: String? - public required init(from decoder: Decoder) throws { + public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.shortcode = try container.decode(String.self, forKey: .shortcode) diff --git a/Packages/Pachyderm/Sources/Pachyderm/Model/FilterV1.swift b/Packages/Pachyderm/Sources/Pachyderm/Model/FilterV1.swift index 125fe61c..d8066be0 100644 --- a/Packages/Pachyderm/Sources/Pachyderm/Model/FilterV1.swift +++ b/Packages/Pachyderm/Sources/Pachyderm/Model/FilterV1.swift @@ -8,7 +8,7 @@ import Foundation -public struct FilterV1: Decodable { +public struct FilterV1: Decodable, Sendable { public let id: String public let phrase: String private let context: [String] @@ -45,7 +45,7 @@ public struct FilterV1: Decodable { } extension FilterV1 { - public enum Context: String, Decodable, CaseIterable { + public enum Context: String, Decodable, CaseIterable, Sendable { case home case notifications case `public` diff --git a/Packages/Pachyderm/Sources/Pachyderm/Model/FilterV2.swift b/Packages/Pachyderm/Sources/Pachyderm/Model/FilterV2.swift index bf68c10a..9075f7f3 100644 --- a/Packages/Pachyderm/Sources/Pachyderm/Model/FilterV2.swift +++ b/Packages/Pachyderm/Sources/Pachyderm/Model/FilterV2.swift @@ -7,7 +7,7 @@ import Foundation -public struct FilterV2: Decodable { +public struct FilterV2: Decodable, Sendable { public let id: String public let title: String public let context: [FilterV1.Context] @@ -80,14 +80,14 @@ public struct FilterV2: Decodable { } extension FilterV2 { - public enum Action: String, Decodable, Hashable, CaseIterable { + public enum Action: String, Decodable, Hashable, CaseIterable, Sendable { case warn case hide } } extension FilterV2 { - public struct Keyword: Decodable { + public struct Keyword: Decodable, Sendable { public let id: String public let keyword: String public let wholeWord: Bool diff --git a/Packages/Pachyderm/Sources/Pachyderm/Model/Hashtag.swift b/Packages/Pachyderm/Sources/Pachyderm/Model/Hashtag.swift index 4e7e699f..e5c98bd4 100644 --- a/Packages/Pachyderm/Sources/Pachyderm/Model/Hashtag.swift +++ b/Packages/Pachyderm/Sources/Pachyderm/Model/Hashtag.swift @@ -10,7 +10,7 @@ import Foundation import WebURL import WebURLFoundationExtras -public class Hashtag: Codable { +public struct Hashtag: Codable, Sendable { public let name: String public let url: WebURL /// Only present when returned from the trending hashtags endpoint @@ -25,7 +25,7 @@ public class Hashtag: Codable { self.following = nil } - public required init(from decoder: Decoder) throws { + public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.name = try container.decode(String.self, forKey: .name) // pixelfed (possibly others) don't fully escape special characters in the hashtag url diff --git a/Packages/Pachyderm/Sources/Pachyderm/Model/History.swift b/Packages/Pachyderm/Sources/Pachyderm/Model/History.swift index 4555c22c..0b3ba9e6 100644 --- a/Packages/Pachyderm/Sources/Pachyderm/Model/History.swift +++ b/Packages/Pachyderm/Sources/Pachyderm/Model/History.swift @@ -8,12 +8,12 @@ import Foundation -public class History: Codable { +public struct History: Codable, Sendable { public let day: Date public let uses: Int public let accounts: Int - public required init(from decoder: Decoder) throws { + public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) if let day = try? container.decode(Date.self, forKey: .day) { diff --git a/Packages/Pachyderm/Sources/Pachyderm/Model/Instance.swift b/Packages/Pachyderm/Sources/Pachyderm/Model/Instance.swift index 10a0ab4b..b3474209 100644 --- a/Packages/Pachyderm/Sources/Pachyderm/Model/Instance.swift +++ b/Packages/Pachyderm/Sources/Pachyderm/Model/Instance.swift @@ -8,7 +8,7 @@ import Foundation -public class Instance: Decodable { +public struct Instance: Decodable, Sendable { public let uri: String public let title: String public let description: String @@ -37,7 +37,7 @@ public class Instance: Decodable { } // we need a custom decoder, because all API-compatible implementations don't return some data in the same format - public required init(from decoder: Decoder) throws { + public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.uri = try container.decode(String.self, forKey: .uri) self.title = try container.decode(String.self, forKey: .title) @@ -93,7 +93,7 @@ public class Instance: Decodable { } extension Instance { - public struct Stats: Decodable { + public struct Stats: Decodable, Sendable { public let domainCount: Int? public let statusCount: Int? public let userCount: Int? @@ -107,7 +107,7 @@ extension Instance { } extension Instance { - public struct Configuration: Decodable { + public struct Configuration: Decodable, Sendable { public let statuses: StatusesConfiguration public let mediaAttachments: MediaAttachmentsConfiguration /// Use Instance.pollsConfiguration to support older instance that don't have this nested @@ -122,7 +122,7 @@ extension Instance { } extension Instance { - public struct StatusesConfiguration: Decodable { + public struct StatusesConfiguration: Decodable, Sendable { public let maxCharacters: Int public let maxMediaAttachments: Int public let charactersReservedPerURL: Int @@ -136,7 +136,7 @@ extension Instance { } extension Instance { - public struct MediaAttachmentsConfiguration: Decodable { + public struct MediaAttachmentsConfiguration: Decodable, Sendable { public let supportedMIMETypes: [String] public let imageSizeLimit: Int public let imageMatrixLimit: Int @@ -156,7 +156,7 @@ extension Instance { } extension Instance { - public struct PollsConfiguration: Decodable { + public struct PollsConfiguration: Decodable, Sendable { public let maxOptions: Int public let maxCharactersPerOption: Int public let minExpiration: TimeInterval @@ -172,7 +172,7 @@ extension Instance { } extension Instance { - public struct Rule: Decodable, Identifiable { + public struct Rule: Decodable, Identifiable, Sendable { public let id: String public let text: String } diff --git a/Packages/Pachyderm/Sources/Pachyderm/Model/List.swift b/Packages/Pachyderm/Sources/Pachyderm/Model/List.swift index 6b51f63d..6ff289e5 100644 --- a/Packages/Pachyderm/Sources/Pachyderm/Model/List.swift +++ b/Packages/Pachyderm/Sources/Pachyderm/Model/List.swift @@ -8,7 +8,7 @@ import Foundation -public class List: Decodable, Equatable, Hashable { +public struct List: Decodable, Equatable, Hashable, Sendable { public let id: String public let title: String diff --git a/Packages/Pachyderm/Sources/Pachyderm/Model/LoginSettings.swift b/Packages/Pachyderm/Sources/Pachyderm/Model/LoginSettings.swift index 3f76388d..a6cbe7d6 100644 --- a/Packages/Pachyderm/Sources/Pachyderm/Model/LoginSettings.swift +++ b/Packages/Pachyderm/Sources/Pachyderm/Model/LoginSettings.swift @@ -8,7 +8,7 @@ import Foundation -public class LoginSettings: Decodable { +public struct LoginSettings: Decodable, Sendable { public let accessToken: String private let scope: String? diff --git a/Packages/Pachyderm/Sources/Pachyderm/Model/Mention.swift b/Packages/Pachyderm/Sources/Pachyderm/Model/Mention.swift index cde4c7a8..61425757 100644 --- a/Packages/Pachyderm/Sources/Pachyderm/Model/Mention.swift +++ b/Packages/Pachyderm/Sources/Pachyderm/Model/Mention.swift @@ -9,7 +9,7 @@ import Foundation import WebURL -public struct Mention: Codable { +public struct Mention: Codable, Sendable { public let url: WebURL public let username: String public let acct: String diff --git a/Packages/Pachyderm/Sources/Pachyderm/Model/NodeInfo.swift b/Packages/Pachyderm/Sources/Pachyderm/Model/NodeInfo.swift index 5148eef8..555674c8 100644 --- a/Packages/Pachyderm/Sources/Pachyderm/Model/NodeInfo.swift +++ b/Packages/Pachyderm/Sources/Pachyderm/Model/NodeInfo.swift @@ -8,11 +8,11 @@ import Foundation -public struct NodeInfo: Decodable { +public struct NodeInfo: Decodable, Sendable { public let version: String public let software: Software - public struct Software: Decodable { + public struct Software: Decodable, Sendable { public let name: String public let version: String } diff --git a/Packages/Pachyderm/Sources/Pachyderm/Model/Notification.swift b/Packages/Pachyderm/Sources/Pachyderm/Model/Notification.swift index 405d89e0..e2789ec2 100644 --- a/Packages/Pachyderm/Sources/Pachyderm/Model/Notification.swift +++ b/Packages/Pachyderm/Sources/Pachyderm/Model/Notification.swift @@ -8,14 +8,14 @@ import Foundation -public class Notification: Decodable { +public struct Notification: Decodable, Sendable { public let id: String public let kind: Kind public let createdAt: Date public let account: Account public let status: Status? - public required init(from decoder: Decoder) throws { + public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.id = try container.decode(String.self, forKey: .id) @@ -45,7 +45,7 @@ public class Notification: Decodable { } extension Notification { - public enum Kind: String, Decodable, CaseIterable { + public enum Kind: String, Decodable, CaseIterable, Sendable { case mention case reblog case favourite diff --git a/Packages/Pachyderm/Sources/Pachyderm/Model/Poll.swift b/Packages/Pachyderm/Sources/Pachyderm/Model/Poll.swift index 5162ceb2..6e6668f8 100644 --- a/Packages/Pachyderm/Sources/Pachyderm/Model/Poll.swift +++ b/Packages/Pachyderm/Sources/Pachyderm/Model/Poll.swift @@ -8,7 +8,7 @@ import Foundation -public final class Poll: Codable { +public struct Poll: Codable, Sendable { public let id: String public let expiresAt: Date? public let expired: Bool @@ -43,7 +43,7 @@ public final class Poll: Codable { } extension Poll { - public final class Option: Codable { + public struct Option: Codable, Sendable { public let title: String public let votesCount: Int? diff --git a/Packages/Pachyderm/Sources/Pachyderm/Model/PushSubscription.swift b/Packages/Pachyderm/Sources/Pachyderm/Model/PushSubscription.swift index b6812966..18923722 100644 --- a/Packages/Pachyderm/Sources/Pachyderm/Model/PushSubscription.swift +++ b/Packages/Pachyderm/Sources/Pachyderm/Model/PushSubscription.swift @@ -8,7 +8,7 @@ import Foundation -public class PushSubscription: Decodable { +public struct PushSubscription: Decodable, Sendable { public let id: String public let endpoint: URL public let serverKey: String diff --git a/Packages/Pachyderm/Sources/Pachyderm/Model/RegisteredApplication.swift b/Packages/Pachyderm/Sources/Pachyderm/Model/RegisteredApplication.swift index ffb5f62d..89a72012 100644 --- a/Packages/Pachyderm/Sources/Pachyderm/Model/RegisteredApplication.swift +++ b/Packages/Pachyderm/Sources/Pachyderm/Model/RegisteredApplication.swift @@ -8,12 +8,12 @@ import Foundation -public class RegisteredApplication: Decodable { +public struct RegisteredApplication: Decodable, Sendable { public let id: String public let clientID: String public let clientSecret: String - public required init(from decoder: Decoder) throws { + public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) // Pixelfed API returns id/client_id as numbers instead of strings diff --git a/Packages/Pachyderm/Sources/Pachyderm/Model/Relationship.swift b/Packages/Pachyderm/Sources/Pachyderm/Model/Relationship.swift index 7e93f651..66245702 100644 --- a/Packages/Pachyderm/Sources/Pachyderm/Model/Relationship.swift +++ b/Packages/Pachyderm/Sources/Pachyderm/Model/Relationship.swift @@ -8,7 +8,7 @@ import Foundation -public class Relationship: Decodable { +public struct Relationship: Decodable, Sendable { public let id: String public let following: Bool public let followedBy: Bool diff --git a/Packages/Pachyderm/Sources/Pachyderm/Model/Report.swift b/Packages/Pachyderm/Sources/Pachyderm/Model/Report.swift index c95582fc..18f47588 100644 --- a/Packages/Pachyderm/Sources/Pachyderm/Model/Report.swift +++ b/Packages/Pachyderm/Sources/Pachyderm/Model/Report.swift @@ -8,7 +8,7 @@ import Foundation -public class Report: Decodable { +public struct Report: Decodable, Sendable { public let id: String public let actionTaken: Bool diff --git a/Packages/Pachyderm/Sources/Pachyderm/Model/Scope.swift b/Packages/Pachyderm/Sources/Pachyderm/Model/Scope.swift index 8f8736ea..75a04b77 100644 --- a/Packages/Pachyderm/Sources/Pachyderm/Model/Scope.swift +++ b/Packages/Pachyderm/Sources/Pachyderm/Model/Scope.swift @@ -8,7 +8,7 @@ import Foundation -public enum Scope: String { +public enum Scope: String, Sendable { case read case write case follow diff --git a/Packages/Pachyderm/Sources/Pachyderm/Model/SearchResultType.swift b/Packages/Pachyderm/Sources/Pachyderm/Model/SearchResultType.swift index 7c3eac95..e40ebeab 100644 --- a/Packages/Pachyderm/Sources/Pachyderm/Model/SearchResultType.swift +++ b/Packages/Pachyderm/Sources/Pachyderm/Model/SearchResultType.swift @@ -8,7 +8,7 @@ import Foundation -public enum SearchResultType: String { +public enum SearchResultType: String, Sendable { case accounts case hashtags case statuses diff --git a/Packages/Pachyderm/Sources/Pachyderm/Model/SearchResults.swift b/Packages/Pachyderm/Sources/Pachyderm/Model/SearchResults.swift index 3ee90764..56013a36 100644 --- a/Packages/Pachyderm/Sources/Pachyderm/Model/SearchResults.swift +++ b/Packages/Pachyderm/Sources/Pachyderm/Model/SearchResults.swift @@ -8,7 +8,7 @@ import Foundation -public class SearchResults: Decodable { +public struct SearchResults: Decodable, Sendable { public let accounts: [Account] public let statuses: [Status] public let hashtags: [Hashtag] diff --git a/Packages/Pachyderm/Sources/Pachyderm/Model/Status.swift b/Packages/Pachyderm/Sources/Pachyderm/Model/Status.swift index b247af62..6941e09a 100644 --- a/Packages/Pachyderm/Sources/Pachyderm/Model/Status.swift +++ b/Packages/Pachyderm/Sources/Pachyderm/Model/Status.swift @@ -9,7 +9,7 @@ import Foundation import WebURL -public final class Status: StatusProtocol, Decodable { +public final class Status: StatusProtocol, Decodable, Sendable { public let id: String public let uri: String public let url: WebURL? @@ -188,7 +188,7 @@ public final class Status: StatusProtocol, Decodable { } extension Status { - public enum Visibility: String, Codable, CaseIterable { + public enum Visibility: String, Codable, CaseIterable, Sendable { case `public` case unlisted case `private` diff --git a/Packages/Pachyderm/Sources/Pachyderm/Model/StatusContentType.swift b/Packages/Pachyderm/Sources/Pachyderm/Model/StatusContentType.swift index c00b8e62..406b79c3 100644 --- a/Packages/Pachyderm/Sources/Pachyderm/Model/StatusContentType.swift +++ b/Packages/Pachyderm/Sources/Pachyderm/Model/StatusContentType.swift @@ -8,7 +8,7 @@ import Foundation -public enum StatusContentType: String, Codable, CaseIterable { +public enum StatusContentType: String, Codable, CaseIterable, Sendable { case plain, markdown, html var mimeType: String { diff --git a/Packages/Pachyderm/Sources/Pachyderm/Model/Suggestion.swift b/Packages/Pachyderm/Sources/Pachyderm/Model/Suggestion.swift index 074b2139..1a290df1 100644 --- a/Packages/Pachyderm/Sources/Pachyderm/Model/Suggestion.swift +++ b/Packages/Pachyderm/Sources/Pachyderm/Model/Suggestion.swift @@ -7,7 +7,7 @@ import Foundation -public struct Suggestion: Decodable { +public struct Suggestion: Decodable, Sendable { public let source: Source public let account: Account @@ -17,7 +17,7 @@ public struct Suggestion: Decodable { } extension Suggestion { - public enum Source: String, Decodable { + public enum Source: String, Decodable, Sendable { case staff case pastInteractions = "past_interactions" case global diff --git a/Packages/Pachyderm/Sources/Pachyderm/Model/Timeline.swift b/Packages/Pachyderm/Sources/Pachyderm/Model/Timeline.swift index 3be1b6e0..4310ca2f 100644 --- a/Packages/Pachyderm/Sources/Pachyderm/Model/Timeline.swift +++ b/Packages/Pachyderm/Sources/Pachyderm/Model/Timeline.swift @@ -8,7 +8,7 @@ import Foundation -public enum Timeline: Equatable, Hashable { +public enum Timeline: Equatable, Hashable, Sendable { case home case `public`(local: Bool) case tag(hashtag: String) diff --git a/Packages/Pachyderm/Sources/Pachyderm/Model/TimelineMarkers.swift b/Packages/Pachyderm/Sources/Pachyderm/Model/TimelineMarkers.swift index f673e2b7..010a8727 100644 --- a/Packages/Pachyderm/Sources/Pachyderm/Model/TimelineMarkers.swift +++ b/Packages/Pachyderm/Sources/Pachyderm/Model/TimelineMarkers.swift @@ -7,7 +7,7 @@ import Foundation -public struct TimelineMarkers: Decodable { +public struct TimelineMarkers: Decodable, Sendable { public let home: Marker? public let notifications: Marker? @@ -26,7 +26,7 @@ public struct TimelineMarkers: Decodable { case notifications } - public struct Marker: Decodable { + public struct Marker: Decodable, Sendable { public let lastReadID: String public let version: Int public let updatedAt: Date diff --git a/Packages/Pachyderm/Sources/Pachyderm/Model/WellKnown.swift b/Packages/Pachyderm/Sources/Pachyderm/Model/WellKnown.swift index a3cd2262..de91a754 100644 --- a/Packages/Pachyderm/Sources/Pachyderm/Model/WellKnown.swift +++ b/Packages/Pachyderm/Sources/Pachyderm/Model/WellKnown.swift @@ -8,7 +8,7 @@ import Foundation -struct WellKnown: Decodable { +struct WellKnown: Decodable, Sendable { let links: [Link] struct Link: Decodable { diff --git a/Packages/Pachyderm/Sources/Pachyderm/Request/Body.swift b/Packages/Pachyderm/Sources/Pachyderm/Request/Body.swift index 5d0165e1..3acdb6a5 100644 --- a/Packages/Pachyderm/Sources/Pachyderm/Request/Body.swift +++ b/Packages/Pachyderm/Sources/Pachyderm/Request/Body.swift @@ -8,7 +8,7 @@ import Foundation -protocol Body { +protocol Body: Sendable { var mimeType: String? { get } var data: Data? { get } } @@ -76,7 +76,7 @@ struct FormDataBody: Body { } } -struct JsonBody: Body { +struct JsonBody: Body { let value: T init(_ value: T) { diff --git a/Packages/Pachyderm/Sources/Pachyderm/Request/Endpoint.swift b/Packages/Pachyderm/Sources/Pachyderm/Request/Endpoint.swift index 1ae874bf..f94a71e1 100644 --- a/Packages/Pachyderm/Sources/Pachyderm/Request/Endpoint.swift +++ b/Packages/Pachyderm/Sources/Pachyderm/Request/Endpoint.swift @@ -8,7 +8,7 @@ import Foundation -public struct Endpoint: ExpressibleByStringInterpolation, CustomStringConvertible { +public struct Endpoint: ExpressibleByStringInterpolation, CustomStringConvertible, Sendable { let components: [Component] public init(stringLiteral value: StringLiteralType) { @@ -54,7 +54,7 @@ public struct Endpoint: ExpressibleByStringInterpolation, CustomStringConvertibl } } - enum Component { + enum Component: Sendable { case literal(String) case interpolated(String) } diff --git a/Packages/Pachyderm/Sources/Pachyderm/Request/FormAttachment.swift b/Packages/Pachyderm/Sources/Pachyderm/Request/FormAttachment.swift index 76cc80c0..ac95a4bb 100644 --- a/Packages/Pachyderm/Sources/Pachyderm/Request/FormAttachment.swift +++ b/Packages/Pachyderm/Sources/Pachyderm/Request/FormAttachment.swift @@ -8,7 +8,7 @@ import Foundation -public struct FormAttachment { +public struct FormAttachment: Sendable { let mimeType: String let data: Data let fileName: String diff --git a/Packages/Pachyderm/Sources/Pachyderm/Request/Method.swift b/Packages/Pachyderm/Sources/Pachyderm/Request/Method.swift index 60cad356..f0111a8b 100644 --- a/Packages/Pachyderm/Sources/Pachyderm/Request/Method.swift +++ b/Packages/Pachyderm/Sources/Pachyderm/Request/Method.swift @@ -8,7 +8,7 @@ import Foundation -public enum Method { +public enum Method: Sendable { case get, post, put, patch, delete } diff --git a/Packages/Pachyderm/Sources/Pachyderm/Request/Parameter.swift b/Packages/Pachyderm/Sources/Pachyderm/Request/Parameter.swift index 82b65aba..65f9f8df 100644 --- a/Packages/Pachyderm/Sources/Pachyderm/Request/Parameter.swift +++ b/Packages/Pachyderm/Sources/Pachyderm/Request/Parameter.swift @@ -8,7 +8,7 @@ import Foundation -struct Parameter { +struct Parameter: Sendable { let name: String let value: String? } diff --git a/Packages/Pachyderm/Sources/Pachyderm/Request/Request.swift b/Packages/Pachyderm/Sources/Pachyderm/Request/Request.swift index 947d1cca..dc857c14 100644 --- a/Packages/Pachyderm/Sources/Pachyderm/Request/Request.swift +++ b/Packages/Pachyderm/Sources/Pachyderm/Request/Request.swift @@ -8,7 +8,7 @@ import Foundation -public struct Request { +public struct Request: Sendable { let method: Method let endpoint: Endpoint let body: Body diff --git a/Packages/Pachyderm/Sources/Pachyderm/Request/RequestRange.swift b/Packages/Pachyderm/Sources/Pachyderm/Request/RequestRange.swift index b683d194..35c1ae5a 100644 --- a/Packages/Pachyderm/Sources/Pachyderm/Request/RequestRange.swift +++ b/Packages/Pachyderm/Sources/Pachyderm/Request/RequestRange.swift @@ -8,7 +8,7 @@ import Foundation -public enum RequestRange { +public enum RequestRange: Sendable { case `default` case count(Int) /// Chronologically immediately before the given ID diff --git a/Packages/Pachyderm/Sources/Pachyderm/Response/Empty.swift b/Packages/Pachyderm/Sources/Pachyderm/Response/Empty.swift index 754b8745..80c653d1 100644 --- a/Packages/Pachyderm/Sources/Pachyderm/Response/Empty.swift +++ b/Packages/Pachyderm/Sources/Pachyderm/Response/Empty.swift @@ -8,6 +8,6 @@ import Foundation -public struct Empty: Decodable { +public struct Empty: Decodable, Sendable { } diff --git a/Packages/Pachyderm/Sources/Pachyderm/Response/Pagination.swift b/Packages/Pachyderm/Sources/Pachyderm/Response/Pagination.swift index a80bb3c8..4abd4838 100644 --- a/Packages/Pachyderm/Sources/Pachyderm/Response/Pagination.swift +++ b/Packages/Pachyderm/Sources/Pachyderm/Response/Pagination.swift @@ -8,7 +8,7 @@ import Foundation -public struct Pagination { +public struct Pagination: Sendable { public let older: RequestRange? public let newer: RequestRange? } diff --git a/Packages/Pachyderm/Sources/Pachyderm/Response/Response.swift b/Packages/Pachyderm/Sources/Pachyderm/Response/Response.swift index 8ec0a5d7..9ad038f5 100644 --- a/Packages/Pachyderm/Sources/Pachyderm/Response/Response.swift +++ b/Packages/Pachyderm/Sources/Pachyderm/Response/Response.swift @@ -8,7 +8,7 @@ import Foundation -public enum Response { +public enum Response: Sendable { case success(Result, Pagination?) case failure(Client.Error) } diff --git a/Packages/Pachyderm/Sources/Pachyderm/Utilities/CollapseState.swift b/Packages/Pachyderm/Sources/Pachyderm/Utilities/CollapseState.swift index 6370d7e9..d574900c 100644 --- a/Packages/Pachyderm/Sources/Pachyderm/Utilities/CollapseState.swift +++ b/Packages/Pachyderm/Sources/Pachyderm/Utilities/CollapseState.swift @@ -8,7 +8,8 @@ import Foundation -public class CollapseState: Equatable { +@MainActor +public final class CollapseState: Sendable { public var collapsible: Bool? public var collapsed: Bool? @@ -33,8 +34,4 @@ public class CollapseState: Equatable { public static var unknown: CollapseState { CollapseState(collapsible: nil, collapsed: nil) } - - public static func == (lhs: CollapseState, rhs: CollapseState) -> Bool { - lhs.collapsible == rhs.collapsible && lhs.collapsed == rhs.collapsed - } } diff --git a/Packages/Pachyderm/Sources/Pachyderm/Utilities/NotificationGroup.swift b/Packages/Pachyderm/Sources/Pachyderm/Utilities/NotificationGroup.swift index 82e01d74..795ca380 100644 --- a/Packages/Pachyderm/Sources/Pachyderm/Utilities/NotificationGroup.swift +++ b/Packages/Pachyderm/Sources/Pachyderm/Utilities/NotificationGroup.swift @@ -14,6 +14,7 @@ public struct NotificationGroup: Identifiable, Hashable { public let kind: Notification.Kind public let statusState: CollapseState? + @MainActor init?(notifications: [Notification]) { guard !notifications.isEmpty else { return nil } self.notifications = notifications @@ -51,6 +52,7 @@ public struct NotificationGroup: Identifiable, Hashable { notifications.append(contentsOf: group.notifications) } + @MainActor public static func createGroups(notifications: [Notification], only allowedTypes: [Notification.Kind]) -> [NotificationGroup] { var groups = [NotificationGroup]() for notification in notifications { diff --git a/Packages/TTTKit/Sources/TTTKit/Logic/GameController.swift b/Packages/TTTKit/Sources/TTTKit/Logic/GameController.swift index 25dd0f10..fe6788cf 100644 --- a/Packages/TTTKit/Sources/TTTKit/Logic/GameController.swift +++ b/Packages/TTTKit/Sources/TTTKit/Logic/GameController.swift @@ -6,6 +6,7 @@ // import Foundation +import Combine public class GameController: ObservableObject { diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index df932201..5b72ab1f 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -220,6 +220,7 @@ D68E6F5F253C9B2D001A1B4C /* BaseEmojiLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68E6F5E253C9B2D001A1B4C /* BaseEmojiLabel.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 */; }; + D691771129A2B76A0054D7EF /* MainActor+Unsafe.swift in Sources */ = {isa = PBXBuildFile; fileRef = D691771029A2B76A0054D7EF /* MainActor+Unsafe.swift */; }; D693A72825CF282E003A14E2 /* TrendingHashtagsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693A72725CF282E003A14E2 /* TrendingHashtagsViewController.swift */; }; D693A72A25CF8C1E003A14E2 /* ProfileDirectoryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693A72925CF8C1E003A14E2 /* ProfileDirectoryViewController.swift */; }; D693A72F25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693A72D25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift */; }; @@ -252,7 +253,6 @@ D6A5BB2D23BBA9C4003BF21D /* MyProfileTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A5BB2C23BBA9C4003BF21D /* MyProfileTests.swift */; }; D6A5BB2F23BBAC97003BF21D /* DelegatingResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A5BB2E23BBAC97003BF21D /* DelegatingResponse.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 */; }; D6A6C11525B62E9700298D0F /* CacheExpiry.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A6C11425B62E9700298D0F /* CacheExpiry.swift */; }; D6A6C11B25B63CEE00298D0F /* MemoryCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A6C11A25B63CEE00298D0F /* MemoryCache.swift */; }; @@ -636,6 +636,7 @@ D68E6F5E253C9B2D001A1B4C /* BaseEmojiLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseEmojiLabel.swift; sourceTree = ""; }; D68FEC4E232C5BC300C84F23 /* SegmentedPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SegmentedPageViewController.swift; sourceTree = ""; }; D690797224A4EF9700023A34 /* UIBezierPath+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIBezierPath+Helpers.swift"; sourceTree = ""; }; + D691771029A2B76A0054D7EF /* MainActor+Unsafe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MainActor+Unsafe.swift"; sourceTree = ""; }; D693A72725CF282E003A14E2 /* TrendingHashtagsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingHashtagsViewController.swift; sourceTree = ""; }; D693A72925CF8C1E003A14E2 /* ProfileDirectoryViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileDirectoryViewController.swift; sourceTree = ""; }; D693A72D25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeaturedProfileCollectionViewCell.swift; sourceTree = ""; }; @@ -668,7 +669,6 @@ D6A5BB2C23BBA9C4003BF21D /* MyProfileTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyProfileTests.swift; sourceTree = ""; }; D6A5BB2E23BBAC97003BF21D /* DelegatingResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DelegatingResponse.swift; sourceTree = ""; }; D6A5BB3023BBAD87003BF21D /* JSONResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONResponse.swift; sourceTree = ""; }; - D6A6C10425B6138A00298D0F /* StatusTablePrefetching.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusTablePrefetching.swift; sourceTree = ""; }; D6A6C10E25B62D2400298D0F /* DiskCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiskCache.swift; sourceTree = ""; }; D6A6C11425B62E9700298D0F /* CacheExpiry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheExpiry.swift; sourceTree = ""; }; D6A6C11A25B63CEE00298D0F /* MemoryCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoryCache.swift; sourceTree = ""; }; @@ -1316,6 +1316,7 @@ D61F758F29353B4300C0B37F /* FileManager+Size.swift */, D61F75AC293AF39000C0B37F /* Filter+Helpers.swift */, D6C3F5162991C1A00009FCFF /* View+AppListStyle.swift */, + D691771029A2B76A0054D7EF /* MainActor+Unsafe.swift */, ); path = Extensions; sourceTree = ""; @@ -1495,7 +1496,6 @@ D6B81F3B2560365300F6E31D /* RefreshableViewController.swift */, D68FEC4E232C5BC300C84F23 /* SegmentedPageViewController.swift */, D63CC7112911F57C000E19DE /* StatusBarTappableViewController.swift */, - D6A6C10425B6138A00298D0F /* StatusTablePrefetching.swift */, D6412B0224AFF6A600F5412E /* TabBarScrollableViewController.swift */, D6B22A0E2560D52D004D82EF /* TabbedPageViewController.swift */, D6538944214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift */, @@ -2200,6 +2200,7 @@ D65B4B58297203A700DABDFB /* ReportSelectRulesView.swift in Sources */, D6C3F4FB299035650009FCFF /* TrendsViewController.swift in Sources */, D6B81F442560390300F6E31D /* MenuController.swift in Sources */, + D691771129A2B76A0054D7EF /* MainActor+Unsafe.swift in Sources */, D65B4B542971F71D00DABDFB /* EditedReport.swift in Sources */, D65234E12561AA68001AF9CF /* NotificationsTableViewController.swift in Sources */, D6A6C11525B62E9700298D0F /* CacheExpiry.swift in Sources */, @@ -2260,7 +2261,6 @@ D6412B0924B0291E00F5412E /* MyProfileViewController.swift in Sources */, D601FA5D297B2E6F00A8E8B5 /* ConversationCollectionViewController.swift in Sources */, D6D12B56292D57E800D528E1 /* AccountCollectionViewCell.swift in Sources */, - D6A6C10525B6138A00298D0F /* StatusTablePrefetching.swift in Sources */, D60088F22980DAA0005B4D00 /* TipJarView.swift in Sources */, D6C82B4125C5BB7E0017F1E6 /* ExploreViewController.swift in Sources */, D6B053A423BD2C8100A066FA /* AssetCollectionsListViewController.swift in Sources */, diff --git a/Tusker/API/CreateListService.swift b/Tusker/API/CreateListService.swift index 90381632..6c733787 100644 --- a/Tusker/API/CreateListService.swift +++ b/Tusker/API/CreateListService.swift @@ -13,11 +13,11 @@ import Pachyderm class CreateListService { private let mastodonController: MastodonController private let present: (UIViewController) -> Void - private let didCreateList: (@MainActor (List) -> Void)? + private let didCreateList: (@MainActor (List) async -> Void)? private var createAction: UIAlertAction? - init(mastodonController: MastodonController, present: @escaping (UIViewController) -> Void, didCreateList: (@MainActor (List) -> Void)?) { + init(mastodonController: MastodonController, present: @escaping (UIViewController) -> Void, didCreateList: (@MainActor (List) async -> Void)?) { self.mastodonController = mastodonController self.present = present self.didCreateList = didCreateList @@ -50,7 +50,7 @@ class CreateListService { let request = Client.createList(title: title) let (list, _) = try await mastodonController.run(request) mastodonController.addedList(list) - self.didCreateList?(list) + await self.didCreateList?(list) } catch { let alert = UIAlertController(title: "Error Creating List", message: error.localizedDescription, preferredStyle: .alert) alert.addAction(UIAlertAction(title: "Cancel", style: .cancel)) diff --git a/Tusker/API/MastodonController.swift b/Tusker/API/MastodonController.swift index d25c9616..4780b1f8 100644 --- a/Tusker/API/MastodonController.swift +++ b/Tusker/API/MastodonController.swift @@ -40,7 +40,7 @@ class MastodonController: ObservableObject { } private let transient: Bool - private(set) lazy var persistentContainer = MastodonCachePersistentStore(for: accountInfo, transient: transient) + private(set) nonisolated lazy var persistentContainer = MastodonCachePersistentStore(for: accountInfo, transient: transient) let instanceURL: URL var accountInfo: LocalData.UserAccountInfo? @@ -110,7 +110,7 @@ class MastodonController: ObservableObject { return response } - func run(_ request: Request) async throws -> (Result, Pagination?) { + func run(_ request: Request) async throws -> (Result, Pagination?) { let response = await runResponse(request) try Task.checkCancellation() switch response { @@ -181,7 +181,7 @@ class MastodonController: ObservableObject { _ = try await (ownAccount, ownInstance) loadLists() - async let _ = await loadFilters() + _ = await loadFilters() } catch { Logging.general.error("MastodonController initialization failed: \(String(describing: error))") } diff --git a/Tusker/Activities/OpenInSafariActivity.swift b/Tusker/Activities/OpenInSafariActivity.swift index 0a3b772f..7f9be32b 100644 --- a/Tusker/Activities/OpenInSafariActivity.swift +++ b/Tusker/Activities/OpenInSafariActivity.swift @@ -39,9 +39,9 @@ class OpenInSafariActivity: UIActivity { static func completionHandler(navigator: TuskerNavigationDelegate, url: URL) -> UIActivityViewController.CompletionWithItemsHandler { return { (activityType, _, _, _) in if activityType == .openInSafari { - let vc = SFSafariViewController(url: url) - vc.preferredControlTintColor = Preferences.shared.accentColor.color - navigator.show(vc) + MainActor.runUnsafely { + navigator.selected(url: url, allowResolveStatuses: false, allowUniversalLinks: false) + } } } } diff --git a/Tusker/Caching/ImageDataCache.swift b/Tusker/Caching/ImageDataCache.swift index e08e3104..9ffb38f8 100644 --- a/Tusker/Caching/ImageDataCache.swift +++ b/Tusker/Caching/ImageDataCache.swift @@ -77,6 +77,7 @@ class ImageDataCache { try? disk?.removeAll() } + // TODO: consider removing this and letting ImageCache just use the UIImage thumbnailing API private func scaleImageIfDesired(data: Data) -> UIImage? { guard let desiredPixelSize = desiredPixelSize, let source = CGImageSourceCreateWithData(data as CFData, [kCGImageSourceShouldCache: false] as CFDictionary) else { @@ -84,14 +85,14 @@ class ImageDataCache { } let maxDimension = max(desiredPixelSize.width, desiredPixelSize.height) - let downsampleOptions = [ + let downsampleOptions: [CFString: Any] = [ kCGImageSourceCreateThumbnailFromImageAlways: true, kCGImageSourceShouldCacheImmediately: true, kCGImageSourceCreateThumbnailWithTransform: true, kCGImageSourceThumbnailMaxPixelSize: maxDimension - ] as CFDictionary + ] - if let downsampled = CGImageSourceCreateThumbnailAtIndex(source, 0, downsampleOptions) { + if let downsampled = CGImageSourceCreateThumbnailAtIndex(source, 0, downsampleOptions as CFDictionary) { return UIImage(cgImage: downsampled) } else { return nil diff --git a/Tusker/Extensions/MainActor+Unsafe.swift b/Tusker/Extensions/MainActor+Unsafe.swift new file mode 100644 index 00000000..69728ac9 --- /dev/null +++ b/Tusker/Extensions/MainActor+Unsafe.swift @@ -0,0 +1,55 @@ +// +// MainActor+Unsafe.swift +// Tusker +// +// Created by Shadowfacts on 2/19/23. +// Copyright © 2023 Shadowfacts. All rights reserved. +// + +import Foundation + +/* + Copied from https://github.com/ChimeHQ/ConcurrencyPlus/blob/fe3b3fd5436b196d8c5211ab2cc4b69fc35524fe/Sources/ConcurrencyPlus/MainActor%2BUnsafe.swift + + Copyright (c) 2022, Chime + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + 3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +public extension MainActor { + /// Execute the given body closure on the main actor without enforcing MainActor isolation. + /// + /// This function exists to work around libraries with incorrect/inconsistent concurrency annotations. You should be **extremely** careful when using it, and only as a last resort. + /// + /// It will crash if run on any non-main thread. + @MainActor(unsafe) + static func runUnsafely(_ body: @MainActor () throws -> T) rethrows -> T { + dispatchPrecondition(condition: .onQueue(.main)) + + return try body() + } +} diff --git a/Tusker/Filterer.swift b/Tusker/Filterer.swift index 31663d70..80a12bf0 100644 --- a/Tusker/Filterer.swift +++ b/Tusker/Filterer.swift @@ -11,7 +11,7 @@ import Pachyderm import Combine /// An opaque object that serves as the cache for the filtered-ness of a particular status. -class FilterState { +class FilterState: @unchecked Sendable { static var unknown: FilterState { FilterState(state: .unknown) } fileprivate var state: State diff --git a/Tusker/Preferences/StatusSwipeAction.swift b/Tusker/Preferences/StatusSwipeAction.swift index 7b5464ea..8beba5fa 100644 --- a/Tusker/Preferences/StatusSwipeAction.swift +++ b/Tusker/Preferences/StatusSwipeAction.swift @@ -128,7 +128,9 @@ private func createReblogAction(status: StatusMO, container: StatusSwipeActionCo private func createShareAction(status: StatusMO, container: StatusSwipeActionContainer) -> UIContextualAction { let action = UIContextualAction(style: .normal, title: "Share") { [unowned container] _, _, completion in - container.navigationDelegate.showMoreOptions(forStatus: status.id, source: .view(container)) + MainActor.runUnsafely { + container.navigationDelegate.showMoreOptions(forStatus: status.id, source: .view(container)) + } completion(true) } // bold to more closesly match other action symbols @@ -166,7 +168,9 @@ private func createBookmarkAction(status: StatusMO, container: StatusSwipeAction private func createOpenInSafariAction(status: StatusMO, container: StatusSwipeActionContainer) -> UIContextualAction { let action = UIContextualAction(style: .normal, title: "Open in Safari") { [unowned container] _, _, completion in - container.navigationDelegate.selected(url: status.url!, allowUniversalLinks: false) + MainActor.runUnsafely { + container.navigationDelegate.selected(url: status.url!, allowUniversalLinks: false) + } completion(true) } action.image = UIImage(systemName: "safari") diff --git a/Tusker/SavedDataManager.swift b/Tusker/SavedDataManager.swift index 8614e52e..52aee5ea 100644 --- a/Tusker/SavedDataManager.swift +++ b/Tusker/SavedDataManager.swift @@ -49,7 +49,7 @@ class SavedDataManager: Codable { var changed = false if let hashtags = savedHashtags[accountID] { - let objects = hashtags.map { + let objects: [[String: Any]] = hashtags.map { ["url": $0.url, "name": $0.name] } let hashtagsReq = NSBatchInsertRequest(entity: SavedHashtag.entity(), objects: objects) diff --git a/Tusker/Screens/Account Follows/AccountFollowsListViewController.swift b/Tusker/Screens/Account Follows/AccountFollowsListViewController.swift index 494c6079..22f32ca7 100644 --- a/Tusker/Screens/Account Follows/AccountFollowsListViewController.swift +++ b/Tusker/Screens/Account Follows/AccountFollowsListViewController.swift @@ -102,7 +102,7 @@ class AccountFollowsListViewController: UIViewController, CollectionViewControll } } - private func request(for range: RequestRange) -> Request<[Account]> { + private nonisolated func request(for range: RequestRange) -> Request<[Account]> { switch mode { case .following: return Account.getFollowing(accountID, range: range.withCount(Self.pageSize)) diff --git a/Tusker/Screens/Large Image/LargeImageContentView.swift b/Tusker/Screens/Large Image/LargeImageContentView.swift index 5568e0a6..197d2bc8 100644 --- a/Tusker/Screens/Large Image/LargeImageContentView.swift +++ b/Tusker/Screens/Large Image/LargeImageContentView.swift @@ -8,8 +8,8 @@ import UIKit import Pachyderm -import AVFoundation -import VisionKit +@preconcurrency import AVFoundation +@preconcurrency import VisionKit protocol LargeImageContentView: UIView { var animationImage: UIImage? { get } diff --git a/Tusker/Screens/Preferences/AcknowledgementsView.swift b/Tusker/Screens/Preferences/AcknowledgementsView.swift index d38647e0..239f004f 100644 --- a/Tusker/Screens/Preferences/AcknowledgementsView.swift +++ b/Tusker/Screens/Preferences/AcknowledgementsView.swift @@ -29,6 +29,37 @@ The above copyright notice and this permission notice shall be included in all c THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +^[[ConcurrencyPlus](https://github.com/ChimeHQ/ConcurrencyPlus)](headingLevel: 2) +BSD 3-Clause License + +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. + ^[[SwiftSoup](https://github.com/scinfu/swiftsoup)](headingLevel: 2) Copyright (c) 2016 Nabil Chatbi diff --git a/Tusker/Screens/Preferences/AdvancedPrefsView.swift b/Tusker/Screens/Preferences/AdvancedPrefsView.swift index de53baba..d5aed245 100644 --- a/Tusker/Screens/Preferences/AdvancedPrefsView.swift +++ b/Tusker/Screens/Preferences/AdvancedPrefsView.swift @@ -79,12 +79,11 @@ struct AdvancedPrefsView : View { } .appGroupedListRowBackground() .task { - CKContainer.default().accountStatus { status, error in - if let error { - Logging.general.error("Unable to get CloudKit status: \(String(describing: error))") - } else { - self.cloudKitStatus = status - } + do { + let status = try await CKContainer.default().accountStatus() + self.cloudKitStatus = status + } catch { + Logging.general.error("Unable to get CloudKit status: \(String(describing: error))") } } } diff --git a/Tusker/Screens/Utilities/Previewing.swift b/Tusker/Screens/Utilities/Previewing.swift index cd35dd38..d4dd20f3 100644 --- a/Tusker/Screens/Utilities/Previewing.swift +++ b/Tusker/Screens/Utilities/Previewing.swift @@ -12,6 +12,7 @@ import Pachyderm import WebURLFoundationExtras import SwiftUI +@MainActor protocol MenuActionProvider: AnyObject { var navigationDelegate: TuskerNavigationDelegate? { get } var toastableViewController: ToastableViewController? { get } @@ -86,10 +87,9 @@ extension MenuActionProvider { self.navigationDelegate!.present($0, animated: true) }) { list in let req = List.add(list, accounts: [accountID]) - mastodonController.run(req) { response in - if case .failure(let error) = response { - self.handleError(error, title: "Error Adding to List") - } + let response = await mastodonController.runResponse(req) + if case .failure(let error) = response { + self.handleError(error, title: "Error Adding to List") } } service.run() diff --git a/Tusker/Screens/Utilities/StatusTablePrefetching.swift b/Tusker/Screens/Utilities/StatusTablePrefetching.swift deleted file mode 100644 index f560b8be..00000000 --- a/Tusker/Screens/Utilities/StatusTablePrefetching.swift +++ /dev/null @@ -1,39 +0,0 @@ -// -// StatusTablePrefetching.swift -// Tusker -// -// Created by Shadowfacts on 1/18/21. -// Copyright © 2021 Shadowfacts. All rights reserved. -// - -import Foundation -import CoreData - -protocol StatusTablePrefetching: TuskerNavigationDelegate { -} - -extension StatusTablePrefetching { - - func prefetchStatuses(with ids: [String]) { - let context = apiController.persistentContainer.prefetchBackgroundContext - context.perform { - guard let statuses = getStatusesWith(ids: ids, in: context) else { - return - } - for status in statuses { - guard let avatar = status.account.avatar else { continue } - ImageCache.avatars.fetchIfNotCached(avatar) - for attachment in status.attachments where attachment.kind == .image { - ImageCache.attachments.fetchIfNotCached(attachment.url) - } - } - } - } - -} - -fileprivate func getStatusesWith(ids: [String], in context: NSManagedObjectContext) -> [StatusMO]? { - let request: NSFetchRequest = StatusMO.fetchRequest() - request.predicate = NSPredicate(format: "id IN %@", ids) - return try? context.fetch(request) -} diff --git a/Tusker/Screens/Utilities/TimelineLikeCollectionViewController.swift b/Tusker/Screens/Utilities/TimelineLikeCollectionViewController.swift index 8736d9d3..36f1536e 100644 --- a/Tusker/Screens/Utilities/TimelineLikeCollectionViewController.swift +++ b/Tusker/Screens/Utilities/TimelineLikeCollectionViewController.swift @@ -23,17 +23,18 @@ protocol TimelineLikeCollectionViewController: UIViewController, TimelineLikeCon var dataSource: UICollectionViewDiffableDataSource! { get } } -protocol TimelineLikeCollectionViewSection: Hashable { +protocol TimelineLikeCollectionViewSection: Hashable, Sendable { static var entries: Self { get } static var footer: Self { get } } -protocol TimelineLikeCollectionViewItem: Hashable { +protocol TimelineLikeCollectionViewItem: Hashable, Sendable { associatedtype TimelineItem static var loadingIndicator: Self { get } static var confirmLoadMore: Self { get } + @MainActor static func fromTimelineItem(_ item: TimelineItem) -> Self } diff --git a/Tusker/TimelineLikeController.swift b/Tusker/TimelineLikeController.swift index deea6c38..ea6171a9 100644 --- a/Tusker/TimelineLikeController.swift +++ b/Tusker/TimelineLikeController.swift @@ -10,7 +10,7 @@ import Foundation import OSLog protocol TimelineLikeControllerDelegate: AnyObject { - associatedtype TimelineItem + associatedtype TimelineItem: Sendable func loadInitial() async throws -> [TimelineItem] @@ -37,7 +37,7 @@ protocol TimelineLikeControllerDelegate: AnyObject { private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "TimelineLikeController") @MainActor -class TimelineLikeController { +class TimelineLikeController { private unowned var delegate: any TimelineLikeControllerDelegate diff --git a/Tusker/TuskerNavigationDelegate.swift b/Tusker/TuskerNavigationDelegate.swift index 1c35e76a..2013eccd 100644 --- a/Tusker/TuskerNavigationDelegate.swift +++ b/Tusker/TuskerNavigationDelegate.swift @@ -10,6 +10,7 @@ import UIKit import SafariServices import Pachyderm +@MainActor protocol TuskerNavigationDelegate: UIViewController, ToastableViewController { var apiController: MastodonController! { get } } diff --git a/Tusker/Views/BaseEmojiLabel.swift b/Tusker/Views/BaseEmojiLabel.swift index 9b706075..4cc760da 100644 --- a/Tusker/Views/BaseEmojiLabel.swift +++ b/Tusker/Views/BaseEmojiLabel.swift @@ -90,8 +90,10 @@ extension BaseEmojiLabel { // even though the closures is invoked on the same thread that withLock is called, so it's unclear why it needs to be @Sendable (FB11494878) // so, just ignore the warnings let emojiAttachments = emojiImages.withLock { - $0.mapValues { image in - NSTextAttachment(emojiImage: image, in: self.emojiFont, with: self.emojiTextColor) + let emojiFont = self.emojiFont + let emojiTextColor = self.emojiTextColor + return $0.mapValues { image in + NSTextAttachment(emojiImage: image, in: emojiFont, with: emojiTextColor) } } let placeholder = usePlaceholders ? NSTextAttachment(emojiPlaceholderIn: self.emojiFont) : nil diff --git a/Tusker/Views/Profile Header/ProfileFieldsView.swift b/Tusker/Views/Profile Header/ProfileFieldsView.swift index 2b41bd4d..73c31264 100644 --- a/Tusker/Views/Profile Header/ProfileFieldsView.swift +++ b/Tusker/Views/Profile Header/ProfileFieldsView.swift @@ -266,6 +266,7 @@ private class ProfileFieldValueView: UIView { } } +@MainActor private struct ProfileFieldVerificationView: View { let acct: String let verifiedAt: Date