forked from shadowfacts/Tusker
Even more strict concurrency fixes
This commit is contained in:
parent
fc26c9fb54
commit
27d44340e8
|
@ -7,7 +7,7 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
public struct StatusSource: Decodable {
|
||||
public struct StatusSource: Decodable, Sendable {
|
||||
public let id: String
|
||||
public let text: String
|
||||
public let spoilerText: String
|
||||
|
|
|
@ -49,7 +49,7 @@ public class InstanceSelector {
|
|||
}
|
||||
|
||||
public extension InstanceSelector {
|
||||
struct Instance: Codable {
|
||||
struct Instance: Codable, Sendable {
|
||||
public let domain: String
|
||||
public let description: String
|
||||
public let proxiedThumbnailURL: URL
|
||||
|
|
|
@ -51,6 +51,7 @@ class MastodonController: ObservableObject {
|
|||
|
||||
let instanceURL: URL
|
||||
let accountInfo: UserAccountInfo?
|
||||
@MainActor
|
||||
private(set) var accountPreferences: AccountPreferences!
|
||||
|
||||
private(set) var client: Client!
|
||||
|
@ -271,17 +272,6 @@ class MastodonController: ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
func getOwnAccount(completion: (@Sendable (Result<AccountMO, any Error>) -> Void)? = nil) {
|
||||
Task.detached {
|
||||
do {
|
||||
let account = try await self.getOwnAccount()
|
||||
completion?(.success(account))
|
||||
} catch {
|
||||
completion?(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func getOwnAccount() async throws -> AccountMO {
|
||||
if let account {
|
||||
|
@ -307,15 +297,8 @@ class MastodonController: ObservableObject {
|
|||
return MainThreadBox(value: accountMO)
|
||||
}
|
||||
}
|
||||
}
|
||||
if let account = account {
|
||||
return account
|
||||
} else {
|
||||
return try await withCheckedThrowingContinuation({ continuation in
|
||||
self.getOwnAccount { result in
|
||||
continuation.resume(with: result)
|
||||
}
|
||||
})
|
||||
fetchOwnAccountTask = task
|
||||
return try await task.value.value
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -550,14 +550,19 @@ class MastodonCachePersistentStore: NSPersistentCloudKitContainer {
|
|||
}
|
||||
}
|
||||
}
|
||||
// Can't capture vars in concurrently-executing closure
|
||||
let hashtags = changedHashtags
|
||||
let instances = changedInstances
|
||||
let timelinePositions = changedTimelinePositions
|
||||
let accountPrefs = changedAccountPrefs
|
||||
DispatchQueue.main.async {
|
||||
if changedHashtags {
|
||||
if hashtags {
|
||||
NotificationCenter.default.post(name: .savedHashtagsChanged, object: nil)
|
||||
}
|
||||
if changedInstances {
|
||||
if instances {
|
||||
NotificationCenter.default.post(name: .savedInstancesChanged, object: nil)
|
||||
}
|
||||
for id in changedTimelinePositions {
|
||||
for id in timelinePositions {
|
||||
guard let timelinePosition = try? self.viewContext.existingObject(with: id) as? TimelinePosition else {
|
||||
continue
|
||||
}
|
||||
|
@ -565,7 +570,7 @@ class MastodonCachePersistentStore: NSPersistentCloudKitContainer {
|
|||
timelinePosition.changedRemotely()
|
||||
NotificationCenter.default.post(name: .timelinePositionChanged, object: timelinePosition)
|
||||
}
|
||||
if changedAccountPrefs {
|
||||
if accountPrefs {
|
||||
NotificationCenter.default.post(name: .accountPreferencesChangedRemotely, object: nil)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,6 +30,7 @@ extension StatusSwipeAction {
|
|||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
protocol StatusSwipeActionContainer: UIView {
|
||||
var mastodonController: MastodonController! { get }
|
||||
var navigationDelegate: any TuskerNavigationDelegate { get }
|
||||
|
|
|
@ -296,7 +296,7 @@ extension ConversationCollectionViewController {
|
|||
case mainStatus
|
||||
case childThread(firstStatusID: String)
|
||||
}
|
||||
enum Item: Hashable {
|
||||
enum Item: Hashable, Sendable {
|
||||
case status(id: String, node: ConversationNode, state: CollapseState, prevLink: Bool, nextLink: Bool)
|
||||
case expandThread(childThreads: [ConversationNode], inline: Bool)
|
||||
case loadingIndicator
|
||||
|
@ -306,7 +306,7 @@ extension ConversationCollectionViewController {
|
|||
case let (.status(id: a, node: _, state: _, prevLink: aPrev, nextLink: aNext), .status(id: b, node: _, state: _, prevLink: bPrev, nextLink: bNext)):
|
||||
return a == b && aPrev == bPrev && aNext == bNext
|
||||
case let (.expandThread(childThreads: a, inline: aInline), .expandThread(childThreads: b, inline: bInline)):
|
||||
return a.count == b.count && zip(a, b).allSatisfy { $0.status.id == $1.status.id } && aInline == bInline
|
||||
return a.count == b.count && zip(a, b).allSatisfy { $0.statusID == $1.statusID } && aInline == bInline
|
||||
case (.loadingIndicator, .loadingIndicator):
|
||||
return true
|
||||
default:
|
||||
|
@ -324,7 +324,7 @@ extension ConversationCollectionViewController {
|
|||
case .expandThread(childThreads: let childThreads, inline: let inline):
|
||||
hasher.combine(1)
|
||||
for thread in childThreads {
|
||||
hasher.combine(thread.status.id)
|
||||
hasher.combine(thread.statusID)
|
||||
}
|
||||
hasher.combine(inline)
|
||||
case .loadingIndicator:
|
||||
|
|
|
@ -9,15 +9,20 @@
|
|||
import Foundation
|
||||
import Pachyderm
|
||||
|
||||
@MainActor
|
||||
class ConversationNode {
|
||||
let statusID: String
|
||||
let status: StatusMO
|
||||
var children: [ConversationNode]
|
||||
|
||||
init(status: StatusMO) {
|
||||
self.statusID = status.id
|
||||
self.status = status
|
||||
self.children = []
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
struct ConversationTree {
|
||||
let ancestors: [ConversationNode]
|
||||
let mainStatus: ConversationNode
|
||||
|
|
|
@ -194,7 +194,7 @@ class ConversationViewController: UIViewController {
|
|||
if let cached = mastodonController.persistentContainer.status(for: mainStatusID) {
|
||||
// if we have a cached copy, display it immediately but still try to refresh it
|
||||
Task {
|
||||
await doLoadMainStatus()
|
||||
_ = await doLoadMainStatus()
|
||||
}
|
||||
mainStatusLoaded(cached)
|
||||
} else {
|
||||
|
|
|
@ -20,8 +20,11 @@ class FeaturedProfileCollectionViewCell: UICollectionViewCell {
|
|||
|
||||
var account: Account?
|
||||
|
||||
private var avatarRequest: ImageCache.Request?
|
||||
private var headerRequest: ImageCache.Request?
|
||||
private var accountImagesTask: Task<Void, Never>?
|
||||
|
||||
deinit {
|
||||
accountImagesTask?.cancel()
|
||||
}
|
||||
|
||||
override func awakeFromNib() {
|
||||
super.awakeFromNib()
|
||||
|
@ -62,37 +65,35 @@ class FeaturedProfileCollectionViewCell: UICollectionViewCell {
|
|||
noteTextView.setEmojis(account.emojis, identifier: account.id)
|
||||
|
||||
avatarImageView.image = nil
|
||||
if let avatar = account.avatar {
|
||||
avatarRequest = ImageCache.avatars.get(avatar) { [weak self] (_, image) in
|
||||
defer {
|
||||
self?.avatarRequest = nil
|
||||
}
|
||||
guard let self = self,
|
||||
let image = image,
|
||||
self.account?.id == account.id else {
|
||||
return
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
self.avatarImageView.image = image
|
||||
}
|
||||
headerImageView.image = nil
|
||||
|
||||
accountImagesTask?.cancel()
|
||||
accountImagesTask = Task {
|
||||
await updateImages(account: account)
|
||||
}
|
||||
}
|
||||
|
||||
headerImageView.image = nil
|
||||
if let header = account.header {
|
||||
headerRequest = ImageCache.headers.get(header) { [weak self] (_, image) in
|
||||
defer {
|
||||
self?.headerRequest = nil
|
||||
}
|
||||
guard let self = self,
|
||||
let image = image,
|
||||
self.account?.id == account.id else {
|
||||
private nonisolated func updateImages(account: Account) async {
|
||||
await withTaskGroup(of: Void.self) { group in
|
||||
group.addTask {
|
||||
guard let avatar = account.avatar,
|
||||
let image = await ImageCache.avatars.get(avatar).1 else {
|
||||
return
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
await MainActor.run {
|
||||
self.avatarImageView.image = image
|
||||
}
|
||||
}
|
||||
group.addTask {
|
||||
guard let header = account.header,
|
||||
let image = await ImageCache.headers.get(header).1 else {
|
||||
return
|
||||
}
|
||||
await MainActor.run {
|
||||
self.headerImageView.image = image
|
||||
}
|
||||
}
|
||||
await group.waitForAll()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -95,7 +95,9 @@ class SuggestedProfilesViewController: UIViewController, CollectionViewControlle
|
|||
|
||||
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||
snapshot.appendSections([.accounts])
|
||||
await dataSource.apply(snapshot)
|
||||
await MainActor.run {
|
||||
dataSource.apply(snapshot)
|
||||
}
|
||||
|
||||
do {
|
||||
let request = Client.getSuggestions(limit: 80)
|
||||
|
@ -108,7 +110,9 @@ class SuggestedProfilesViewController: UIViewController, CollectionViewControlle
|
|||
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||
snapshot.appendSections([.accounts])
|
||||
snapshot.appendItems(suggestions.map { .account($0.account.id, $0.source) })
|
||||
await dataSource.apply(snapshot)
|
||||
await MainActor.run {
|
||||
dataSource.apply(snapshot)
|
||||
}
|
||||
|
||||
state = .loaded
|
||||
} catch {
|
||||
|
|
|
@ -107,7 +107,9 @@ class TrendingHashtagsViewController: UIViewController {
|
|||
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||
snapshot.appendSections([.trendingTags])
|
||||
snapshot.appendItems([.loadingIndicator])
|
||||
await dataSource.apply(snapshot)
|
||||
await MainActor.run {
|
||||
dataSource.apply(snapshot)
|
||||
}
|
||||
|
||||
do {
|
||||
let request = self.request(offset: nil)
|
||||
|
@ -115,10 +117,14 @@ class TrendingHashtagsViewController: UIViewController {
|
|||
snapshot.deleteItems([.loadingIndicator])
|
||||
snapshot.appendItems(hashtags.map { .tag($0) })
|
||||
state = .loaded
|
||||
await dataSource.apply(snapshot)
|
||||
await MainActor.run {
|
||||
dataSource.apply(snapshot)
|
||||
}
|
||||
} catch {
|
||||
snapshot.deleteItems([.loadingIndicator])
|
||||
await dataSource.apply(snapshot)
|
||||
await MainActor.run {
|
||||
dataSource.apply(snapshot)
|
||||
}
|
||||
state = .unloaded
|
||||
|
||||
let config = ToastConfiguration(from: error, with: "Error Loading Trending Tags", in: self) { [weak self] toast in
|
||||
|
@ -140,7 +146,9 @@ class TrendingHashtagsViewController: UIViewController {
|
|||
var snapshot = origSnapshot
|
||||
if Preferences.shared.disableInfiniteScrolling {
|
||||
snapshot.appendItems([.confirmLoadMore(false)])
|
||||
await dataSource.apply(snapshot)
|
||||
await MainActor.run {
|
||||
dataSource.apply(snapshot)
|
||||
}
|
||||
|
||||
for await _ in confirmLoadMore.values {
|
||||
break
|
||||
|
@ -148,10 +156,14 @@ class TrendingHashtagsViewController: UIViewController {
|
|||
|
||||
snapshot.deleteItems([.confirmLoadMore(false)])
|
||||
snapshot.appendItems([.confirmLoadMore(true)])
|
||||
await dataSource.apply(snapshot, animatingDifferences: false)
|
||||
await MainActor.run {
|
||||
dataSource.apply(snapshot, animatingDifferences: false)
|
||||
}
|
||||
} else {
|
||||
snapshot.appendItems([.loadingIndicator])
|
||||
await dataSource.apply(snapshot)
|
||||
await MainActor.run {
|
||||
dataSource.apply(snapshot)
|
||||
}
|
||||
}
|
||||
|
||||
do {
|
||||
|
@ -159,9 +171,13 @@ class TrendingHashtagsViewController: UIViewController {
|
|||
let (hashtags, _) = try await mastodonController.run(request)
|
||||
var snapshot = origSnapshot
|
||||
snapshot.appendItems(hashtags.map { .tag($0) })
|
||||
await dataSource.apply(snapshot)
|
||||
await MainActor.run {
|
||||
dataSource.apply(snapshot)
|
||||
}
|
||||
} catch {
|
||||
await dataSource.apply(origSnapshot)
|
||||
await MainActor.run {
|
||||
dataSource.apply(origSnapshot)
|
||||
}
|
||||
|
||||
let config = ToastConfiguration(from: error, with: "Error Loading More Tags", in: self) { [weak self] toast in
|
||||
toast.dismissToast(animated: true)
|
||||
|
|
|
@ -128,7 +128,9 @@ class TrendingLinksViewController: UIViewController, CollectionViewController {
|
|||
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||
snapshot.appendSections([.loadingIndicator])
|
||||
snapshot.appendItems([.loadingIndicator])
|
||||
await dataSource.apply(snapshot)
|
||||
await MainActor.run {
|
||||
dataSource.apply(snapshot)
|
||||
}
|
||||
|
||||
do {
|
||||
let request = Client.getTrendingLinks()
|
||||
|
@ -137,9 +139,13 @@ class TrendingLinksViewController: UIViewController, CollectionViewController {
|
|||
snapshot.appendSections([.links])
|
||||
snapshot.appendItems(links.map { .link($0) })
|
||||
state = .loaded
|
||||
await dataSource.apply(snapshot)
|
||||
await MainActor.run {
|
||||
dataSource.apply(snapshot)
|
||||
}
|
||||
} catch {
|
||||
await dataSource.apply(NSDiffableDataSourceSnapshot())
|
||||
await MainActor.run {
|
||||
dataSource.apply(NSDiffableDataSourceSnapshot())
|
||||
}
|
||||
state = .unloaded
|
||||
let config = ToastConfiguration(from: error, with: "Error Loading Trending Links", in: self) { [weak self] toast in
|
||||
toast.dismissToast(animated: true)
|
||||
|
@ -161,7 +167,9 @@ class TrendingLinksViewController: UIViewController, CollectionViewController {
|
|||
if Preferences.shared.disableInfiniteScrolling {
|
||||
snapshot.appendSections([.loadingIndicator])
|
||||
snapshot.appendItems([.confirmLoadMore(false)], toSection: .loadingIndicator)
|
||||
await dataSource.apply(snapshot)
|
||||
await MainActor.run {
|
||||
dataSource.apply(snapshot)
|
||||
}
|
||||
|
||||
for await _ in confirmLoadMore.values {
|
||||
break
|
||||
|
@ -169,11 +177,15 @@ class TrendingLinksViewController: UIViewController, CollectionViewController {
|
|||
|
||||
snapshot.deleteItems([.confirmLoadMore(false)])
|
||||
snapshot.appendItems([.confirmLoadMore(true)], toSection: .loadingIndicator)
|
||||
await dataSource.apply(snapshot, animatingDifferences: false)
|
||||
await MainActor.run {
|
||||
dataSource.apply(snapshot, animatingDifferences: false)
|
||||
}
|
||||
} else {
|
||||
snapshot.appendSections([.loadingIndicator])
|
||||
snapshot.appendItems([.loadingIndicator], toSection: .loadingIndicator)
|
||||
await dataSource.apply(snapshot)
|
||||
await MainActor.run {
|
||||
dataSource.apply(snapshot)
|
||||
}
|
||||
}
|
||||
|
||||
do {
|
||||
|
@ -181,9 +193,13 @@ class TrendingLinksViewController: UIViewController, CollectionViewController {
|
|||
let (links, _) = try await mastodonController.run(request)
|
||||
var snapshot = origSnapshot
|
||||
snapshot.appendItems(links.map { .link($0) }, toSection: .links)
|
||||
await dataSource.apply(snapshot)
|
||||
await MainActor.run {
|
||||
dataSource.apply(snapshot)
|
||||
}
|
||||
} catch {
|
||||
await dataSource.apply(origSnapshot)
|
||||
await MainActor.run {
|
||||
dataSource.apply(origSnapshot)
|
||||
}
|
||||
let config = ToastConfiguration(from: error, with: "Erorr Loading More Links", in: self) { [weak self] toast in
|
||||
toast.dismissToast(animated: true)
|
||||
await self?.loadOlder()
|
||||
|
|
|
@ -126,7 +126,9 @@ class TrendingStatusesViewController: UIViewController, CollectionViewController
|
|||
statuses = try await mastodonController.run(Client.getTrendingStatuses()).0
|
||||
} catch {
|
||||
let snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||
await dataSource.apply(snapshot)
|
||||
await MainActor.run {
|
||||
dataSource.apply(snapshot)
|
||||
}
|
||||
let config = ToastConfiguration(from: error, with: "Loading Trending Posts", in: self) { toast in
|
||||
toast.dismissToast(animated: true)
|
||||
await self.loadTrendingStatuses()
|
||||
|
@ -138,7 +140,9 @@ class TrendingStatusesViewController: UIViewController, CollectionViewController
|
|||
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||
snapshot.appendSections([.statuses])
|
||||
snapshot.appendItems(statuses.map { .status(id: $0.id, collapseState: .unknown, filterState: .unknown) })
|
||||
await dataSource.apply(snapshot)
|
||||
await MainActor.run {
|
||||
dataSource.apply(snapshot)
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func handleStatusDeleted(_ notification: Foundation.Notification) {
|
||||
|
|
|
@ -238,7 +238,7 @@ class TrendsViewController: UIViewController, CollectionViewController {
|
|||
}
|
||||
isShowingTrends = shouldShowTrends
|
||||
guard shouldShowTrends else {
|
||||
await dataSource.apply(NSDiffableDataSourceSnapshot())
|
||||
await apply(snapshot: NSDiffableDataSourceSnapshot())
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -355,9 +355,9 @@ class TrendsViewController: UIViewController, CollectionViewController {
|
|||
}
|
||||
|
||||
private func apply(snapshot: NSDiffableDataSourceSnapshot<Section, Item>, animatingDifferences: Bool = true) async {
|
||||
await Task { @MainActor in
|
||||
await MainActor.run {
|
||||
self.dataSource.apply(snapshot, animatingDifferences: animatingDifferences)
|
||||
}.value
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
|
|
|
@ -49,7 +49,7 @@ class FastSwitchingAccountView: UIView {
|
|||
private let instanceLabel = UILabel()
|
||||
private let avatarImageView = UIImageView()
|
||||
|
||||
private var avatarRequest: ImageCache.Request?
|
||||
private var avatarTask: Task<Void, Never>?
|
||||
|
||||
init(account: UserAccountInfo, orientation: FastAccountSwitcherViewController.ItemOrientation) {
|
||||
self.orientation = orientation
|
||||
|
@ -69,6 +69,10 @@ class FastSwitchingAccountView: UIView {
|
|||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
avatarTask?.cancel()
|
||||
}
|
||||
|
||||
private func commonInit() {
|
||||
usernameLabel.textColor = .white
|
||||
usernameLabel.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .headline), size: 0)
|
||||
|
@ -133,17 +137,14 @@ class FastSwitchingAccountView: UIView {
|
|||
instanceLabel.text = account.instanceURL.host!
|
||||
}
|
||||
let controller = MastodonController.getForAccount(account)
|
||||
controller.getOwnAccount { [weak self] (result) in
|
||||
guard let self = self,
|
||||
case let .success(account) = result,
|
||||
let avatar = account.avatar else { return }
|
||||
self.avatarRequest = ImageCache.avatars.get(avatar) { [weak self] (_, image) in
|
||||
guard let self = self, let image = image else { return }
|
||||
DispatchQueue.main.async {
|
||||
avatarTask = Task {
|
||||
guard let account = try? await controller.getOwnAccount(),
|
||||
let avatar = account.avatar,
|
||||
let image = await ImageCache.avatars.get(avatar).1 else {
|
||||
return
|
||||
}
|
||||
self.avatarImageView.image = image
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
accessibilityLabel = "\(account.username!)@\(instanceLabel.text!)"
|
||||
}
|
||||
|
|
|
@ -105,8 +105,8 @@ class LoadingLargeImageViewController: UIViewController, LargeImageAnimatableVie
|
|||
embedChild(loadingVC!)
|
||||
imageRequest = cache.get(url, loadOriginal: true) { [weak self] (data, image) in
|
||||
guard let self = self, let image = image else { return }
|
||||
self.imageRequest = nil
|
||||
DispatchQueue.main.async {
|
||||
self.imageRequest = nil
|
||||
self.loadingVC?.removeViewAndController()
|
||||
self.createLargeImage(data: data, image: image, url: self.url)
|
||||
}
|
||||
|
|
|
@ -195,7 +195,9 @@ class EditListAccountsViewController: UIViewController, CollectionViewController
|
|||
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||
snapshot.appendSections([.accounts])
|
||||
snapshot.appendItems([.loadingIndicator])
|
||||
await dataSource.apply(snapshot)
|
||||
await MainActor.run {
|
||||
dataSource.apply(snapshot)
|
||||
}
|
||||
|
||||
do {
|
||||
let (accounts, pagination) = try await results
|
||||
|
@ -210,7 +212,9 @@ class EditListAccountsViewController: UIViewController, CollectionViewController
|
|||
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||
snapshot.appendSections([.accounts])
|
||||
snapshot.appendItems(accounts.map { .account(id: $0.id) })
|
||||
await dataSource.apply(snapshot)
|
||||
await MainActor.run {
|
||||
dataSource.apply(snapshot)
|
||||
}
|
||||
|
||||
state = .loaded
|
||||
} catch {
|
||||
|
@ -221,7 +225,9 @@ class EditListAccountsViewController: UIViewController, CollectionViewController
|
|||
self.showToast(configuration: config, animated: true)
|
||||
|
||||
state = .unloaded
|
||||
await dataSource.apply(.init())
|
||||
await MainActor.run {
|
||||
dataSource.apply(.init())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -236,7 +242,9 @@ class EditListAccountsViewController: UIViewController, CollectionViewController
|
|||
let origSnapshot = dataSource.snapshot()
|
||||
var snapshot = origSnapshot
|
||||
snapshot.appendItems([.loadingIndicator])
|
||||
await dataSource.apply(snapshot)
|
||||
await MainActor.run {
|
||||
dataSource.apply(snapshot)
|
||||
}
|
||||
|
||||
do {
|
||||
let (accounts, pagination) = try await results
|
||||
|
@ -250,7 +258,9 @@ class EditListAccountsViewController: UIViewController, CollectionViewController
|
|||
|
||||
var snapshot = origSnapshot
|
||||
snapshot.appendItems(accounts.map { .account(id: $0.id) })
|
||||
await dataSource.apply(snapshot)
|
||||
await MainActor.run {
|
||||
dataSource.apply(snapshot)
|
||||
}
|
||||
|
||||
state = .loaded
|
||||
} catch {
|
||||
|
@ -261,7 +271,9 @@ class EditListAccountsViewController: UIViewController, CollectionViewController
|
|||
self.showToast(configuration: config, animated: true)
|
||||
|
||||
state = .loaded
|
||||
await dataSource.apply(origSnapshot)
|
||||
await MainActor.run {
|
||||
dataSource.apply(origSnapshot)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -273,8 +273,11 @@ class NotificationsCollectionViewController: UIViewController, TimelineLikeColle
|
|||
}
|
||||
}
|
||||
|
||||
private func dismissNotificationsInGroup(at indexPath: IndexPath) async {
|
||||
guard case .group(let group, let collapseState, let filterState) = dataSource.itemIdentifier(for: indexPath) else {
|
||||
private nonisolated func dismissNotificationsInGroup(at indexPath: IndexPath) async {
|
||||
let item = await MainActor.run {
|
||||
dataSource.itemIdentifier(for: indexPath)
|
||||
}
|
||||
guard case .group(let group, let collapseState, let filterState) = item else {
|
||||
return
|
||||
}
|
||||
let notifications = group.notifications
|
||||
|
@ -295,7 +298,9 @@ class NotificationsCollectionViewController: UIViewController, TimelineLikeColle
|
|||
}
|
||||
})
|
||||
}
|
||||
var snapshot = dataSource.snapshot()
|
||||
var snapshot = await MainActor.run {
|
||||
dataSource.snapshot()
|
||||
}
|
||||
if dismissFailedIndices.isEmpty {
|
||||
snapshot.deleteItems([.group(group, collapseState, filterState)])
|
||||
} else if !dismissFailedIndices.isEmpty && dismissFailedIndices.count == notifications.count {
|
||||
|
|
|
@ -311,7 +311,7 @@ extension InstanceSelectorTableViewController {
|
|||
case selected
|
||||
case recommendedInstances
|
||||
}
|
||||
enum Item: Equatable, Hashable {
|
||||
enum Item: Equatable, Hashable, Sendable {
|
||||
case selected(URL, InstanceV1)
|
||||
case recommended(InstanceSelector.Instance)
|
||||
|
||||
|
|
|
@ -32,21 +32,20 @@ struct LocalAccountAvatarView: View {
|
|||
.resizable()
|
||||
.frame(width: 30, height: 30)
|
||||
.cornerRadius(preferences.avatarStyle.cornerRadiusFraction * 30)
|
||||
.onAppear(perform: self.loadImage)
|
||||
.task {
|
||||
await self.loadImage()
|
||||
}
|
||||
}
|
||||
|
||||
func loadImage() {
|
||||
func loadImage() async {
|
||||
let controller = MastodonController.getForAccount(localAccountInfo)
|
||||
controller.getOwnAccount { (result) in
|
||||
guard case let .success(account) = result,
|
||||
let avatar = account.avatar else { return }
|
||||
_ = ImageCache.avatars.get(avatar) { (_, image) in
|
||||
DispatchQueue.main.async {
|
||||
guard let account = try? await controller.getOwnAccount(),
|
||||
let avatar = account.avatar,
|
||||
let image = await ImageCache.avatars.get(avatar).1 else {
|
||||
return
|
||||
}
|
||||
self.avatarImage = image
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//struct LocalAccountAvatarView_Previews: PreviewProvider {
|
||||
|
|
|
@ -18,15 +18,14 @@ class MyProfileViewController: ProfileViewController {
|
|||
title = "My Profile"
|
||||
tabBarItem.image = UIImage(systemName: "person.fill")
|
||||
|
||||
mastodonController.getOwnAccount { (result) in
|
||||
guard case let .success(account) = result else { return }
|
||||
|
||||
DispatchQueue.main.async {
|
||||
Task {
|
||||
guard let account = try? await mastodonController.getOwnAccount() else {
|
||||
return
|
||||
}
|
||||
self.accountID = account.id
|
||||
self.setAvatarTabBarImage(account: account)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
|
|
|
@ -18,12 +18,14 @@ protocol MenuActionProvider: AnyObject {
|
|||
var toastableViewController: ToastableViewController? { get }
|
||||
}
|
||||
|
||||
@MainActor
|
||||
protocol MenuPreviewProvider: AnyObject {
|
||||
typealias PreviewProviders = (content: UIContextMenuContentPreviewProvider, actions: () -> [UIMenuElement])
|
||||
|
||||
func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders?
|
||||
}
|
||||
|
||||
@MainActor
|
||||
protocol CustomPreviewPresenting {
|
||||
func presentFromPreview(presenter: UIViewController)
|
||||
}
|
||||
|
@ -478,7 +480,7 @@ extension MenuActionProvider {
|
|||
await fetchRelationship(accountID: accountID, mastodonController: mastodonController)
|
||||
}
|
||||
Task { @MainActor in
|
||||
if let relationship = await relationship.value,
|
||||
if let relationship = await relationship.value?.value,
|
||||
let action = builder(relationship, mastodonController) {
|
||||
elementHandler([action])
|
||||
} else {
|
||||
|
@ -612,20 +614,25 @@ extension MenuActionProvider {
|
|||
|
||||
}
|
||||
|
||||
private func fetchRelationship(accountID: String, mastodonController: MastodonController) async -> RelationshipMO? {
|
||||
private func fetchRelationship(accountID: String, mastodonController: MastodonController) async -> MainThreadBox<RelationshipMO>? {
|
||||
let req = Client.getRelationships(accounts: [accountID])
|
||||
guard let (relationships, _) = try? await mastodonController.run(req),
|
||||
let r = relationships.first else {
|
||||
return nil
|
||||
}
|
||||
return await withCheckedContinuation { continuation in
|
||||
DispatchQueue.main.async {
|
||||
mastodonController.persistentContainer.addOrUpdate(relationship: r, in: mastodonController.persistentContainer.viewContext) { mo in
|
||||
continuation.resume(returning: mo)
|
||||
continuation.resume(returning: MainThreadBox(value: mo))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct MenuPreviewHelper {
|
||||
private init() {}
|
||||
|
||||
@MainActor
|
||||
static func willPerformPreviewAction(animator: UIContextMenuInteractionCommitAnimating, presenter: UIViewController) {
|
||||
if let viewController = animator.previewViewController {
|
||||
animator.preferredCommitStyle = .pop
|
||||
|
|
|
@ -54,6 +54,7 @@ enum AppShortcutItem: String, CaseIterable {
|
|||
}
|
||||
|
||||
extension AppShortcutItem {
|
||||
@MainActor
|
||||
static func createItems(for application: UIApplication) {
|
||||
application.shortcutItems = allCases.map {
|
||||
return UIApplicationShortcutItem(type: $0.rawValue, localizedTitle: $0.title, localizedSubtitle: nil, icon: $0.icon, userInfo: nil)
|
||||
|
|
|
@ -10,6 +10,7 @@ import Foundation
|
|||
import OSLog
|
||||
import Combine
|
||||
|
||||
@MainActor
|
||||
protocol TimelineLikeControllerDelegate<TimelineItem>: AnyObject {
|
||||
associatedtype TimelineItem: Sendable
|
||||
|
||||
|
@ -217,7 +218,7 @@ class TimelineLikeController<Item: Sendable> {
|
|||
}
|
||||
}
|
||||
|
||||
enum State: Equatable, CustomDebugStringConvertible {
|
||||
enum State: Equatable, CustomDebugStringConvertible, Sendable {
|
||||
case notLoadedInitial
|
||||
case idle
|
||||
case restoringInitial(LoadAttemptToken, hasAddedLoadingIndicator: Bool)
|
||||
|
@ -360,7 +361,7 @@ class TimelineLikeController<Item: Sendable> {
|
|||
}
|
||||
}
|
||||
|
||||
class LoadAttemptToken: Equatable {
|
||||
final class LoadAttemptToken: Equatable, Sendable {
|
||||
static func ==(lhs: LoadAttemptToken, rhs: LoadAttemptToken) -> Bool {
|
||||
return lhs === rhs
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue