First pass at strict concurrency checking

This commit is contained in:
Shadowfacts 2024-01-26 11:02:40 -05:00
parent 5cef76e494
commit c2402303cc
54 changed files with 122 additions and 57 deletions

View File

@ -7,6 +7,7 @@
import UIKit
@MainActor
public protocol DuckableViewController: UIViewController {
func duckableViewControllerShouldDuck() -> DuckAttemptAction

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -128,7 +128,7 @@ extension UIColor {
return .systemBackground
}
}
static let appGroupedBackground = UIColor { traitCollection in
if case .dark = traitCollection.userInterfaceStyle,
!Preferences.shared.pureBlackDarkMode {

View File

@ -54,6 +54,7 @@ struct SwitchAccountContainerView: View {
}
}
@MainActor
private struct AccountButtonLabel: View {
static let urlSession = URLSession(configuration: .ephemeral)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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, *) {

View File

@ -8,6 +8,7 @@
import UIKit
@MainActor
struct MenuController {
static let composeCommand: UIKeyCommand = {

View File

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

View File

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

View File

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

View File

@ -9,6 +9,7 @@
import UIKit
import Sentry
@MainActor
protocol TuskerSceneDelegate: UISceneDelegate {
var window: UIWindow? { get }
var rootViewController: TuskerRootViewController? { get }

View File

@ -9,6 +9,7 @@
import UIKit
import PencilKit
@MainActor
protocol ComposeDrawingViewControllerDelegate: AnyObject {
func composeDrawingViewControllerClose(_ drawingController: ComposeDrawingViewController)
func composeDrawingViewController(_ drawingController: ComposeDrawingViewController, saveDrawing drawing: PKDrawing)

View File

@ -16,6 +16,7 @@ import Pachyderm
import CoreData
import Duckable
@MainActor
protocol ComposeHostingControllerDelegate: AnyObject {
func dismissCompose(mode: DismissMode) -> Bool
}

View File

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

View File

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

View File

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

View File

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

View File

@ -12,6 +12,7 @@ import Pachyderm
@preconcurrency import VisionKit
import TuskerComponents
@MainActor
protocol LargeImageContentView: UIView {
var animationImage: UIImage? { get }
var activityItemsForSharing: [Any] { get }

View File

@ -9,6 +9,7 @@
import UIKit
import TuskerComponents
@MainActor
protocol LargeImageAnimatableViewController: UIViewController {
var animationSourceView: UIImageView? { get }
var largeImageController: LargeImageViewController? { get }

View File

@ -11,6 +11,7 @@ import ScreenCorners
import UserAccounts
import ComposeUI
@MainActor
protocol AccountSwitchableViewController: TuskerRootViewController {
var isFastAccountSwitcherActive: Bool { get }
}

View File

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

View File

@ -481,6 +481,7 @@ extension MainSplitViewController: MainSidebarViewControllerDelegate {
}
fileprivate extension MainSidebarViewController.Item {
@MainActor
func createRootViewController(_ mastodonController: MastodonController) -> UIViewController? {
switch self {
case let .tab(tab):

View File

@ -216,6 +216,7 @@ extension MainTabBarViewController {
case explore
case myProfile
@MainActor
func createViewController(_ mastodonController: MastodonController) -> UIViewController {
switch self {
case .timelines:

View File

@ -83,6 +83,7 @@ enum TuskerRoute {
// case myProfile
//}
//
@MainActor
protocol NavigationControllerProtocol: UIViewController {
var viewControllers: [UIViewController] { get set }
var topViewController: UIViewController? { get }

View File

@ -10,6 +10,7 @@ import UIKit
import Combine
import Pachyderm
@MainActor
protocol InstanceSelectorTableViewControllerDelegate: AnyObject {
func didSelectInstance(url: URL)
}

View File

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

View File

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

View File

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

View File

@ -9,6 +9,7 @@
import UIKit
import Pachyderm
@MainActor
protocol InstanceTimelineViewControllerDelegate: AnyObject {
func didSaveInstance(url: URL)
func didUnsaveInstance(url: URL)

View File

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

View File

@ -8,6 +8,7 @@
import Foundation
@MainActor
protocol BackgroundableViewController {
func sceneDidEnterBackground()
}

View File

@ -8,6 +8,7 @@
import UIKit
@MainActor
protocol CollectionViewController: UIViewController {
var collectionView: UICollectionView! { get }
}

View File

@ -8,6 +8,7 @@
import UIKit
@MainActor
@objc protocol RefreshableViewController {
func refresh()

View File

@ -339,6 +339,7 @@ private class SplitSecondaryNavigationController: EnhancedNavigationViewControll
}
@MainActor
protocol NestedResponderProvider {
var innerResponder: UIResponder? { get }
}

View File

@ -8,6 +8,7 @@
import UIKit
@MainActor
protocol StateRestorableViewController: UIViewController {
func stateRestorationActivity() -> NSUserActivity?
}

View File

@ -8,6 +8,7 @@
import UIKit
@MainActor
protocol StatusBarTappableViewController: UIViewController {
func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult
}

View File

@ -8,6 +8,7 @@
import UIKit
@MainActor
protocol TabBarScrollableViewController: UIViewController {
func tabBarScrollToTop()
}

View File

@ -8,6 +8,7 @@
import UIKit
@MainActor
@objc protocol TabbedPageViewController {
func selectNextPage()
func selectPrevPage()

View File

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

View File

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

View File

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

View File

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

View File

@ -9,6 +9,7 @@
import UIKit
import Pachyderm
@MainActor
protocol ProfileHeaderViewDelegate: TuskerNavigationDelegate, MenuActionProvider {
func profileHeader(_ headerView: ProfileHeaderView, selectedPageChangedTo newPage: ProfileViewController.Page)
}

View File

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

View File

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