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