forked from shadowfacts/Tusker
First pass at strict concurrency checking
This commit is contained in:
parent
5cef76e494
commit
c2402303cc
|
@ -7,6 +7,7 @@
|
|||
|
||||
import UIKit
|
||||
|
||||
@MainActor
|
||||
public protocol DuckableViewController: UIViewController {
|
||||
func duckableViewControllerShouldDuck() -> DuckAttemptAction
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
public enum SearchOperatorType: String, CaseIterable, Equatable {
|
||||
public enum SearchOperatorType: String, CaseIterable, Equatable, Sendable {
|
||||
case has
|
||||
case `is`
|
||||
case language
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
public struct StatusEdit: Decodable {
|
||||
public struct StatusEdit: Decodable, Sendable {
|
||||
public let content: String
|
||||
public let spoilerText: String
|
||||
public let sensitive: Bool
|
||||
|
@ -28,10 +28,10 @@ public struct StatusEdit: Decodable {
|
|||
case emojis
|
||||
}
|
||||
|
||||
public struct Poll: Decodable {
|
||||
public struct Poll: Decodable, Sendable {
|
||||
public let options: [Option]
|
||||
|
||||
public struct Option: Decodable {
|
||||
public struct Option: Decodable, Sendable {
|
||||
public let title: String
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import Foundation
|
||||
import Pachyderm
|
||||
|
||||
public enum PostVisibility: Codable, Hashable, CaseIterable {
|
||||
public enum PostVisibility: Codable, Hashable, CaseIterable, Sendable {
|
||||
case serverDefault
|
||||
case visibility(Visibility)
|
||||
|
||||
|
@ -59,6 +59,7 @@ public enum ReplyVisibility: Codable, Hashable, CaseIterable {
|
|||
|
||||
public static var allCases: [ReplyVisibility] = [.sameAsPost] + Visibility.allCases.map { .visibility($0) }
|
||||
|
||||
@MainActor
|
||||
public func resolved(withServerDefault serverDefault: Visibility?) -> Visibility {
|
||||
switch self {
|
||||
case .sameAsPost:
|
||||
|
|
|
@ -12,12 +12,14 @@ import Combine
|
|||
|
||||
public final class Preferences: Codable, ObservableObject {
|
||||
|
||||
@MainActor
|
||||
public static var shared: Preferences = load()
|
||||
|
||||
private static var documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
|
||||
private static var appGroupDirectory = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.space.vaccor.Tusker")!
|
||||
private static var archiveURL = appGroupDirectory.appendingPathComponent("preferences").appendingPathExtension("plist")
|
||||
|
||||
@MainActor
|
||||
public static func save() {
|
||||
let encoder = PropertyListEncoder()
|
||||
let data = try? encoder.encode(shared)
|
||||
|
@ -33,6 +35,7 @@ public final class Preferences: Codable, ObservableObject {
|
|||
return Preferences()
|
||||
}
|
||||
|
||||
@MainActor
|
||||
public static func migrate(from url: URL) -> Result<Void, any Error> {
|
||||
do {
|
||||
try? FileManager.default.removeItem(at: archiveURL)
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
import UIKit
|
||||
import Pachyderm
|
||||
|
||||
public enum StatusSwipeAction: String, Codable, Hashable, CaseIterable {
|
||||
public enum StatusSwipeAction: String, Codable, Hashable, CaseIterable, Sendable {
|
||||
case reply
|
||||
case favorite
|
||||
case reblog
|
||||
|
|
|
@ -128,7 +128,7 @@ extension UIColor {
|
|||
return .systemBackground
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static let appGroupedBackground = UIColor { traitCollection in
|
||||
if case .dark = traitCollection.userInterfaceStyle,
|
||||
!Preferences.shared.pureBlackDarkMode {
|
||||
|
|
|
@ -54,6 +54,7 @@ struct SwitchAccountContainerView: View {
|
|||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private struct AccountButtonLabel: View {
|
||||
static let urlSession = URLSession(configuration: .ephemeral)
|
||||
|
||||
|
|
|
@ -2392,6 +2392,7 @@
|
|||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||
SWIFT_OPTIMIZE_OBJECT_LIFETIME = YES;
|
||||
SWIFT_STRICT_CONCURRENCY = complete;
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Dist;
|
||||
|
@ -2636,6 +2637,7 @@
|
|||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_OPTIMIZE_OBJECT_LIFETIME = YES;
|
||||
SWIFT_STRICT_CONCURRENCY = complete;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
|
@ -2694,6 +2696,7 @@
|
|||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||
SWIFT_OPTIMIZE_OBJECT_LIFETIME = YES;
|
||||
SWIFT_STRICT_CONCURRENCY = complete;
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Release;
|
||||
|
|
|
@ -18,11 +18,9 @@ private let oauthScopes = [Scope.read, .write, .follow]
|
|||
|
||||
class MastodonController: ObservableObject {
|
||||
|
||||
@MainActor
|
||||
static private(set) var all = [UserAccountInfo: MastodonController]()
|
||||
|
||||
@available(*, message: "do something less dumb")
|
||||
static var first: MastodonController { all.first!.value }
|
||||
|
||||
@MainActor
|
||||
static func getForAccount(_ account: UserAccountInfo) -> MastodonController {
|
||||
if let controller = all[account] {
|
||||
|
@ -34,10 +32,12 @@ class MastodonController: ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
static func removeForAccount(_ account: UserAccountInfo) {
|
||||
all.removeValue(forKey: account)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
static func resetAll() {
|
||||
all = [:]
|
||||
}
|
||||
|
@ -544,6 +544,7 @@ class MastodonController: ObservableObject {
|
|||
filters = (try? persistentContainer.viewContext.fetch(FilterMO.fetchRequest())) ?? []
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func createDraft(inReplyToID: String? = nil, mentioningAcct: String? = nil, text: String? = nil) -> Draft {
|
||||
var acctsToMention = [String]()
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ typealias Preferences = TuskerPreferences.Preferences
|
|||
let stateRestorationLogger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "StateRestoration")
|
||||
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "AppDelegate")
|
||||
|
||||
@UIApplicationMain
|
||||
@main
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||
|
|
|
@ -8,11 +8,15 @@
|
|||
|
||||
import UIKit
|
||||
|
||||
class ImageCache {
|
||||
final class ImageCache: @unchecked Sendable {
|
||||
|
||||
@MainActor
|
||||
static let avatars = ImageCache(name: "Avatars", memoryExpiry: .seconds(60 * 60), diskExpiry: .seconds(60 * 60 * 24 * 7), desiredSize: CGSize(width: 50, height: 50))
|
||||
@MainActor
|
||||
static let headers = ImageCache(name: "Headers", memoryExpiry: .seconds(60 * 5), diskExpiry: .seconds(60 * 60 * 24 * 7))
|
||||
@MainActor
|
||||
static let attachments = ImageCache(name: "Attachments", memoryExpiry: .seconds(60 * 2))
|
||||
@MainActor
|
||||
static let emojis = ImageCache(name: "Emojis", memoryExpiry: .seconds(60 * 5), diskExpiry: .seconds(60 * 60 * 24 * 7))
|
||||
|
||||
#if DEBUG
|
||||
|
@ -24,6 +28,7 @@ class ImageCache {
|
|||
private let cache: ImageDataCache
|
||||
private let desiredPixelSize: CGSize?
|
||||
|
||||
@MainActor
|
||||
init(name: String, memoryExpiry: CacheExpiry, diskExpiry: CacheExpiry? = nil, desiredSize: CGSize? = nil) {
|
||||
// todo: might not always want to use UIScreen.main for this, e.g. Catalyst?
|
||||
let pixelSize = desiredSize?.applying(.init(scaleX: UIScreen.main.scale, y: UIScreen.main.scale))
|
||||
|
@ -31,7 +36,7 @@ class ImageCache {
|
|||
self.cache = ImageDataCache(name: name, memoryExpiry: memoryExpiry, diskExpiry: diskExpiry, storeOriginalDataInMemory: diskExpiry == nil, desiredPixelSize: pixelSize)
|
||||
}
|
||||
|
||||
func get(_ url: URL, loadOriginal: Bool = false, completion: ((Data?, UIImage?) -> Void)?) -> Request? {
|
||||
func get(_ url: URL, loadOriginal: Bool = false, completion: (@Sendable (Data?, UIImage?) -> Void)?) -> Request? {
|
||||
if !ImageCache.disableCaching,
|
||||
let entry = try? cache.get(url.absoluteString, loadOriginal: loadOriginal) {
|
||||
completion?(entry.data, entry.image)
|
||||
|
@ -41,7 +46,7 @@ class ImageCache {
|
|||
}
|
||||
}
|
||||
|
||||
func getFromSource(_ url: URL, completion: ((Data?, UIImage?) -> Void)?) -> Request? {
|
||||
func getFromSource(_ url: URL, completion: (@Sendable (Data?, UIImage?) -> Void)?) -> Request? {
|
||||
return Task.detached(priority: .userInitiated) {
|
||||
let result = await self.fetch(url: url)
|
||||
switch result {
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
import Foundation
|
||||
|
||||
/*
|
||||
Copied from https://github.com/ChimeHQ/ConcurrencyPlus/blob/fe3b3fd5436b196d8c5211ab2cc4b69fc35524fe/Sources/ConcurrencyPlus/MainActor%2BUnsafe.swift
|
||||
Copied from https://github.com/ChimeHQ/ConcurrencyPlus/blob/daf69ed837fa04d4ba666f5a99378cf1815f0dab/Sources/ConcurrencyPlus/MainActor%2BUnsafe.swift
|
||||
|
||||
Copyright (c) 2022, Chime
|
||||
All rights reserved.
|
||||
|
@ -46,10 +46,14 @@ public extension MainActor {
|
|||
/// 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)
|
||||
@_unavailableFromAsync
|
||||
static func runUnsafely<T>(_ body: @MainActor () throws -> T) rethrows -> T {
|
||||
dispatchPrecondition(condition: .onQueue(.main))
|
||||
if #available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) {
|
||||
return try MainActor.assumeIsolated(body)
|
||||
}
|
||||
|
||||
return try body()
|
||||
}
|
||||
}
|
||||
dispatchPrecondition(condition: .onQueue(.main))
|
||||
return try withoutActuallyEscaping(body) { fn in
|
||||
try unsafeBitCast(fn, to: (() throws -> T).self)()
|
||||
}
|
||||
}}
|
||||
|
|
|
@ -11,7 +11,9 @@ import PencilKit
|
|||
|
||||
extension PKDrawing {
|
||||
|
||||
func imageInLightMode(from rect: CGRect, scale: CGFloat = UIScreen.main.scale) -> UIImage {
|
||||
@MainActor
|
||||
func imageInLightMode(from rect: CGRect, scale: CGFloat? = nil) -> UIImage {
|
||||
let scale = scale ?? UIScreen.main.scale
|
||||
let lightTraitCollection = UITraitCollection(userInterfaceStyle: .light)
|
||||
var drawingImage: UIImage!
|
||||
lightTraitCollection.performAsCurrent {
|
||||
|
|
|
@ -10,6 +10,7 @@ import SwiftUI
|
|||
import Combine
|
||||
|
||||
extension View {
|
||||
@MainActor
|
||||
@ViewBuilder
|
||||
func appGroupedListBackground(container: UIAppearanceContainer.Type, applyBackground: Bool = true) -> some View {
|
||||
if #available(iOS 16.0, *) {
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
import UIKit
|
||||
|
||||
@MainActor
|
||||
struct MenuController {
|
||||
|
||||
static let composeCommand: UIKeyCommand = {
|
||||
|
|
|
@ -12,7 +12,7 @@ import os
|
|||
// once we target iOS 16, replace uses of this with OSAllocatedUnfairLock<[Key: Value]>
|
||||
// to make the lock semantics more clear
|
||||
@available(iOS, obsoleted: 16.0)
|
||||
class MultiThreadDictionary<Key: Hashable & Sendable, Value: Sendable> {
|
||||
final class MultiThreadDictionary<Key: Hashable & Sendable, Value: Sendable>: @unchecked Sendable {
|
||||
private let lock: any Lock<[Key: Value]>
|
||||
|
||||
init() {
|
||||
|
|
|
@ -11,6 +11,7 @@ import Pachyderm
|
|||
import TuskerPreferences
|
||||
|
||||
extension StatusSwipeAction {
|
||||
@MainActor
|
||||
func createAction(status: StatusMO, container: StatusSwipeActionContainer) -> UIContextualAction? {
|
||||
switch self {
|
||||
case .reply:
|
||||
|
@ -40,6 +41,7 @@ protocol StatusSwipeActionContainer: UIView {
|
|||
func performReplyAction()
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func createReplyAction(status: StatusMO, container: StatusSwipeActionContainer) -> UIContextualAction? {
|
||||
guard container.mastodonController.loggedIn else {
|
||||
return nil
|
||||
|
@ -53,6 +55,7 @@ private func createReplyAction(status: StatusMO, container: StatusSwipeActionCon
|
|||
return action
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func createFavoriteAction(status: StatusMO, container: StatusSwipeActionContainer) -> UIContextualAction? {
|
||||
guard container.mastodonController.loggedIn else {
|
||||
return nil
|
||||
|
@ -69,6 +72,7 @@ private func createFavoriteAction(status: StatusMO, container: StatusSwipeAction
|
|||
return action
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func createReblogAction(status: StatusMO, container: StatusSwipeActionContainer) -> UIContextualAction? {
|
||||
guard container.mastodonController.loggedIn,
|
||||
container.canReblog else {
|
||||
|
@ -86,6 +90,7 @@ private func createReblogAction(status: StatusMO, container: StatusSwipeActionCo
|
|||
return action
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func createShareAction(status: StatusMO, container: StatusSwipeActionContainer) -> UIContextualAction {
|
||||
let action = UIContextualAction(style: .normal, title: "Share") { [unowned container] _, _, completion in
|
||||
MainActor.runUnsafely {
|
||||
|
@ -100,6 +105,7 @@ private func createShareAction(status: StatusMO, container: StatusSwipeActionCon
|
|||
return action
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func createBookmarkAction(status: StatusMO, container: StatusSwipeActionContainer) -> UIContextualAction? {
|
||||
guard container.mastodonController.loggedIn else {
|
||||
return nil
|
||||
|
@ -124,11 +130,10 @@ private func createBookmarkAction(status: StatusMO, container: StatusSwipeAction
|
|||
return action
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func createOpenInSafariAction(status: StatusMO, container: StatusSwipeActionContainer) -> UIContextualAction {
|
||||
let action = UIContextualAction(style: .normal, title: "Open in Safari") { [unowned container] _, _, completion in
|
||||
MainActor.runUnsafely {
|
||||
container.navigationDelegate.selected(url: status.url!, allowUniversalLinks: false)
|
||||
}
|
||||
container.navigationDelegate.selected(url: status.url!, allowUniversalLinks: false)
|
||||
completion(true)
|
||||
}
|
||||
action.image = UIImage(systemName: "safari")
|
||||
|
|
|
@ -10,10 +10,14 @@ import Foundation
|
|||
import Pachyderm
|
||||
import CoreData
|
||||
|
||||
// TODO: remove this class eventually
|
||||
class SavedDataManager: Codable {
|
||||
@MainActor
|
||||
private static var documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
|
||||
@MainActor
|
||||
private static var archiveURL = SavedDataManager.documentsDirectory.appendingPathComponent("saved_data").appendingPathExtension("plist")
|
||||
|
||||
@MainActor
|
||||
static func load() -> SavedDataManager? {
|
||||
let decoder = PropertyListDecoder()
|
||||
if let data = try? Data(contentsOf: archiveURL),
|
||||
|
@ -23,6 +27,7 @@ class SavedDataManager: Codable {
|
|||
return nil
|
||||
}
|
||||
|
||||
@MainActor
|
||||
static func destroy() throws {
|
||||
try FileManager.default.removeItem(at: archiveURL)
|
||||
}
|
||||
|
@ -39,6 +44,7 @@ class SavedDataManager: Codable {
|
|||
return s
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func save() {
|
||||
let encoder = PropertyListEncoder()
|
||||
let data = try? encoder.encode(self)
|
||||
|
@ -46,8 +52,6 @@ class SavedDataManager: Codable {
|
|||
}
|
||||
|
||||
func migrateToCoreData(accountID: String, context: NSManagedObjectContext) throws {
|
||||
var changed = false
|
||||
|
||||
if let hashtags = savedHashtags[accountID] {
|
||||
let objects: [[String: Any]] = hashtags.map {
|
||||
["url": $0.url, "name": $0.name]
|
||||
|
@ -55,7 +59,6 @@ class SavedDataManager: Codable {
|
|||
let hashtagsReq = NSBatchInsertRequest(entity: SavedHashtag.entity(), objects: objects)
|
||||
try context.execute(hashtagsReq)
|
||||
savedHashtags.removeValue(forKey: accountID)
|
||||
changed = true
|
||||
}
|
||||
|
||||
if let instances = savedInstances[accountID] {
|
||||
|
@ -65,11 +68,6 @@ class SavedDataManager: Codable {
|
|||
let instancesReq = NSBatchInsertRequest(entity: SavedInstance.entity(), objects: objects)
|
||||
try context.execute(instancesReq)
|
||||
savedInstances.removeValue(forKey: accountID)
|
||||
changed = true
|
||||
}
|
||||
|
||||
if changed {
|
||||
save()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
import UIKit
|
||||
import Sentry
|
||||
|
||||
@MainActor
|
||||
protocol TuskerSceneDelegate: UISceneDelegate {
|
||||
var window: UIWindow? { get }
|
||||
var rootViewController: TuskerRootViewController? { get }
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
import UIKit
|
||||
import PencilKit
|
||||
|
||||
@MainActor
|
||||
protocol ComposeDrawingViewControllerDelegate: AnyObject {
|
||||
func composeDrawingViewControllerClose(_ drawingController: ComposeDrawingViewController)
|
||||
func composeDrawingViewController(_ drawingController: ComposeDrawingViewController, saveDrawing drawing: PKDrawing)
|
||||
|
|
|
@ -16,6 +16,7 @@ import Pachyderm
|
|||
import CoreData
|
||||
import Duckable
|
||||
|
||||
@MainActor
|
||||
protocol ComposeHostingControllerDelegate: AnyObject {
|
||||
func dismissCompose(mode: DismissMode) -> Bool
|
||||
}
|
||||
|
|
|
@ -216,7 +216,7 @@ class ConversationViewController: UIViewController {
|
|||
state = .loading(indicator)
|
||||
|
||||
let effectiveURL: String
|
||||
class RedirectBlocker: NSObject, URLSessionTaskDelegate {
|
||||
final class RedirectBlocker: NSObject, URLSessionTaskDelegate, Sendable {
|
||||
func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: @escaping (URLRequest?) -> Void) {
|
||||
completionHandler(nil)
|
||||
}
|
||||
|
|
|
@ -166,13 +166,15 @@ class IssueReporterViewController: UIViewController {
|
|||
}
|
||||
|
||||
extension IssueReporterViewController: MFMailComposeViewControllerDelegate {
|
||||
func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
|
||||
controller.dismiss(animated: true) {
|
||||
if result == .cancelled {
|
||||
// don't dismiss ourself, to allowe the user to send the report a different way
|
||||
} else {
|
||||
self.finishedReport()
|
||||
self.doDismiss()
|
||||
nonisolated func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
|
||||
MainActor.runUnsafely {
|
||||
controller.dismiss(animated: true) {
|
||||
if result == .cancelled {
|
||||
// don't dismiss ourself, to allowe the user to send the report a different way
|
||||
} else {
|
||||
self.finishedReport()
|
||||
self.doDismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
import UIKit
|
||||
import UserAccounts
|
||||
|
||||
@MainActor
|
||||
protocol FastAccountSwitcherViewControllerDelegate: AnyObject {
|
||||
func fastAccountSwitcherAddToViewHierarchy(_ fastAccountSwitcher: FastAccountSwitcherViewController)
|
||||
/// - Parameter point: In the coordinate space of the view to which the pan gesture recognizer is attached.
|
||||
|
|
|
@ -57,12 +57,14 @@ class GalleryFallbackViewController: QLPreviewController {
|
|||
}
|
||||
|
||||
extension GalleryFallbackViewController: QLPreviewControllerDataSource {
|
||||
func numberOfPreviewItems(in controller: QLPreviewController) -> Int {
|
||||
nonisolated func numberOfPreviewItems(in controller: QLPreviewController) -> Int {
|
||||
return 1
|
||||
}
|
||||
|
||||
func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
|
||||
return previewItem
|
||||
nonisolated func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
|
||||
return MainActor.runUnsafely {
|
||||
previewItem
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ import Pachyderm
|
|||
@preconcurrency import VisionKit
|
||||
import TuskerComponents
|
||||
|
||||
@MainActor
|
||||
protocol LargeImageContentView: UIView {
|
||||
var animationImage: UIImage? { get }
|
||||
var activityItemsForSharing: [Any] { get }
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
import UIKit
|
||||
import TuskerComponents
|
||||
|
||||
@MainActor
|
||||
protocol LargeImageAnimatableViewController: UIViewController {
|
||||
var animationSourceView: UIImageView? { get }
|
||||
var largeImageController: LargeImageViewController? { get }
|
||||
|
|
|
@ -11,6 +11,7 @@ import ScreenCorners
|
|||
import UserAccounts
|
||||
import ComposeUI
|
||||
|
||||
@MainActor
|
||||
protocol AccountSwitchableViewController: TuskerRootViewController {
|
||||
var isFastAccountSwitcherActive: Bool { get }
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import UIKit
|
|||
import Pachyderm
|
||||
import Combine
|
||||
|
||||
@MainActor
|
||||
protocol MainSidebarViewControllerDelegate: AnyObject {
|
||||
func sidebarRequestPresentCompose(_ sidebarViewController: MainSidebarViewController)
|
||||
func sidebar(_ sidebarViewController: MainSidebarViewController, didSelectItem item: MainSidebarViewController.Item)
|
||||
|
|
|
@ -481,6 +481,7 @@ extension MainSplitViewController: MainSidebarViewControllerDelegate {
|
|||
}
|
||||
|
||||
fileprivate extension MainSidebarViewController.Item {
|
||||
@MainActor
|
||||
func createRootViewController(_ mastodonController: MastodonController) -> UIViewController? {
|
||||
switch self {
|
||||
case let .tab(tab):
|
||||
|
|
|
@ -216,6 +216,7 @@ extension MainTabBarViewController {
|
|||
case explore
|
||||
case myProfile
|
||||
|
||||
@MainActor
|
||||
func createViewController(_ mastodonController: MastodonController) -> UIViewController {
|
||||
switch self {
|
||||
case .timelines:
|
||||
|
|
|
@ -83,6 +83,7 @@ enum TuskerRoute {
|
|||
// case myProfile
|
||||
//}
|
||||
//
|
||||
@MainActor
|
||||
protocol NavigationControllerProtocol: UIViewController {
|
||||
var viewControllers: [UIViewController] { get set }
|
||||
var topViewController: UIViewController? { get }
|
||||
|
|
|
@ -10,6 +10,7 @@ import UIKit
|
|||
import Combine
|
||||
import Pachyderm
|
||||
|
||||
@MainActor
|
||||
protocol InstanceSelectorTableViewControllerDelegate: AnyObject {
|
||||
func didSelectInstance(url: URL)
|
||||
}
|
||||
|
|
|
@ -103,6 +103,7 @@ private struct WideCapsule: Shape {
|
|||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private protocol NavigationModePreview: UIView {
|
||||
init(startAnimation: PassthroughSubject<Void, Never>)
|
||||
}
|
||||
|
@ -118,6 +119,7 @@ private struct NavigationModeRepresentable<UIViewType: NavigationModePreview>: U
|
|||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private let timingParams = UISpringTimingParameters(mass: 1, stiffness: 70, damping: 16, initialVelocity: .zero)
|
||||
|
||||
private final class StackNavigationPreview: UIView, NavigationModePreview {
|
||||
|
|
|
@ -15,6 +15,7 @@ fileprivate let accountCell = "accountCell"
|
|||
fileprivate let statusCell = "statusCell"
|
||||
fileprivate let hashtagCell = "hashtagCell"
|
||||
|
||||
@MainActor
|
||||
protocol SearchResultsViewControllerDelegate: AnyObject {
|
||||
func selectedSearchResult(account accountID: String)
|
||||
func selectedSearchResult(hashtag: Hashtag)
|
||||
|
@ -370,7 +371,7 @@ extension SearchResultsViewController {
|
|||
}
|
||||
|
||||
extension SearchResultsViewController {
|
||||
enum Section: Hashable {
|
||||
enum Section: Hashable, Sendable {
|
||||
case tokenSuggestions(SearchOperatorType)
|
||||
case loadingIndicator
|
||||
case accounts
|
||||
|
@ -392,7 +393,7 @@ extension SearchResultsViewController {
|
|||
}
|
||||
}
|
||||
}
|
||||
enum Item: Hashable {
|
||||
enum Item: Hashable, Sendable {
|
||||
case tokenSuggestion(String)
|
||||
case loadingIndicator
|
||||
case account(String)
|
||||
|
|
|
@ -162,7 +162,7 @@ extension StatusEditHistoryViewController {
|
|||
enum Section {
|
||||
case edits
|
||||
}
|
||||
enum Item: Hashable, Equatable {
|
||||
enum Item: Hashable, Equatable, Sendable {
|
||||
case edit(StatusEdit, CollapseState, index: Int)
|
||||
case loadingIndicator
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
import UIKit
|
||||
import Pachyderm
|
||||
|
||||
@MainActor
|
||||
protocol InstanceTimelineViewControllerDelegate: AnyObject {
|
||||
func didSaveInstance(url: URL)
|
||||
func didUnsaveInstance(url: URL)
|
||||
|
|
|
@ -11,6 +11,7 @@ import Pachyderm
|
|||
import Combine
|
||||
import Sentry
|
||||
|
||||
@MainActor
|
||||
protocol TimelineViewControllerDelegate: AnyObject {
|
||||
func timelineViewController(_ timelineViewController: TimelineViewController, willShowJumpToPresentToastWith animator: UIViewPropertyAnimator?)
|
||||
func timelineViewController(_ timelineViewController: TimelineViewController, willDismissJumpToPresentToastWith animator: UIViewPropertyAnimator?)
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
@MainActor
|
||||
protocol BackgroundableViewController {
|
||||
func sceneDidEnterBackground()
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
import UIKit
|
||||
|
||||
@MainActor
|
||||
protocol CollectionViewController: UIViewController {
|
||||
var collectionView: UICollectionView! { get }
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
import UIKit
|
||||
|
||||
@MainActor
|
||||
@objc protocol RefreshableViewController {
|
||||
|
||||
func refresh()
|
||||
|
|
|
@ -339,6 +339,7 @@ private class SplitSecondaryNavigationController: EnhancedNavigationViewControll
|
|||
|
||||
}
|
||||
|
||||
@MainActor
|
||||
protocol NestedResponderProvider {
|
||||
var innerResponder: UIResponder? { get }
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
import UIKit
|
||||
|
||||
@MainActor
|
||||
protocol StateRestorableViewController: UIViewController {
|
||||
func stateRestorationActivity() -> NSUserActivity?
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
import UIKit
|
||||
|
||||
@MainActor
|
||||
protocol StatusBarTappableViewController: UIViewController {
|
||||
func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
import UIKit
|
||||
|
||||
@MainActor
|
||||
protocol TabBarScrollableViewController: UIViewController {
|
||||
func tabBarScrollToTop()
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
import UIKit
|
||||
|
||||
@MainActor
|
||||
@objc protocol TabbedPageViewController {
|
||||
func selectNextPage()
|
||||
func selectPrevPage()
|
||||
|
|
|
@ -180,6 +180,7 @@ enum PopoverSource {
|
|||
case view(WeakHolder<UIView>)
|
||||
case barButtonItem(WeakHolder<UIBarButtonItem>)
|
||||
|
||||
@MainActor
|
||||
func apply(to viewController: UIViewController) {
|
||||
if let popoverPresentationController = viewController.popoverPresentationController {
|
||||
switch self {
|
||||
|
|
|
@ -11,6 +11,7 @@ import Pachyderm
|
|||
import AVFoundation
|
||||
import TuskerComponents
|
||||
|
||||
@MainActor
|
||||
protocol AttachmentViewDelegate: AnyObject {
|
||||
func attachmentViewGallery(startingAt index: Int) -> GalleryViewController?
|
||||
func attachmentViewPresent(_ vc: UIViewController, animated: Bool)
|
||||
|
|
|
@ -12,6 +12,7 @@ import WebURLFoundationExtras
|
|||
|
||||
private let emojiRegex = try! NSRegularExpression(pattern: ":(\\w+):", options: [])
|
||||
|
||||
@MainActor
|
||||
protocol BaseEmojiLabel: AnyObject {
|
||||
var emojiIdentifier: AnyHashable? { get set }
|
||||
var emojiRequests: [ImageCache.Request] { get set }
|
||||
|
@ -42,10 +43,11 @@ extension BaseEmojiLabel {
|
|||
|
||||
// Based on https://github.com/ReticentJohn/Amaroq/blob/7c5b7088eb9fd1611dcb0f47d43bf8df093e142c/DireFloof/InlineImageHelpers.m
|
||||
let adjustedCapHeight = emojiFont.capHeight - 1
|
||||
let screenScale = UIScreen.main.scale
|
||||
@Sendable
|
||||
func emojiImageSize(_ image: UIImage) -> CGSize {
|
||||
var imageSizeMatchingFontSize = CGSize(width: image.size.width * (adjustedCapHeight / image.size.height), height: adjustedCapHeight)
|
||||
var scale: CGFloat = 1.4
|
||||
scale *= UIScreen.main.scale
|
||||
let scale = 1.4 * screenScale
|
||||
imageSizeMatchingFontSize = CGSize(width: imageSizeMatchingFontSize.width * scale, height: imageSizeMatchingFontSize.height * scale)
|
||||
return imageSizeMatchingFontSize
|
||||
}
|
||||
|
@ -88,7 +90,7 @@ extension BaseEmojiLabel {
|
|||
}
|
||||
image.prepareThumbnail(of: emojiImageSize(image)) { thumbnail in
|
||||
guard let thumbnail = thumbnail?.cgImage,
|
||||
case let rescaled = UIImage(cgImage: thumbnail, scale: UIScreen.main.scale, orientation: .up),
|
||||
case let rescaled = UIImage(cgImage: thumbnail, scale: screenScale, orientation: .up),
|
||||
let transformedImage = ImageGrayscalifier.convertIfNecessary(url: URL(emoji.url)!, image: rescaled) else {
|
||||
group.leave()
|
||||
return
|
||||
|
|
|
@ -87,7 +87,9 @@ class ContentTextView: LinkTextView, BaseEmojiLabel {
|
|||
updateLinkUnderlineStyle()
|
||||
}
|
||||
|
||||
private func updateLinkUnderlineStyle(preference: Bool = Preferences.shared.underlineTextLinks) {
|
||||
@MainActor
|
||||
private func updateLinkUnderlineStyle(preference: Bool? = nil) {
|
||||
let preference = preference ?? Preferences.shared.underlineTextLinks
|
||||
if UIAccessibility.buttonShapesEnabled || preference {
|
||||
linkTextAttributes[.underlineStyle] = NSUnderlineStyle.single.rawValue
|
||||
} else {
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
import UIKit
|
||||
import Pachyderm
|
||||
|
||||
@MainActor
|
||||
protocol ProfileHeaderViewDelegate: TuskerNavigationDelegate, MenuActionProvider {
|
||||
func profileHeader(_ headerView: ProfileHeaderView, selectedPageChangedTo newPage: ProfileViewController.Page)
|
||||
}
|
||||
|
|
|
@ -179,13 +179,16 @@ extension StatusContentContainer {
|
|||
}
|
||||
|
||||
private extension UIView {
|
||||
func observeIsHidden(_ f: @escaping () -> Void) -> NSKeyValueObservation {
|
||||
func observeIsHidden(_ f: @escaping @Sendable @MainActor () -> Void) -> NSKeyValueObservation {
|
||||
self.observe(\.isHidden) { _, _ in
|
||||
f()
|
||||
MainActor.runUnsafely {
|
||||
f()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
protocol StatusContentView: UIView {
|
||||
var statusContentFillsHorizontally: Bool { get }
|
||||
func estimateHeight(effectiveWidth: CGFloat) -> CGFloat
|
||||
|
|
|
@ -18,8 +18,8 @@ struct ToastConfiguration {
|
|||
var title: String
|
||||
var subtitle: String?
|
||||
var actionTitle: String?
|
||||
var action: ((ToastView) -> Void)?
|
||||
var longPressAction: ((ToastView) -> Void)?
|
||||
var action: (@MainActor (ToastView) -> Void)?
|
||||
var longPressAction: (@MainActor (ToastView) -> Void)?
|
||||
var edgeSpacing: CGFloat = 8
|
||||
var edge: Edge = .automatic
|
||||
var dismissOnScroll = true
|
||||
|
@ -40,7 +40,7 @@ struct ToastConfiguration {
|
|||
}
|
||||
|
||||
extension ToastConfiguration {
|
||||
init(from error: Error, with title: String, in viewController: TuskerNavigationDelegate, retryAction: ((ToastView) -> Void)?) {
|
||||
init(from error: Error, with title: String, in viewController: TuskerNavigationDelegate, retryAction: (@MainActor (ToastView) -> Void)?) {
|
||||
self.init(title: title)
|
||||
// localizedDescription is statically dispatched, so we need to call it after the downcast
|
||||
if let error = error as? Pachyderm.Client.Error {
|
||||
|
@ -71,7 +71,7 @@ extension ToastConfiguration {
|
|||
}
|
||||
}
|
||||
|
||||
init(from error: Error, with title: String, in viewController: TuskerNavigationDelegate, retryAction: @escaping @MainActor (ToastView) async -> Void) {
|
||||
init(from error: Error, with title: String, in viewController: TuskerNavigationDelegate, retryAction: @Sendable @escaping @MainActor (ToastView) async -> Void) {
|
||||
self.init(from: error, with: title, in: viewController) { toast in
|
||||
Task {
|
||||
await retryAction(toast)
|
||||
|
|
Loading…
Reference in New Issue