forked from shadowfacts/Tusker
Another round of strict concurrency fixes
This commit is contained in:
parent
27d44340e8
commit
b235f0e826
|
@ -18,19 +18,23 @@ class ActionViewController: UIViewController {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
findURLFromWebPage { (components) in
|
findURLFromWebPage { (components) in
|
||||||
if let components = components {
|
DispatchQueue.main.async {
|
||||||
self.searchForURLInApp(components)
|
if let components {
|
||||||
} else {
|
self.searchForURLInApp(components)
|
||||||
self.findURLItem { (components) in
|
} else {
|
||||||
if let components = components {
|
self.findURLItem { (components) in
|
||||||
self.searchForURLInApp(components)
|
if let components {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.searchForURLInApp(components)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func findURLFromWebPage(completion: @escaping (URLComponents?) -> Void) {
|
private func findURLFromWebPage(completion: @escaping @Sendable (URLComponents?) -> Void) {
|
||||||
for item in extensionContext!.inputItems as! [NSExtensionItem] {
|
for item in extensionContext!.inputItems as! [NSExtensionItem] {
|
||||||
for provider in item.attachments! {
|
for provider in item.attachments! {
|
||||||
guard provider.hasItemConformingToTypeIdentifier(UTType.propertyList.identifier) else {
|
guard provider.hasItemConformingToTypeIdentifier(UTType.propertyList.identifier) else {
|
||||||
|
@ -54,7 +58,7 @@ class ActionViewController: UIViewController {
|
||||||
completion(nil)
|
completion(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func findURLItem(completion: @escaping (URLComponents?) -> Void) {
|
private func findURLItem(completion: @escaping @Sendable (URLComponents?) -> Void) {
|
||||||
for item in extensionContext!.inputItems as! [NSExtensionItem] {
|
for item in extensionContext!.inputItems as! [NSExtensionItem] {
|
||||||
for provider in item.attachments! {
|
for provider in item.attachments! {
|
||||||
guard provider.hasItemConformingToTypeIdentifier(UTType.url.identifier) else {
|
guard provider.hasItemConformingToTypeIdentifier(UTType.url.identifier) else {
|
||||||
|
|
|
@ -10,7 +10,7 @@ import Foundation
|
||||||
import Combine
|
import Combine
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
|
|
||||||
public class InstanceFeatures: ObservableObject {
|
public final class InstanceFeatures: ObservableObject {
|
||||||
private static let pleromaVersionRegex = try! NSRegularExpression(pattern: "\\(compatible; (pleroma|akkoma) (.*)\\)", options: .caseInsensitive)
|
private static let pleromaVersionRegex = try! NSRegularExpression(pattern: "\\(compatible; (pleroma|akkoma) (.*)\\)", options: .caseInsensitive)
|
||||||
|
|
||||||
private let _featuresUpdated = PassthroughSubject<Void, Never>()
|
private let _featuresUpdated = PassthroughSubject<Void, Never>()
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
import CryptoKit
|
import CryptoKit
|
||||||
|
|
||||||
public struct UserAccountInfo: Equatable, Hashable, Identifiable {
|
public struct UserAccountInfo: Equatable, Hashable, Identifiable, Sendable {
|
||||||
public let id: String
|
public let id: String
|
||||||
public let instanceURL: URL
|
public let instanceURL: URL
|
||||||
public let clientID: String
|
public let clientID: String
|
||||||
|
|
|
@ -12,7 +12,7 @@ import UserAccounts
|
||||||
import InstanceFeatures
|
import InstanceFeatures
|
||||||
import Combine
|
import Combine
|
||||||
|
|
||||||
class ShareMastodonContext: ComposeMastodonContext, ObservableObject {
|
final class ShareMastodonContext: ComposeMastodonContext, ObservableObject, Sendable {
|
||||||
let accountInfo: UserAccountInfo?
|
let accountInfo: UserAccountInfo?
|
||||||
let client: Client
|
let client: Client
|
||||||
let instanceFeatures: InstanceFeatures
|
let instanceFeatures: InstanceFeatures
|
||||||
|
@ -20,7 +20,9 @@ class ShareMastodonContext: ComposeMastodonContext, ObservableObject {
|
||||||
@MainActor
|
@MainActor
|
||||||
private var customEmojis: [Emoji]?
|
private var customEmojis: [Emoji]?
|
||||||
|
|
||||||
@Published var ownAccount: Account?
|
@MainActor
|
||||||
|
@Published
|
||||||
|
private(set) var ownAccount: Account?
|
||||||
|
|
||||||
init(accountInfo: UserAccountInfo) {
|
init(accountInfo: UserAccountInfo) {
|
||||||
self.accountInfo = accountInfo
|
self.accountInfo = accountInfo
|
||||||
|
|
|
@ -20,7 +20,7 @@ import OSLog
|
||||||
private let oauthScopes = [Scope.read, .write, .follow]
|
private let oauthScopes = [Scope.read, .write, .follow]
|
||||||
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "MastodonController")
|
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "MastodonController")
|
||||||
|
|
||||||
class MastodonController: ObservableObject {
|
final class MastodonController: ObservableObject, Sendable {
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
static private(set) var all = [UserAccountInfo: MastodonController]()
|
static private(set) var all = [UserAccountInfo: MastodonController]()
|
||||||
|
@ -283,7 +283,8 @@ class MastodonController: ObservableObject {
|
||||||
let account = try await run(Client.getSelfAccount()).0
|
let account = try await run(Client.getSelfAccount()).0
|
||||||
|
|
||||||
let context = persistentContainer.viewContext
|
let context = persistentContainer.viewContext
|
||||||
return await context.perform {
|
// this closure is declared separately so we can tell the compiler it's Sendable
|
||||||
|
let performBlock: @MainActor @Sendable () -> MainThreadBox<AccountMO> = {
|
||||||
let accountMO: AccountMO
|
let accountMO: AccountMO
|
||||||
if let existing = self.persistentContainer.account(for: account.id, in: context) {
|
if let existing = self.persistentContainer.account(for: account.id, in: context) {
|
||||||
accountMO = existing
|
accountMO = existing
|
||||||
|
@ -296,6 +297,8 @@ class MastodonController: ObservableObject {
|
||||||
self.account = accountMO
|
self.account = accountMO
|
||||||
return MainThreadBox(value: accountMO)
|
return MainThreadBox(value: accountMO)
|
||||||
}
|
}
|
||||||
|
// it's safe to remove the MainActor annotation, because this is the view context
|
||||||
|
return await context.perform(unsafeBitCast(performBlock, to: (@Sendable () -> MainThreadBox<AccountMO>).self))
|
||||||
}
|
}
|
||||||
fetchOwnAccountTask = task
|
fetchOwnAccountTask = task
|
||||||
return try await task.value.value
|
return try await task.value.value
|
||||||
|
@ -342,7 +345,7 @@ class MastodonController: ObservableObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func retrying<T: Sendable>(_ label: StaticString, action: () async throws -> T) async throws -> T {
|
private func retrying<T: Sendable>(_ label: StaticString, action: @Sendable () async throws -> T) async throws -> T {
|
||||||
for attempt in 0..<4 {
|
for attempt in 0..<4 {
|
||||||
do {
|
do {
|
||||||
return try await action()
|
return try await action()
|
||||||
|
@ -582,6 +585,7 @@ class MastodonController: ObservableObject {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
func createDraft(editing status: StatusMO, source: StatusSource) -> Draft {
|
func createDraft(editing status: StatusMO, source: StatusSource) -> Draft {
|
||||||
precondition(status.id == source.id)
|
precondition(status.id == source.id)
|
||||||
let draft = DraftsPersistentContainer.shared.createEditDraft(
|
let draft = DraftsPersistentContainer.shared.createEditDraft(
|
||||||
|
|
|
@ -27,6 +27,18 @@ struct ImageGrayscalifier {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func convertIfNecessary(url: URL?, image: UIImage) async -> UIImage? {
|
||||||
|
let grayscale = await MainActor.run {
|
||||||
|
Preferences.shared.grayscaleImages
|
||||||
|
}
|
||||||
|
if grayscale,
|
||||||
|
let source = image.cgImage {
|
||||||
|
return await convert(url: url, cgImage: source)
|
||||||
|
} else {
|
||||||
|
return image
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static func convert(url: URL?, image: UIImage) -> UIImage? {
|
static func convert(url: URL?, image: UIImage) -> UIImage? {
|
||||||
if let url,
|
if let url,
|
||||||
let cached = cache.object(forKey: url as NSURL) {
|
let cached = cache.object(forKey: url as NSURL) {
|
||||||
|
|
|
@ -14,7 +14,7 @@ import WebURLFoundationExtras
|
||||||
|
|
||||||
class ExploreViewController: UIViewController, UICollectionViewDelegate, CollectionViewController {
|
class ExploreViewController: UIViewController, UICollectionViewDelegate, CollectionViewController {
|
||||||
|
|
||||||
weak var mastodonController: MastodonController!
|
private let mastodonController: MastodonController
|
||||||
|
|
||||||
var collectionView: UICollectionView!
|
var collectionView: UICollectionView!
|
||||||
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
||||||
|
|
|
@ -11,7 +11,7 @@ import Pachyderm
|
||||||
|
|
||||||
class ProfileDirectoryViewController: UIViewController {
|
class ProfileDirectoryViewController: UIViewController {
|
||||||
|
|
||||||
weak var mastodonController: MastodonController!
|
private let mastodonController: MastodonController
|
||||||
|
|
||||||
private var collectionView: UICollectionView!
|
private var collectionView: UICollectionView!
|
||||||
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
||||||
|
|
|
@ -11,7 +11,7 @@ import Pachyderm
|
||||||
|
|
||||||
class SuggestedProfilesViewController: UIViewController, CollectionViewController {
|
class SuggestedProfilesViewController: UIViewController, CollectionViewController {
|
||||||
|
|
||||||
weak var mastodonController: MastodonController!
|
private let mastodonController: MastodonController
|
||||||
|
|
||||||
var collectionView: UICollectionView!
|
var collectionView: UICollectionView!
|
||||||
private var layout: MultiColumnCollectionViewLayout!
|
private var layout: MultiColumnCollectionViewLayout!
|
||||||
|
|
|
@ -13,7 +13,7 @@ import Combine
|
||||||
|
|
||||||
class TrendingHashtagsViewController: UIViewController {
|
class TrendingHashtagsViewController: UIViewController {
|
||||||
|
|
||||||
weak var mastodonController: MastodonController!
|
private let mastodonController: MastodonController
|
||||||
|
|
||||||
private var collectionView: UICollectionView!
|
private var collectionView: UICollectionView!
|
||||||
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
||||||
|
|
|
@ -14,7 +14,7 @@ import Combine
|
||||||
|
|
||||||
class TrendingLinksViewController: UIViewController, CollectionViewController {
|
class TrendingLinksViewController: UIViewController, CollectionViewController {
|
||||||
|
|
||||||
weak var mastodonController: MastodonController!
|
private let mastodonController: MastodonController
|
||||||
|
|
||||||
var collectionView: UICollectionView!
|
var collectionView: UICollectionView!
|
||||||
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
||||||
|
|
|
@ -11,7 +11,7 @@ import Pachyderm
|
||||||
|
|
||||||
class TrendingStatusesViewController: UIViewController, CollectionViewController {
|
class TrendingStatusesViewController: UIViewController, CollectionViewController {
|
||||||
|
|
||||||
weak var mastodonController: MastodonController!
|
private let mastodonController: MastodonController
|
||||||
let filterer: Filterer
|
let filterer: Filterer
|
||||||
|
|
||||||
var collectionView: UICollectionView! {
|
var collectionView: UICollectionView! {
|
||||||
|
|
|
@ -144,7 +144,9 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
|
||||||
contentViewTopConstraint,
|
contentViewTopConstraint,
|
||||||
])
|
])
|
||||||
contentViewSizeObservation = (contentView as UIView).observe(\.bounds, changeHandler: { [unowned self] _, _ in
|
contentViewSizeObservation = (contentView as UIView).observe(\.bounds, changeHandler: { [unowned self] _, _ in
|
||||||
self.centerImage()
|
MainActor.runUnsafely {
|
||||||
|
self.centerImage()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ import Combine
|
||||||
|
|
||||||
class MainSplitViewController: UISplitViewController {
|
class MainSplitViewController: UISplitViewController {
|
||||||
|
|
||||||
weak var mastodonController: MastodonController!
|
private let mastodonController: MastodonController
|
||||||
|
|
||||||
private var sidebar: MainSidebarViewController!
|
private var sidebar: MainSidebarViewController!
|
||||||
private var fastAccountSwitcher: FastAccountSwitcherViewController?
|
private var fastAccountSwitcher: FastAccountSwitcherViewController?
|
||||||
|
|
|
@ -11,7 +11,7 @@ import ComposeUI
|
||||||
|
|
||||||
class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate {
|
class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate {
|
||||||
|
|
||||||
weak var mastodonController: MastodonController!
|
private let mastodonController: MastodonController
|
||||||
|
|
||||||
private var composePlaceholder: UIViewController!
|
private var composePlaceholder: UIViewController!
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,7 @@ import Sentry
|
||||||
|
|
||||||
class NotificationsCollectionViewController: UIViewController, TimelineLikeCollectionViewController, CollectionViewController {
|
class NotificationsCollectionViewController: UIViewController, TimelineLikeCollectionViewController, CollectionViewController {
|
||||||
|
|
||||||
weak var mastodonController: MastodonController!
|
private let mastodonController: MastodonController
|
||||||
private let filterer: Filterer
|
private let filterer: Filterer
|
||||||
|
|
||||||
private let allowedTypes: [Pachyderm.Notification.Kind]
|
private let allowedTypes: [Pachyderm.Notification.Kind]
|
||||||
|
|
|
@ -27,7 +27,7 @@ class InstanceSelectorTableViewController: UITableViewController {
|
||||||
|
|
||||||
weak var delegate: InstanceSelectorTableViewControllerDelegate?
|
weak var delegate: InstanceSelectorTableViewControllerDelegate?
|
||||||
|
|
||||||
var dataSource: DataSource!
|
var dataSource: UITableViewDiffableDataSource<Section, Item>!
|
||||||
var searchController: UISearchController!
|
var searchController: UISearchController!
|
||||||
|
|
||||||
var recommendedInstances: [InstanceSelector.Instance] = []
|
var recommendedInstances: [InstanceSelector.Instance] = []
|
||||||
|
@ -73,7 +73,7 @@ class InstanceSelectorTableViewController: UITableViewController {
|
||||||
tableView.estimatedRowHeight = 120
|
tableView.estimatedRowHeight = 120
|
||||||
createActivityIndicatorHeader()
|
createActivityIndicatorHeader()
|
||||||
|
|
||||||
dataSource = DataSource(tableView: tableView, cellProvider: { (tableView, indexPath, item) -> UITableViewCell? in
|
dataSource = UITableViewDiffableDataSource(tableView: tableView, cellProvider: { (tableView, indexPath, item) -> UITableViewCell? in
|
||||||
switch item {
|
switch item {
|
||||||
case let .selected(_, instance):
|
case let .selected(_, instance):
|
||||||
let cell = tableView.dequeueReusableCell(withIdentifier: instanceCell, for: indexPath) as! InstanceTableViewCell
|
let cell = tableView.dequeueReusableCell(withIdentifier: instanceCell, for: indexPath) as! InstanceTableViewCell
|
||||||
|
@ -338,9 +338,6 @@ extension InstanceSelectorTableViewController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class DataSource: UITableViewDiffableDataSource<Section, Item> {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension InstanceSelectorTableViewController: UISearchResultsUpdating {
|
extension InstanceSelectorTableViewController: UISearchResultsUpdating {
|
||||||
|
|
|
@ -12,7 +12,7 @@ import Combine
|
||||||
|
|
||||||
class ProfileViewController: UIViewController, StateRestorableViewController {
|
class ProfileViewController: UIViewController, StateRestorableViewController {
|
||||||
|
|
||||||
weak var mastodonController: MastodonController!
|
let mastodonController: MastodonController
|
||||||
|
|
||||||
// This property is optional because MyProfileViewController may not have the user's account ID
|
// This property is optional because MyProfileViewController may not have the user's account ID
|
||||||
// when first constructed. It should never be set to nil.
|
// when first constructed. It should never be set to nil.
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
@MainActor
|
||||||
private var converter = HTMLConverter(
|
private var converter = HTMLConverter(
|
||||||
font: .preferredFont(forTextStyle: .body),
|
font: .preferredFont(forTextStyle: .body),
|
||||||
monospaceFont: UIFontMetrics.default.scaledFont(for: .monospacedSystemFont(ofSize: 17, weight: .regular)),
|
monospaceFont: UIFontMetrics.default.scaledFont(for: .monospacedSystemFont(ofSize: 17, weight: .regular)),
|
||||||
|
@ -15,6 +16,7 @@ private var converter = HTMLConverter(
|
||||||
paragraphStyle: .default
|
paragraphStyle: .default
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@MainActor
|
||||||
struct ReportStatusView: View {
|
struct ReportStatusView: View {
|
||||||
let status: StatusMO
|
let status: StatusMO
|
||||||
let mastodonController: MastodonController
|
let mastodonController: MastodonController
|
||||||
|
|
|
@ -30,7 +30,7 @@ extension SearchResultsViewControllerDelegate {
|
||||||
|
|
||||||
class SearchResultsViewController: UIViewController, CollectionViewController {
|
class SearchResultsViewController: UIViewController, CollectionViewController {
|
||||||
|
|
||||||
weak var mastodonController: MastodonController!
|
let mastodonController: MastodonController
|
||||||
|
|
||||||
weak var exploreNavigationController: UINavigationController?
|
weak var exploreNavigationController: UINavigationController?
|
||||||
weak var delegate: SearchResultsViewControllerDelegate?
|
weak var delegate: SearchResultsViewControllerDelegate?
|
||||||
|
|
|
@ -32,7 +32,7 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
|
||||||
weak var delegate: TimelineViewControllerDelegate?
|
weak var delegate: TimelineViewControllerDelegate?
|
||||||
|
|
||||||
let timeline: Timeline
|
let timeline: Timeline
|
||||||
weak var mastodonController: MastodonController!
|
let mastodonController: MastodonController
|
||||||
private let filterer: Filterer
|
private let filterer: Filterer
|
||||||
|
|
||||||
var persistsState = false
|
var persistsState = false
|
||||||
|
@ -587,7 +587,7 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
|
||||||
snapshot.appendSections([.statuses])
|
snapshot.appendSections([.statuses])
|
||||||
let items = allStatuses.map { Item.status(id: $0.id, collapseState: .unknown, filterState: .unknown) }
|
let items = allStatuses.map { Item.status(id: $0.id, collapseState: .unknown, filterState: .unknown) }
|
||||||
snapshot.appendItems(items)
|
snapshot.appendItems(items)
|
||||||
await dataSource.apply(snapshot, animatingDifferences: false)
|
await apply(snapshot, animatingDifferences: false)
|
||||||
collectionView.contentOffset = CGPoint(x: 0, y: -collectionView.adjustedContentInset.top)
|
collectionView.contentOffset = CGPoint(x: 0, y: -collectionView.adjustedContentInset.top)
|
||||||
|
|
||||||
stateRestorationLogger.debug("TimelineViewController: restored from timeline marker with last read ID: \(home.lastReadID)")
|
stateRestorationLogger.debug("TimelineViewController: restored from timeline marker with last read ID: \(home.lastReadID)")
|
||||||
|
|
|
@ -73,8 +73,8 @@ class LargeAccountDetailView: UIView {
|
||||||
if let avatar = account.avatar {
|
if let avatar = account.avatar {
|
||||||
avatarRequest = ImageCache.avatars.get(avatar) { [weak self] (_, image) in
|
avatarRequest = ImageCache.avatars.get(avatar) { [weak self] (_, image) in
|
||||||
guard let self = self, let image = image else { return }
|
guard let self = self, let image = image else { return }
|
||||||
self.avatarRequest = nil
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
|
self.avatarRequest = nil
|
||||||
self.avatarImageView.image = image
|
self.avatarImageView.image = image
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -90,8 +90,7 @@ class CachedImageView: UIImageView {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
try Task.checkCancellation()
|
try Task.checkCancellation()
|
||||||
// TODO: check that this isn't on the main thread
|
guard let transformedImage = await ImageGrayscalifier.convertIfNecessary(url: url, image: image) else {
|
||||||
guard let transformedImage = ImageGrayscalifier.convertIfNecessary(url: url, image: image) else {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
try Task.checkCancellation()
|
try Task.checkCancellation()
|
||||||
|
|
|
@ -19,8 +19,11 @@ class InstanceTableViewCell: UITableViewCell {
|
||||||
var instance: InstanceV1?
|
var instance: InstanceV1?
|
||||||
var selectorInstance: InstanceSelector.Instance?
|
var selectorInstance: InstanceSelector.Instance?
|
||||||
|
|
||||||
var thumbnailURL: URL?
|
private var thumbnailTask: Task<Void, Never>?
|
||||||
var thumbnailRequest: ImageCache.Request?
|
|
||||||
|
deinit {
|
||||||
|
thumbnailTask?.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
override func awakeFromNib() {
|
override func awakeFromNib() {
|
||||||
super.awakeFromNib()
|
super.awakeFromNib()
|
||||||
|
@ -68,20 +71,19 @@ class InstanceTableViewCell: UITableViewCell {
|
||||||
|
|
||||||
private func updateThumbnail(url: URL) {
|
private func updateThumbnail(url: URL) {
|
||||||
thumbnailImageView.image = nil
|
thumbnailImageView.image = nil
|
||||||
thumbnailURL = url
|
thumbnailTask = Task {
|
||||||
thumbnailRequest = ImageCache.attachments.get(url) { [weak self] (_, image) in
|
guard let image = await ImageCache.attachments.get(url).1,
|
||||||
guard let self = self, self.thumbnailURL == url, let image = image else { return }
|
!Task.isCancelled else {
|
||||||
self.thumbnailRequest = nil
|
return
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.thumbnailImageView.image = image
|
|
||||||
}
|
}
|
||||||
|
thumbnailImageView.image = image
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func prepareForReuse() {
|
override func prepareForReuse() {
|
||||||
super.prepareForReuse()
|
super.prepareForReuse()
|
||||||
|
|
||||||
thumbnailRequest?.cancel()
|
thumbnailTask?.cancel()
|
||||||
instance = nil
|
instance = nil
|
||||||
selectorInstance = nil
|
selectorInstance = nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,7 @@ class MultiSourceEmojiLabel: UILabel, BaseEmojiLabel {
|
||||||
|
|
||||||
var attributedStrings = pairs.map { NSAttributedString(string: $0.0) }
|
var attributedStrings = pairs.map { NSAttributedString(string: $0.0) }
|
||||||
|
|
||||||
let recombine = { [weak self] in
|
let recombine: @MainActor @Sendable () -> Void = { [weak self] in
|
||||||
if let self,
|
if let self,
|
||||||
let combiner = self.combiner {
|
let combiner = self.combiner {
|
||||||
self.attributedText = combiner(attributedStrings)
|
self.attributedText = combiner(attributedStrings)
|
||||||
|
|
|
@ -53,7 +53,9 @@ class ProfileFieldsView: UIView {
|
||||||
|
|
||||||
private func commonInit() {
|
private func commonInit() {
|
||||||
boundsObservation = observe(\.bounds, changeHandler: { [unowned self] _, _ in
|
boundsObservation = observe(\.bounds, changeHandler: { [unowned self] _, _ in
|
||||||
self.setNeedsUpdateConstraints()
|
MainActor.runUnsafely {
|
||||||
|
self.setNeedsUpdateConstraints()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
#if os(visionOS)
|
#if os(visionOS)
|
||||||
|
|
|
@ -42,8 +42,7 @@ class ProfileHeaderView: UIView {
|
||||||
|
|
||||||
var accountID: String!
|
var accountID: String!
|
||||||
|
|
||||||
private var avatarRequest: ImageCache.Request?
|
private var imagesTask: Task<Void, Never>?
|
||||||
private var headerRequest: ImageCache.Request?
|
|
||||||
|
|
||||||
private var isGrayscale = false
|
private var isGrayscale = false
|
||||||
private var followButtonMode = FollowButtonMode.follow {
|
private var followButtonMode = FollowButtonMode.follow {
|
||||||
|
@ -56,8 +55,7 @@ class ProfileHeaderView: UIView {
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
avatarRequest?.cancel()
|
imagesTask?.cancel()
|
||||||
headerRequest?.cancel()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override func awakeFromNib() {
|
override func awakeFromNib() {
|
||||||
|
@ -134,7 +132,12 @@ class ProfileHeaderView: UIView {
|
||||||
usernameLabel.text = "@\(account.acct)"
|
usernameLabel.text = "@\(account.acct)"
|
||||||
lockImageView.isHidden = !account.locked
|
lockImageView.isHidden = !account.locked
|
||||||
|
|
||||||
updateImages(account: account)
|
imagesTask?.cancel()
|
||||||
|
let avatar = account.avatar
|
||||||
|
let header = account.header
|
||||||
|
imagesTask = Task {
|
||||||
|
await updateImages(avatar: avatar, header: header)
|
||||||
|
}
|
||||||
|
|
||||||
moreButton.menu = UIMenu(title: "", image: nil, identifier: nil, options: [], children: delegate?.actionsForProfile(accountID: accountID, source: .view(moreButton), fetchRelationship: false) ?? [])
|
moreButton.menu = UIMenu(title: "", image: nil, identifier: nil, options: [], children: delegate?.actionsForProfile(accountID: accountID, source: .view(moreButton), fetchRelationship: false) ?? [])
|
||||||
|
|
||||||
|
@ -286,50 +289,41 @@ class ProfileHeaderView: UIView {
|
||||||
displayNameLabel.updateForAccountDisplayName(account: account)
|
displayNameLabel.updateForAccountDisplayName(account: account)
|
||||||
|
|
||||||
if isGrayscale != Preferences.shared.grayscaleImages {
|
if isGrayscale != Preferences.shared.grayscaleImages {
|
||||||
updateImages(account: account)
|
isGrayscale = Preferences.shared.grayscaleImages
|
||||||
|
imagesTask?.cancel()
|
||||||
|
let avatar = account.avatar
|
||||||
|
let header = account.header
|
||||||
|
imagesTask = Task {
|
||||||
|
await updateImages(avatar: avatar, header: header)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateImages(account: AccountMO) {
|
private nonisolated func updateImages(avatar: URL?, header: URL?) async {
|
||||||
isGrayscale = Preferences.shared.grayscaleImages
|
await withTaskGroup(of: Void.self) { group in
|
||||||
|
group.addTask {
|
||||||
let accountID = account.id
|
guard let avatar,
|
||||||
if let avatarURL = account.avatar {
|
let image = await ImageCache.avatars.get(avatar, loadOriginal: true).1,
|
||||||
// always load original for avatars, because ImageCache.avatars stores them scaled-down in memory
|
let transformedImage = await ImageGrayscalifier.convertIfNecessary(url: avatar, image: image),
|
||||||
avatarRequest = ImageCache.avatars.get(avatarURL, loadOriginal: true) { [weak self] (_, image) in
|
!Task.isCancelled else {
|
||||||
guard let self = self,
|
|
||||||
let image = image,
|
|
||||||
self.accountID == accountID,
|
|
||||||
let transformedImage = ImageGrayscalifier.convertIfNecessary(url: avatarURL, image: image) else {
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self?.avatarRequest = nil
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
await MainActor.run {
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.avatarRequest = nil
|
|
||||||
self.avatarImageView.image = transformedImage
|
self.avatarImageView.image = transformedImage
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
group.addTask {
|
||||||
if let header = account.header {
|
guard let header,
|
||||||
headerRequest = ImageCache.headers.get(header) { [weak self] (_, image) in
|
let image = await ImageCache.avatars.get(header, loadOriginal: true).1,
|
||||||
guard let self = self,
|
let transformedImage = await ImageGrayscalifier.convertIfNecessary(url: header, image: image),
|
||||||
let image = image,
|
!Task.isCancelled else {
|
||||||
self.accountID == accountID,
|
|
||||||
let transformedImage = ImageGrayscalifier.convertIfNecessary(url: header, image: image) else {
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self?.headerRequest = nil
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
await MainActor.run {
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.headerRequest = nil
|
|
||||||
self.headerImageView.image = transformedImage
|
self.headerImageView.image = transformedImage
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
await group.waitForAll()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue