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:
Jim Dovey 2023-02-26 21:39:07 -08:00 committed by GitHub
parent 9f026fbc42
commit d1209e6704
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
52 changed files with 216 additions and 28 deletions

View file

@ -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

View file

@ -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",

View file

@ -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)

View file

@ -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() }

View file

@ -1,3 +1,4 @@
import Combine
import DesignSystem import DesignSystem
import EmojiText import EmojiText
import Env import Env

View file

@ -1,3 +1,4 @@
import Combine
import Foundation import Foundation
import Models import Models
import Network import Network

View file

@ -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
{ {

View file

@ -1,3 +1,4 @@
import Combine
import Env import Env
import Models import Models
import Network import Network

View file

@ -1,3 +1,4 @@
import Combine
import UIKit import UIKit
public class SceneDelegate: NSObject, ObservableObject, UIWindowSceneDelegate { public class SceneDelegate: NSObject, ObservableObject, UIWindowSceneDelegate {

View file

@ -1,3 +1,4 @@
import Combine
import Foundation import Foundation
import Models import Models
import Network import Network

View file

@ -1,3 +1,4 @@
import Combine
import Foundation import Foundation
import Models import Models
import Network import Network

View file

@ -1,3 +1,4 @@
import Combine
import CryptoKit import CryptoKit
import Foundation import Foundation
import KeychainSwift import KeychainSwift

View file

@ -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

View file

@ -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)
} }
} }
} }

View file

@ -1,3 +1,4 @@
import Combine
import Foundation import Foundation
import Models import Models
import Network import Network

View file

@ -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 {

View file

@ -1,3 +1,4 @@
import Combine
import Models import Models
import Network import Network
import SwiftUI import SwiftUI

View file

@ -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",

View file

@ -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 {}

View file

@ -27,3 +27,5 @@ public struct AppAccount: Codable, Identifiable, Hashable {
self.oauthToken = oauthToken self.oauthToken = oauthToken
} }
} }
extension AppAccount: Sendable {}

View file

@ -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 {}

View file

@ -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 {}

View file

@ -22,3 +22,5 @@ public struct Conversation: Identifiable, Decodable, Hashable, Equatable {
.placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder()] .placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder()]
} }
} }
extension Conversation: Sendable {}

View file

@ -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 {}

View file

@ -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 {}

View file

@ -18,3 +18,5 @@ public struct Language: Identifiable, Equatable, Hashable {
) )
} }
} }
extension Language: Sendable {}

View file

@ -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 {}

View file

@ -21,3 +21,5 @@ public struct MastodonPushNotification: Codable {
case body case body
} }
} }
extension MastodonPushNotification: Sendable {}

View file

@ -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 {}

View file

@ -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 {}

View file

@ -23,3 +23,6 @@ public struct Notification: Decodable, Identifiable, Equatable {
status: .placeholder()) status: .placeholder())
} }
} }
extension Notification: Sendable {}
extension Notification.NotificationType: Sendable {}

View file

@ -48,3 +48,7 @@ public struct NullableString: Codable, Equatable, Hashable {
} }
} }
} }
extension Poll: Sendable {}
extension Poll.Option: Sendable {}
extension NullableString: Sendable {}

View file

@ -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 {}

View file

@ -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 {}

View file

@ -14,3 +14,5 @@ public struct SearchResults: Decodable {
accounts.isEmpty && statuses.isEmpty && hashtags.isEmpty accounts.isEmpty && statuses.isEmpty && hashtags.isEmpty
} }
} }
extension SearchResults: Sendable {}

View file

@ -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 {}

View file

@ -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 {}

View file

@ -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 {}

View file

@ -8,3 +8,5 @@ public struct StatusContext: Decodable {
.init(ancestors: [], descendants: []) .init(ancestors: [], descendants: [])
} }
} }
extension StatusContext: Sendable {}

View file

@ -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 {}

View file

@ -11,3 +11,5 @@ public struct StatusTranslation: Decodable {
self.provider = provider self.provider = provider
} }
} }
extension StatusTranslation: Sendable {}

View file

@ -58,3 +58,7 @@ public struct FeaturedTag: Codable, Identifiable {
} }
} }
} }
extension Tag: Sendable {}
extension Tag.History: Sendable {}
extension FeaturedTag: Sendable {}

View file

@ -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 {}

View file

@ -16,3 +16,5 @@ public struct LinkHandler {
return nil return nil
} }
} }
extension LinkHandler: Sendable {}

View file

@ -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 {}

View file

@ -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

View file

@ -1,3 +1,4 @@
import Combine
import DesignSystem import DesignSystem
import Env import Env
import Models import Models

View file

@ -1,3 +1,4 @@
import Combine
import Models import Models
import SwiftUI import SwiftUI

View file

@ -1,3 +1,4 @@
import Combine
import Models import Models
import Network import Network
import SwiftUI import SwiftUI

View file

@ -1,3 +1,4 @@
import Combine
import Env import Env
import Models import Models
import NaturalLanguage import NaturalLanguage

View file

@ -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:

View file

@ -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 {}