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