mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2025-02-19 19:16:16 +00:00
Updated to resolve all possible Sendability warnings from Swift 6 compatibility mode. (#1072)
Co-authored-by: Jim Dovey <jimdovey@apple.com>
This commit is contained in:
parent
9f026fbc42
commit
d1209e6704
52 changed files with 216 additions and 28 deletions
|
@ -15,7 +15,7 @@ import Network
|
||||||
// Sample code was sending this from a thread to another, let asume @Sendable for this
|
// Sample code was sending this from a thread to another, let asume @Sendable for this
|
||||||
extension NSExtensionContext: @unchecked Sendable {}
|
extension NSExtensionContext: @unchecked Sendable {}
|
||||||
|
|
||||||
class ActionRequestHandler: NSObject, NSExtensionRequestHandling {
|
final class ActionRequestHandler: NSObject, NSExtensionRequestHandling, Sendable {
|
||||||
enum Error: Swift.Error {
|
enum Error: Swift.Error {
|
||||||
case inputProviderNotFound
|
case inputProviderNotFound
|
||||||
case loadedItemHasWrongType
|
case loadedItemHasWrongType
|
||||||
|
|
|
@ -63,6 +63,15 @@
|
||||||
"version" : "0.13.3"
|
"version" : "0.13.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-atomics",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/apple/swift-atomics.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "ff3d2212b6b093db7f177d0855adbc4ef9c5f036",
|
||||||
|
"version" : "1.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"identity" : "swiftsoup",
|
"identity" : "swiftsoup",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
|
|
|
@ -82,6 +82,11 @@ class NotificationService: UNNotificationServiceExtension {
|
||||||
let fileURL = temporaryDirectoryURL.appendingPathComponent(filename)
|
let fileURL = temporaryDirectoryURL.appendingPathComponent(filename)
|
||||||
|
|
||||||
Task {
|
Task {
|
||||||
|
// Warning: Non-sendable type '(any URLSessionTaskDelegate)?' exiting main actor-isolated
|
||||||
|
// context in call to non-isolated instance method 'data(for:delegate:)' cannot cross actor
|
||||||
|
// boundary.
|
||||||
|
// This is on the defaulted-to-nil second parameter of `.data(from:delegate:)`.
|
||||||
|
// There is a Radar tracking this & others like it.
|
||||||
if let (data, _) = try? await URLSession.shared.data(for: .init(url: url)) {
|
if let (data, _) = try? await URLSession.shared.data(for: .init(url: url)) {
|
||||||
if let image = UIImage(data: data) {
|
if let image = UIImage(data: data) {
|
||||||
try? image.pngData()?.write(to: fileURL)
|
try? image.pngData()?.write(to: fileURL)
|
||||||
|
|
|
@ -82,6 +82,9 @@ public struct AccountDetailView: View {
|
||||||
isCurrentUser = currentAccount.account?.id == viewModel.accountId
|
isCurrentUser = currentAccount.account?.id == viewModel.accountId
|
||||||
viewModel.isCurrentUser = isCurrentUser
|
viewModel.isCurrentUser = isCurrentUser
|
||||||
viewModel.client = client
|
viewModel.client = client
|
||||||
|
|
||||||
|
// Avoid capturing non-Sendable `self` just to access the view model.
|
||||||
|
let viewModel = self.viewModel
|
||||||
Task {
|
Task {
|
||||||
await withTaskGroup(of: Void.self) { group in
|
await withTaskGroup(of: Void.self) { group in
|
||||||
group.addTask { await viewModel.fetchAccount() }
|
group.addTask { await viewModel.fetchAccount() }
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import Combine
|
||||||
import DesignSystem
|
import DesignSystem
|
||||||
import EmojiText
|
import EmojiText
|
||||||
import Env
|
import Env
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import Combine
|
||||||
import Foundation
|
import Foundation
|
||||||
import Models
|
import Models
|
||||||
import Network
|
import Network
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import Combine
|
||||||
import DesignSystem
|
import DesignSystem
|
||||||
import Models
|
import Models
|
||||||
import Network
|
import Network
|
||||||
|
@ -61,6 +62,11 @@ public class AppAccountViewModel: ObservableObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func refreshAvatar(account: Account) async {
|
private func refreshAvatar(account: Account) async {
|
||||||
|
// Warning: Non-sendable type '(any URLSessionTaskDelegate)?' exiting main actor-isolated
|
||||||
|
// context in call to non-isolated instance method 'data(for:delegate:)' cannot cross actor
|
||||||
|
// boundary.
|
||||||
|
// This is on the defaulted-to-nil second parameter of `.data(from:delegate:)`.
|
||||||
|
// There is a Radar tracking this & others like it.
|
||||||
if let (data, _) = try? await URLSession.shared.data(from: account.avatar),
|
if let (data, _) = try? await URLSession.shared.data(from: account.avatar),
|
||||||
let image = UIImage(data: data)?.roundedImage
|
let image = UIImage(data: data)?.roundedImage
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import Combine
|
||||||
import Env
|
import Env
|
||||||
import Models
|
import Models
|
||||||
import Network
|
import Network
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import Combine
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
public class SceneDelegate: NSObject, ObservableObject, UIWindowSceneDelegate {
|
public class SceneDelegate: NSObject, ObservableObject, UIWindowSceneDelegate {
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import Combine
|
||||||
import Foundation
|
import Foundation
|
||||||
import Models
|
import Models
|
||||||
import Network
|
import Network
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import Combine
|
||||||
import Foundation
|
import Foundation
|
||||||
import Models
|
import Models
|
||||||
import Network
|
import Network
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import Combine
|
||||||
import CryptoKit
|
import CryptoKit
|
||||||
import Foundation
|
import Foundation
|
||||||
import KeychainSwift
|
import KeychainSwift
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import Combine
|
||||||
import QuickLook
|
import QuickLook
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
@ -69,6 +70,12 @@ public class QuickLook: ObservableObject {
|
||||||
private func localPathFor(url: URL) async throws -> URL {
|
private func localPathFor(url: URL) async throws -> URL {
|
||||||
try? FileManager.default.createDirectory(at: quickLookDir, withIntermediateDirectories: true)
|
try? FileManager.default.createDirectory(at: quickLookDir, withIntermediateDirectories: true)
|
||||||
let path = quickLookDir.appendingPathComponent(url.lastPathComponent)
|
let path = quickLookDir.appendingPathComponent(url.lastPathComponent)
|
||||||
|
|
||||||
|
// Warning: Non-sendable type '(any URLSessionTaskDelegate)?' exiting main actor-isolated
|
||||||
|
// context in call to non-isolated instance method 'data(for:delegate:)' cannot cross actor
|
||||||
|
// boundary.
|
||||||
|
// This is on the defaulted-to-nil second parameter of `.data(from:delegate:)`.
|
||||||
|
// There is a Radar tracking this & others like it.
|
||||||
let data = try await URLSession.shared.data(from: url).0
|
let data = try await URLSession.shared.data(from: url).0
|
||||||
try data.write(to: path)
|
try data.write(to: path)
|
||||||
return path
|
return path
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import Combine
|
||||||
import Foundation
|
import Foundation
|
||||||
import Models
|
import Models
|
||||||
import Network
|
import Network
|
||||||
|
@ -140,7 +141,7 @@ public class RouterPath: ObservableObject {
|
||||||
if let account = results?.accounts.first {
|
if let account = results?.accounts.first {
|
||||||
navigate(to: .accountDetailWithAccount(account: account))
|
navigate(to: .accountDetailWithAccount(account: account))
|
||||||
} else {
|
} else {
|
||||||
await UIApplication.shared.open(url)
|
_ = await UIApplication.shared.open(url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -154,7 +155,7 @@ public class RouterPath: ObservableObject {
|
||||||
if let account = results?.accounts.first {
|
if let account = results?.accounts.first {
|
||||||
navigate(to: .accountDetailWithAccount(account: account))
|
navigate(to: .accountDetailWithAccount(account: account))
|
||||||
} else {
|
} else {
|
||||||
await UIApplication.shared.open(url)
|
_ = await UIApplication.shared.open(url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import Combine
|
||||||
import Foundation
|
import Foundation
|
||||||
import Models
|
import Models
|
||||||
import Network
|
import Network
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import Combine
|
||||||
import Foundation
|
import Foundation
|
||||||
import Models
|
import Models
|
||||||
import Network
|
import Network
|
||||||
|
@ -61,6 +62,16 @@ public class UserPreferences: ObservableObject {
|
||||||
return "enum.swipeactions.icon-only"
|
return "enum.swipeactions.icon-only"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Have to implement this manually here due to compiler not implicitly
|
||||||
|
// inserting `nonisolated`, which leads to a warning:
|
||||||
|
//
|
||||||
|
// Main actor-isolated static property 'allCases' cannot be used to
|
||||||
|
// satisfy nonisolated protocol requirement
|
||||||
|
//
|
||||||
|
nonisolated public static var allCases: [Self] {
|
||||||
|
[.iconWithText, .iconOnly]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public var postVisibility: Models.Visibility {
|
public var postVisibility: Models.Visibility {
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import Combine
|
||||||
import Models
|
import Models
|
||||||
import Network
|
import Network
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
|
@ -17,11 +17,15 @@ let package = Package(
|
||||||
],
|
],
|
||||||
dependencies: [
|
dependencies: [
|
||||||
.package(url: "https://github.com/scinfu/SwiftSoup.git", from: "2.4.3"),
|
.package(url: "https://github.com/scinfu/SwiftSoup.git", from: "2.4.3"),
|
||||||
|
.package(url: "https://github.com/apple/swift-atomics.git", from: "1.0.3"),
|
||||||
],
|
],
|
||||||
targets: [
|
targets: [
|
||||||
.target(
|
.target(
|
||||||
name: "Models",
|
name: "Models",
|
||||||
dependencies: ["SwiftSoup"]
|
dependencies: [
|
||||||
|
"SwiftSoup",
|
||||||
|
.product(name: "Atomics", package: "swift-atomics")
|
||||||
|
]
|
||||||
),
|
),
|
||||||
.testTarget(
|
.testTarget(
|
||||||
name: "ModelsTests",
|
name: "ModelsTests",
|
||||||
|
|
|
@ -121,3 +121,5 @@ public struct FamiliarAccounts: Decodable {
|
||||||
public let id: String
|
public let id: String
|
||||||
public let accounts: [Account]
|
public let accounts: [Account]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension FamiliarAccounts: Sendable {}
|
||||||
|
|
|
@ -27,3 +27,5 @@ public struct AppAccount: Codable, Identifiable, Hashable {
|
||||||
self.oauthToken = oauthToken
|
self.oauthToken = oauthToken
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension AppAccount: Sendable {}
|
||||||
|
|
|
@ -11,3 +11,5 @@ public struct Card: Codable, Identifiable, Equatable, Hashable {
|
||||||
public let type: String
|
public let type: String
|
||||||
public let image: URL?
|
public let image: URL?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension Card: Sendable {}
|
||||||
|
|
|
@ -41,3 +41,5 @@ public struct ConsolidatedNotification: Identifiable {
|
||||||
[.placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder()]
|
[.placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder()]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension ConsolidatedNotification: Sendable {}
|
||||||
|
|
|
@ -22,3 +22,5 @@ public struct Conversation: Identifiable, Decodable, Hashable, Equatable {
|
||||||
.placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder()]
|
.placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder()]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension Conversation: Sendable {}
|
||||||
|
|
|
@ -20,3 +20,8 @@ public struct Filter: Codable, Identifiable, Equatable, Hashable {
|
||||||
public let context: [String]
|
public let context: [String]
|
||||||
public let filterAction: Action
|
public let filterAction: Action
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension Filtered: Sendable {}
|
||||||
|
extension Filter: Sendable {}
|
||||||
|
extension Filter.Action: Sendable {}
|
||||||
|
extension Filter.Context: Sendable {}
|
||||||
|
|
|
@ -9,3 +9,5 @@ public struct InstanceApp: Codable, Identifiable {
|
||||||
public let clientSecret: String
|
public let clientSecret: String
|
||||||
public let vapidKey: String?
|
public let vapidKey: String?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension InstanceApp: Sendable {}
|
||||||
|
|
|
@ -18,3 +18,5 @@ public struct Language: Identifiable, Equatable, Hashable {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension Language: Sendable {}
|
||||||
|
|
|
@ -5,3 +5,5 @@ public struct List: Codable, Identifiable, Equatable, Hashable {
|
||||||
public let title: String
|
public let title: String
|
||||||
public let repliesPolicy: String
|
public let repliesPolicy: String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension List: Sendable {}
|
||||||
|
|
|
@ -21,3 +21,5 @@ public struct MastodonPushNotification: Codable {
|
||||||
case body
|
case body
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension MastodonPushNotification: Sendable {}
|
||||||
|
|
|
@ -29,3 +29,8 @@ public struct MediaAttachment: Codable, Identifiable, Hashable, Equatable {
|
||||||
public let description: String?
|
public let description: String?
|
||||||
public let meta: MetaContainer?
|
public let meta: MetaContainer?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension MediaAttachment: Sendable {}
|
||||||
|
extension MediaAttachment.MetaContainer: Sendable {}
|
||||||
|
extension MediaAttachment.MetaContainer.Meta: Sendable {}
|
||||||
|
extension MediaAttachment.SupportedType: Sendable {}
|
||||||
|
|
|
@ -6,3 +6,5 @@ public struct Mention: Codable, Equatable, Hashable {
|
||||||
public let url: URL
|
public let url: URL
|
||||||
public let acct: String
|
public let acct: String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension Mention: Sendable {}
|
||||||
|
|
|
@ -23,3 +23,6 @@ public struct Notification: Decodable, Identifiable, Equatable {
|
||||||
status: .placeholder())
|
status: .placeholder())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension Notification: Sendable {}
|
||||||
|
extension Notification.NotificationType: Sendable {}
|
||||||
|
|
|
@ -48,3 +48,7 @@ public struct NullableString: Codable, Equatable, Hashable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension Poll: Sendable {}
|
||||||
|
extension Poll.Option: Sendable {}
|
||||||
|
extension NullableString: Sendable {}
|
||||||
|
|
|
@ -15,3 +15,6 @@ public struct PushSubscription: Identifiable, Decodable {
|
||||||
public let serverKey: String
|
public let serverKey: String
|
||||||
public let alerts: Alerts
|
public let alerts: Alerts
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension PushSubscription: Sendable {}
|
||||||
|
extension PushSubscription.Alerts: Sendable {}
|
||||||
|
|
|
@ -50,3 +50,5 @@ public extension Relationship {
|
||||||
notifying = try values.decodeIfPresent(Bool.self, forKey: .notifying) ?? false
|
notifying = try values.decodeIfPresent(Bool.self, forKey: .notifying) ?? false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension Relationship: Sendable {}
|
||||||
|
|
|
@ -14,3 +14,5 @@ public struct SearchResults: Decodable {
|
||||||
accounts.isEmpty && statuses.isEmpty && hashtags.isEmpty
|
accounts.isEmpty && statuses.isEmpty && hashtags.isEmpty
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension SearchResults: Sendable {}
|
||||||
|
|
|
@ -3,3 +3,5 @@ import Foundation
|
||||||
public struct ServerError: Decodable, Error {
|
public struct ServerError: Decodable, Error {
|
||||||
public let error: String?
|
public let error: String?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension ServerError: Sendable {}
|
||||||
|
|
|
@ -33,3 +33,6 @@ public struct ServerPreferences: Decodable {
|
||||||
case autoExpandSpoilers = "reading:expand:spoilers"
|
case autoExpandSpoilers = "reading:expand:spoilers"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension ServerPreferences: Sendable {}
|
||||||
|
extension ServerPreferences.AutoExpandMedia: Sendable {}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import Atomics
|
||||||
|
|
||||||
public struct Application: Codable, Identifiable, Hashable, Equatable {
|
public struct Application: Codable, Identifiable, Hashable, Equatable {
|
||||||
public var id: String {
|
public var id: String {
|
||||||
|
@ -103,6 +104,7 @@ public final class Status: AnyStatus, Codable, Identifiable, Equatable, Hashable
|
||||||
public let sensitive: Bool
|
public let sensitive: Bool
|
||||||
public let language: String?
|
public let language: String?
|
||||||
|
|
||||||
|
|
||||||
public init(id: String, content: HTMLString, account: Account, createdAt: ServerDate, editedAt: ServerDate?, reblog: ReblogStatus?, mediaAttachments: [MediaAttachment], mentions: [Mention], repliesCount: Int, reblogsCount: Int, favouritesCount: Int, card: Card?, favourited: Bool?, reblogged: Bool?, pinned: Bool?, bookmarked: Bool?, emojis: [Emoji], url: String?, application: Application?, inReplyToId: String?, inReplyToAccountId: String?, visibility: Visibility, poll: Poll?, spoilerText: HTMLString, filtered: [Filtered]?, sensitive: Bool, language: String?) {
|
public init(id: String, content: HTMLString, account: Account, createdAt: ServerDate, editedAt: ServerDate?, reblog: ReblogStatus?, mediaAttachments: [MediaAttachment], mentions: [Mention], repliesCount: Int, reblogsCount: Int, favouritesCount: Int, card: Card?, favourited: Bool?, reblogged: Bool?, pinned: Bool?, bookmarked: Bool?, emojis: [Emoji], url: String?, application: Application?, inReplyToId: String?, inReplyToAccountId: String?, visibility: Visibility, poll: Poll?, spoilerText: HTMLString, filtered: [Filtered]?, sensitive: Bool, language: String?) {
|
||||||
self.id = id
|
self.id = id
|
||||||
self.content = content
|
self.content = content
|
||||||
|
@ -228,7 +230,7 @@ public final class ReblogStatus: AnyStatus, Codable, Identifiable, Equatable, Ha
|
||||||
public let bookmarked: Bool?
|
public let bookmarked: Bool?
|
||||||
public let emojis: [Emoji]
|
public let emojis: [Emoji]
|
||||||
public let url: String?
|
public let url: String?
|
||||||
public var application: Application?
|
public let application: Application?
|
||||||
public let inReplyToId: String?
|
public let inReplyToId: String?
|
||||||
public let inReplyToAccountId: String?
|
public let inReplyToAccountId: String?
|
||||||
public let visibility: Visibility
|
public let visibility: Visibility
|
||||||
|
@ -267,3 +269,14 @@ public final class ReblogStatus: AnyStatus, Codable, Identifiable, Equatable, Ha
|
||||||
self.language = language
|
self.language = language
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension Application: Sendable {}
|
||||||
|
extension StatusViewId: Sendable {}
|
||||||
|
|
||||||
|
// Every property in Status is immutable.
|
||||||
|
extension Status: Sendable {}
|
||||||
|
|
||||||
|
// Every property in ReblogStatus is immutable.
|
||||||
|
extension ReblogStatus: Sendable {}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -8,3 +8,5 @@ public struct StatusContext: Decodable {
|
||||||
.init(ancestors: [], descendants: [])
|
.init(ancestors: [], descendants: [])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension StatusContext: Sendable {}
|
||||||
|
|
|
@ -9,3 +9,5 @@ public struct StatusHistory: Decodable, Identifiable {
|
||||||
public let createdAt: ServerDate
|
public let createdAt: ServerDate
|
||||||
public let emojis: [Emoji]
|
public let emojis: [Emoji]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension StatusHistory: Sendable {}
|
||||||
|
|
|
@ -11,3 +11,5 @@ public struct StatusTranslation: Decodable {
|
||||||
self.provider = provider
|
self.provider = provider
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension StatusTranslation: Sendable {}
|
||||||
|
|
|
@ -58,3 +58,7 @@ public struct FeaturedTag: Codable, Identifiable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension Tag: Sendable {}
|
||||||
|
extension Tag.History: Sendable {}
|
||||||
|
extension FeaturedTag: Sendable {}
|
||||||
|
|
|
@ -1,12 +1,17 @@
|
||||||
|
import Combine
|
||||||
import Foundation
|
import Foundation
|
||||||
import Models
|
import Models
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import os
|
||||||
|
|
||||||
public final class Client: ObservableObject, Equatable, Identifiable, Hashable {
|
public final class Client: ObservableObject, Equatable, Identifiable, Hashable {
|
||||||
public static func == (lhs: Client, rhs: Client) -> Bool {
|
public static func == (lhs: Client, rhs: Client) -> Bool {
|
||||||
lhs.isAuth == rhs.isAuth &&
|
let lhsToken = lhs.critical.withLock { $0.oauthToken }
|
||||||
|
let rhsToken = rhs.critical.withLock { $0.oauthToken }
|
||||||
|
|
||||||
|
return (lhsToken != nil) == (rhsToken != nil) &&
|
||||||
lhs.server == rhs.server &&
|
lhs.server == rhs.server &&
|
||||||
lhs.oauthToken?.accessToken == rhs.oauthToken?.accessToken
|
lhsToken?.accessToken == rhsToken?.accessToken
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum Version: String, Sendable {
|
public enum Version: String, Sendable {
|
||||||
|
@ -19,7 +24,10 @@ public final class Client: ObservableObject, Equatable, Identifiable, Hashable {
|
||||||
}
|
}
|
||||||
|
|
||||||
public var id: String {
|
public var id: String {
|
||||||
"\(isAuth)\(server)\(oauthToken?.createdAt ?? 0)"
|
critical.withLock {
|
||||||
|
let isAuth = $0.oauthToken != nil
|
||||||
|
return "\(isAuth)\(server)\($0.oauthToken?.createdAt ?? 0)"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func hash(into hasher: inout Hasher) {
|
public func hash(into hasher: inout Hasher) {
|
||||||
|
@ -28,42 +36,52 @@ public final class Client: ObservableObject, Equatable, Identifiable, Hashable {
|
||||||
|
|
||||||
public let server: String
|
public let server: String
|
||||||
public let version: Version
|
public let version: Version
|
||||||
public private(set) var connections: Set<String>
|
|
||||||
private let urlSession: URLSession
|
private let urlSession: URLSession
|
||||||
private let decoder = JSONDecoder()
|
private let decoder = JSONDecoder()
|
||||||
|
|
||||||
/// Only used as a transitionary app while in the oauth flow.
|
// Putting all mutable state inside an `OSAllocatedUnfairLock` makes `Client`
|
||||||
private var oauthApp: InstanceApp?
|
// provably `Sendable`. The lock is a struct, but it uses a `ManagedBuffer`
|
||||||
|
// reference type to hold its associated state.
|
||||||
private var oauthToken: OauthToken?
|
private let critical: OSAllocatedUnfairLock<Critical>
|
||||||
|
private struct Critical: Sendable {
|
||||||
|
/// Only used as a transitionary app while in the oauth flow.
|
||||||
|
var oauthApp: InstanceApp?
|
||||||
|
var oauthToken: OauthToken?
|
||||||
|
var connections: Set<String> = []
|
||||||
|
}
|
||||||
|
|
||||||
public var isAuth: Bool {
|
public var isAuth: Bool {
|
||||||
oauthToken != nil
|
critical.withLock { $0.oauthToken != nil }
|
||||||
|
}
|
||||||
|
|
||||||
|
public var connections: Set<String> {
|
||||||
|
critical.withLock { $0.connections }
|
||||||
}
|
}
|
||||||
|
|
||||||
public init(server: String, version: Version = .v1, oauthToken: OauthToken? = nil) {
|
public init(server: String, version: Version = .v1, oauthToken: OauthToken? = nil) {
|
||||||
self.server = server
|
self.server = server
|
||||||
self.version = version
|
self.version = version
|
||||||
|
self.critical = .init(initialState: Critical(oauthToken: oauthToken, connections: [server]))
|
||||||
urlSession = URLSession.shared
|
urlSession = URLSession.shared
|
||||||
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||||
self.oauthToken = oauthToken
|
|
||||||
connections = Set([server])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public func addConnections(_ connections: [String]) {
|
public func addConnections(_ connections: [String]) {
|
||||||
connections.forEach {
|
critical.withLock {
|
||||||
self.connections.insert($0)
|
$0.connections.formUnion(connections)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func hasConnection(with url: URL) -> Bool {
|
public func hasConnection(with url: URL) -> Bool {
|
||||||
guard let host = url.host else { return false }
|
guard let host = url.host else { return false }
|
||||||
if let rootHost = host.split(separator: ".", maxSplits: 1).last {
|
return critical.withLock {
|
||||||
// Sometimes the connection is with the root host instead of a subdomain
|
if let rootHost = host.split(separator: ".", maxSplits: 1).last {
|
||||||
// eg. Mastodon runs on mastdon.domain.com but the connection is with domain.com
|
// Sometimes the connection is with the root host instead of a subdomain
|
||||||
return connections.contains(host) || connections.contains(String(rootHost))
|
// eg. Mastodon runs on mastdon.domain.com but the connection is with domain.com
|
||||||
} else {
|
return $0.connections.contains(host) || $0.connections.contains(String(rootHost))
|
||||||
return connections.contains(host)
|
} else {
|
||||||
|
return $0.connections.contains(host)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,7 +105,7 @@ public final class Client: ObservableObject, Equatable, Identifiable, Hashable {
|
||||||
private func makeURLRequest(url: URL, endpoint: Endpoint, httpMethod: String) -> URLRequest {
|
private func makeURLRequest(url: URL, endpoint: Endpoint, httpMethod: String) -> URLRequest {
|
||||||
var request = URLRequest(url: url)
|
var request = URLRequest(url: url)
|
||||||
request.httpMethod = httpMethod
|
request.httpMethod = httpMethod
|
||||||
if let oauthToken {
|
if let oauthToken = critical.withLock({ $0.oauthToken }) {
|
||||||
request.setValue("Bearer \(oauthToken.accessToken)", forHTTPHeaderField: "Authorization")
|
request.setValue("Bearer \(oauthToken.accessToken)", forHTTPHeaderField: "Authorization")
|
||||||
}
|
}
|
||||||
if let json = endpoint.jsonValue {
|
if let json = endpoint.jsonValue {
|
||||||
|
@ -175,12 +193,12 @@ public final class Client: ObservableObject, Equatable, Identifiable, Hashable {
|
||||||
|
|
||||||
public func oauthURL() async throws -> URL {
|
public func oauthURL() async throws -> URL {
|
||||||
let app: InstanceApp = try await post(endpoint: Apps.registerApp)
|
let app: InstanceApp = try await post(endpoint: Apps.registerApp)
|
||||||
oauthApp = app
|
critical.withLock { $0.oauthApp = app }
|
||||||
return makeURL(endpoint: Oauth.authorize(clientId: app.clientId))
|
return makeURL(endpoint: Oauth.authorize(clientId: app.clientId))
|
||||||
}
|
}
|
||||||
|
|
||||||
public func continueOauthFlow(url: URL) async throws -> OauthToken {
|
public func continueOauthFlow(url: URL) async throws -> OauthToken {
|
||||||
guard let app = oauthApp else {
|
guard let app = critical.withLock({ $0.oauthApp }) else {
|
||||||
throw OauthError.missingApp
|
throw OauthError.missingApp
|
||||||
}
|
}
|
||||||
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
|
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
|
||||||
|
@ -191,7 +209,7 @@ public final class Client: ObservableObject, Equatable, Identifiable, Hashable {
|
||||||
let token: OauthToken = try await post(endpoint: Oauth.token(code: code,
|
let token: OauthToken = try await post(endpoint: Oauth.token(code: code,
|
||||||
clientId: app.clientId,
|
clientId: app.clientId,
|
||||||
clientSecret: app.clientSecret))
|
clientSecret: app.clientSecret))
|
||||||
oauthToken = token
|
critical.withLock { $0.oauthToken = token }
|
||||||
return token
|
return token
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -239,3 +257,5 @@ public final class Client: ObservableObject, Equatable, Identifiable, Hashable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension Client: Sendable {}
|
||||||
|
|
|
@ -16,3 +16,5 @@ public struct LinkHandler {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension LinkHandler: Sendable {}
|
||||||
|
|
|
@ -105,3 +105,9 @@ public struct OpenAIClient {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension OpenAIClient: Sendable {}
|
||||||
|
extension OpenAIClient.Prompt: Sendable {}
|
||||||
|
extension OpenAIClient.Request: Sendable {}
|
||||||
|
extension OpenAIClient.Response: Sendable {}
|
||||||
|
extension OpenAIClient.Response.Choice: Sendable {}
|
||||||
|
|
|
@ -24,6 +24,17 @@ enum StatusEditorUTTypeSupported: String, CaseIterable {
|
||||||
|
|
||||||
case uiimage = "com.apple.uikit.image"
|
case uiimage = "com.apple.uikit.image"
|
||||||
|
|
||||||
|
// Have to implement this manually here due to compiler not implicitly
|
||||||
|
// inserting `nonisolated`, which leads to a warning:
|
||||||
|
//
|
||||||
|
// Main actor-isolated static property 'allCases' cannot be used to
|
||||||
|
// satisfy nonisolated protocol requirement
|
||||||
|
//
|
||||||
|
nonisolated public static var allCases: [StatusEditorUTTypeSupported] {
|
||||||
|
[.url, .text, .plaintext, .image, .jpeg, .png, .tiff, .video,
|
||||||
|
.movie, .mp4, .gif, .gif2, .quickTimeMovie, .uiimage]
|
||||||
|
}
|
||||||
|
|
||||||
static func types() -> [UTType] {
|
static func types() -> [UTType] {
|
||||||
[.url, .text, .plainText, .image, .jpeg, .png, .tiff, .video, .mpeg4Movie, .gif, .movie, .quickTimeMovie]
|
[.url, .text, .plainText, .image, .jpeg, .png, .tiff, .video, .mpeg4Movie, .gif, .movie, .quickTimeMovie]
|
||||||
}
|
}
|
||||||
|
@ -47,6 +58,8 @@ enum StatusEditorUTTypeSupported: String, CaseIterable {
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadItemContent(item: NSItemProvider) async throws -> Any? {
|
func loadItemContent(item: NSItemProvider) async throws -> Any? {
|
||||||
|
// Many warnings here about non-sendable type `[AnyHashable: Any]?` crossing
|
||||||
|
// actor boundaries. Many Radars have been filed.
|
||||||
let result = try await item.loadItem(forTypeIdentifier: rawValue)
|
let result = try await item.loadItem(forTypeIdentifier: rawValue)
|
||||||
if isVideo, let transferable = await getVideoTransferable(item: item) {
|
if isVideo, let transferable = await getVideoTransferable(item: item) {
|
||||||
return transferable
|
return transferable
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import Combine
|
||||||
import DesignSystem
|
import DesignSystem
|
||||||
import Env
|
import Env
|
||||||
import Models
|
import Models
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import Combine
|
||||||
import Models
|
import Models
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import Combine
|
||||||
import Models
|
import Models
|
||||||
import Network
|
import Network
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import Combine
|
||||||
import Env
|
import Env
|
||||||
import Models
|
import Models
|
||||||
import NaturalLanguage
|
import NaturalLanguage
|
||||||
|
|
|
@ -17,6 +17,16 @@ struct StatusRowActionsView: View {
|
||||||
enum Action: CaseIterable {
|
enum Action: CaseIterable {
|
||||||
case respond, boost, favorite, bookmark, share
|
case respond, boost, favorite, bookmark, share
|
||||||
|
|
||||||
|
// Have to implement this manually here due to compiler not implicitly
|
||||||
|
// inserting `nonisolated`, which leads to a warning:
|
||||||
|
//
|
||||||
|
// Main actor-isolated static property 'allCases' cannot be used to
|
||||||
|
// satisfy nonisolated protocol requirement
|
||||||
|
//
|
||||||
|
nonisolated public static var allCases: [StatusRowActionsView.Action] {
|
||||||
|
[.respond, .boost, .favorite, .bookmark, .share]
|
||||||
|
}
|
||||||
|
|
||||||
func iconName(viewModel: StatusRowViewModel, privateBoost: Bool = false) -> String {
|
func iconName(viewModel: StatusRowViewModel, privateBoost: Bool = false) -> String {
|
||||||
switch self {
|
switch self {
|
||||||
case .respond:
|
case .respond:
|
||||||
|
|
|
@ -59,3 +59,9 @@ public actor TimelineCache {
|
||||||
UserDefaults.standard.array(forKey: "timeline-last-seen-\(client.id)") as? [String]
|
UserDefaults.standard.array(forKey: "timeline-last-seen-\(client.id)") as? [String]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Quiets down the warnings from this one. Bodega is nicely async so we don't
|
||||||
|
// want to just use `@preconcurrency`, but the CacheKey type is (incorrectly)
|
||||||
|
// not marked as `Sendable`---it's a value type containing two `String`
|
||||||
|
// properties.
|
||||||
|
extension Bodega.CacheKey: @unchecked Sendable {}
|
||||||
|
|
Loading…
Reference in a new issue