diff --git a/IceCubesApp/App/Report/ReportView.swift b/IceCubesApp/App/Report/ReportView.swift index dcb37d9a..46cf82bc 100644 --- a/IceCubesApp/App/Report/ReportView.swift +++ b/IceCubesApp/App/Report/ReportView.swift @@ -44,6 +44,7 @@ public struct ReportView: View { ToolbarItem(placement: .navigationBarTrailing) { Button { isSendingReport = true + let client = client Task { do { let _: ReportSent = diff --git a/IceCubesApp/App/Tabs/Settings/AboutView.swift b/IceCubesApp/App/Tabs/Settings/AboutView.swift index e75e8461..3fc144ac 100644 --- a/IceCubesApp/App/Tabs/Settings/AboutView.swift +++ b/IceCubesApp/App/Tabs/Settings/AboutView.swift @@ -140,13 +140,13 @@ struct AboutView: View { private func fetchAccounts() async { await withThrowingTaskGroup(of: Void.self) { group in group.addTask { - let viewModel = try await fetchAccountViewModel(account: "dimillian@mastodon.social") + let viewModel = try await fetchAccountViewModel(client, account: "dimillian@mastodon.social") await MainActor.run { dimillianAccount = viewModel } } group.addTask { - let viewModel = try await fetchAccountViewModel(account: "icecubesapp@mastodon.online") + let viewModel = try await fetchAccountViewModel(client, account: "icecubesapp@mastodon.online") await MainActor.run { iceCubesAccount = viewModel } @@ -154,7 +154,7 @@ struct AboutView: View { } } - private func fetchAccountViewModel(account: String) async throws -> AccountsListRowViewModel { + private func fetchAccountViewModel(_ client: Client, account: String) async throws -> AccountsListRowViewModel { let dimillianAccount: Account = try await client.get(endpoint: Accounts.lookup(name: account)) let rel: [Relationship] = try await client.get(endpoint: Accounts.relationships(ids: [dimillianAccount.id])) return .init(account: dimillianAccount, relationShip: rel.first) diff --git a/IceCubesApp/App/Tabs/Settings/AddAccountsView.swift b/IceCubesApp/App/Tabs/Settings/AddAccountsView.swift index 9fadcd51..50cdb089 100644 --- a/IceCubesApp/App/Tabs/Settings/AddAccountsView.swift +++ b/IceCubesApp/App/Tabs/Settings/AddAccountsView.swift @@ -94,6 +94,7 @@ struct AddAccountView: View { } .onAppear { isInstanceURLFieldFocused = true + let instanceName = instanceName Task { let instances = await instanceSocialClient.fetchInstances(keyword: instanceName) withAnimation { @@ -104,6 +105,8 @@ struct AddAccountView: View { } .onChange(of: instanceName) { searchingTask.cancel() + let instanceName = instanceName + let instanceSocialClient = instanceSocialClient searchingTask = Task { try? await Task.sleep(for: .seconds(0.1)) guard !Task.isCancelled else { return } @@ -129,7 +132,7 @@ struct AddAccountView: View { let instance: Instance = try await instanceDetailClient.get(endpoint: Instances.instance) withAnimation { self.instance = instance - instanceName = sanitizedName // clean up the text box, principally to chop off the username if present so it's clear that you might not wind up siging in as the thing in the box + self.instanceName = sanitizedName // clean up the text box, principally to chop off the username if present so it's clear that you might not wind up siging in as the thing in the box } instanceFetchError = nil } else { diff --git a/IceCubesApp/App/Tabs/Timeline/AddRemoteTimelineView.swift b/IceCubesApp/App/Tabs/Timeline/AddRemoteTimelineView.swift index ef1a9429..f34d40d5 100644 --- a/IceCubesApp/App/Tabs/Timeline/AddRemoteTimelineView.swift +++ b/IceCubesApp/App/Tabs/Timeline/AddRemoteTimelineView.swift @@ -77,6 +77,7 @@ struct AddRemoteTimelineView: View { .onAppear { isInstanceURLFieldFocused = true let client = InstanceSocialClient() + let instanceName = instanceName Task { instances = await client.fetchInstances(keyword: instanceName) } diff --git a/Packages/Account/Sources/Account/AccountDetailContextMenu.swift b/Packages/Account/Sources/Account/AccountDetailContextMenu.swift index d4fdd01b..37f886b6 100644 --- a/Packages/Account/Sources/Account/AccountDetailContextMenu.swift +++ b/Packages/Account/Sources/Account/AccountDetailContextMenu.swift @@ -35,6 +35,7 @@ public struct AccountDetailContextMenu: View { if viewModel.relationship?.blocking == true { Button { + let client = client Task { do { viewModel.relationship = try await client.post(endpoint: Accounts.unblock(id: account.id)) @@ -53,6 +54,7 @@ public struct AccountDetailContextMenu: View { if viewModel.relationship?.muting == true { Button { + let client = client Task { do { viewModel.relationship = try await client.post(endpoint: Accounts.unmute(id: account.id)) @@ -65,6 +67,7 @@ public struct AccountDetailContextMenu: View { Menu { ForEach(Duration.mutingDurations(), id: \.rawValue) { duration in Button(duration.description) { + let client = client Task { do { viewModel.relationship = try await client.post(endpoint: Accounts.mute(id: account.id, json: MuteData(duration: duration.rawValue))) @@ -82,6 +85,7 @@ public struct AccountDetailContextMenu: View { { if relationship.notifying { Button { + let client = client Task { do { viewModel.relationship = try await client.post(endpoint: Accounts.follow(id: account.id, @@ -94,6 +98,7 @@ public struct AccountDetailContextMenu: View { } } else { Button { + let client = client Task { do { viewModel.relationship = try await client.post(endpoint: Accounts.follow(id: account.id, @@ -107,6 +112,7 @@ public struct AccountDetailContextMenu: View { } if relationship.showingReblogs { Button { + let client = client Task { do { viewModel.relationship = try await client.post(endpoint: Accounts.follow(id: account.id, @@ -119,6 +125,7 @@ public struct AccountDetailContextMenu: View { } } else { Button { + let client = client Task { do { viewModel.relationship = try await client.post(endpoint: Accounts.follow(id: account.id, diff --git a/Packages/Account/Sources/Account/AccountDetailView.swift b/Packages/Account/Sources/Account/AccountDetailView.swift index e209d259..33cfa9cf 100644 --- a/Packages/Account/Sources/Account/AccountDetailView.swift +++ b/Packages/Account/Sources/Account/AccountDetailView.swift @@ -364,6 +364,7 @@ public struct AccountDetailView: View { .confirmationDialog("Block User", isPresented: $showBlockConfirmation) { if let account = viewModel.account { Button("account.action.block-user-\(account.username)", role: .destructive) { + let client = client Task { do { viewModel.relationship = try await client.post(endpoint: Accounts.block(id: account.id)) diff --git a/Packages/Account/Sources/Account/Filters/EditFilterView.swift b/Packages/Account/Sources/Account/Filters/EditFilterView.swift index 2539ff8c..c51e511b 100644 --- a/Packages/Account/Sources/Account/Filters/EditFilterView.swift +++ b/Packages/Account/Sources/Account/Filters/EditFilterView.swift @@ -118,7 +118,7 @@ struct EditFilterView: View { .focused($focusedField, equals: .title) .onSubmit { Task { - await saveFilter() + await saveFilter(client) } } } @@ -130,7 +130,7 @@ struct EditFilterView: View { Section { Button { Task { - await saveFilter() + await saveFilter(client) } } label: { if isSavingFilter { @@ -158,7 +158,7 @@ struct EditFilterView: View { Spacer() Button { Task { - await deleteKeyword(keyword: keyword) + await deleteKeyword(client, keyword: keyword) } } label: { Image(systemName: "trash") @@ -170,7 +170,7 @@ struct EditFilterView: View { if let index = indexes.first { let keyword = keywords[index] Task { - await deleteKeyword(keyword: keyword) + await deleteKeyword(client, keyword: keyword) } } } @@ -179,7 +179,7 @@ struct EditFilterView: View { .focused($focusedField, equals: .newKeyword) .onSubmit { Task { - await addKeyword(name: newKeyword) + await addKeyword(client, name: newKeyword) newKeyword = "" focusedField = .newKeyword } @@ -189,7 +189,7 @@ struct EditFilterView: View { Button { Task { Task { - await addKeyword(name: newKeyword) + await addKeyword(client, name: newKeyword) newKeyword = "" } } @@ -217,7 +217,7 @@ struct EditFilterView: View { contexts.append(context) } Task { - await saveFilter() + await saveFilter(client) } })) { Label(context.name, systemImage: context.iconName) @@ -242,7 +242,7 @@ struct EditFilterView: View { } .onChange(of: filterAction) { Task { - await saveFilter() + await saveFilter(client) } } .pickerStyle(.inline) @@ -256,11 +256,11 @@ struct EditFilterView: View { Button { Task { if !newKeyword.isEmpty { - await addKeyword(name: newKeyword) + await addKeyword(client, name: newKeyword) newKeyword = "" focusedField = .newKeyword } - await saveFilter() + await saveFilter(client) dismiss() } } label: { @@ -273,7 +273,7 @@ struct EditFilterView: View { .disabled(!canSave) } - private func saveFilter() async { + private func saveFilter(_ client: Client) async { do { isSavingFilter = true if let filter { @@ -288,7 +288,7 @@ struct EditFilterView: View { isSavingFilter = false } - private func addKeyword(name: String) async { + private func addKeyword(_ client: Client, name: String) async { guard let filterId = filter?.id else { return } isSavingFilter = true do { @@ -302,7 +302,7 @@ struct EditFilterView: View { isSavingFilter = false } - private func deleteKeyword(keyword: ServerFilter.Keyword) async { + private func deleteKeyword(_ client: Client, keyword: ServerFilter.Keyword) async { isSavingFilter = true do { let response = try await client.delete(endpoint: ServerFilters.removeKeyword(id: keyword.id), diff --git a/Packages/Account/Sources/Account/Filters/FiltersListView.swift b/Packages/Account/Sources/Account/Filters/FiltersListView.swift index 75628d38..a54d21d2 100644 --- a/Packages/Account/Sources/Account/Filters/FiltersListView.swift +++ b/Packages/Account/Sources/Account/Filters/FiltersListView.swift @@ -50,7 +50,7 @@ public struct FiltersListView: View { } } .onDelete { indexes in - deleteFilter(indexes: indexes) + deleteFilter(client, indexes: indexes) } } } @@ -78,6 +78,7 @@ public struct FiltersListView: View { .background(theme.secondaryBackgroundColor) #endif .task { + let client = client do { isLoading = true filters = try await client.get(endpoint: ServerFilters.filters, forceVersion: .v2) @@ -89,7 +90,7 @@ public struct FiltersListView: View { } } - private func deleteFilter(indexes: IndexSet) { + private func deleteFilter(_ client: Client, indexes: IndexSet) { if let index = indexes.first { Task { do { diff --git a/Packages/Conversations/Sources/Conversations/Detail/ConversationMessageView.swift b/Packages/Conversations/Sources/Conversations/Detail/ConversationMessageView.swift index 2621c984..83b8c275 100644 --- a/Packages/Conversations/Sources/Conversations/Detail/ConversationMessageView.swift +++ b/Packages/Conversations/Sources/Conversations/Detail/ConversationMessageView.swift @@ -108,6 +108,7 @@ struct ConversationMessageView: View { Label("status.action.copy-text", systemImage: "doc.on.doc") } Button { + let client = client Task { do { let status: Status @@ -125,7 +126,9 @@ struct ConversationMessageView: View { Label(isLiked ? "status.action.unfavorite" : "status.action.favorite", systemImage: isLiked ? "star.fill" : "star") } - Button { Task { + Button { + let client = client + Task { do { let status: Status if isBookmarked { @@ -144,6 +147,7 @@ struct ConversationMessageView: View { Divider() if message.account.id == currentAccount.account?.id { Button("status.action.delete", role: .destructive) { + let client = client Task { _ = try await client.delete(endpoint: Statuses.status(id: message.id)) } diff --git a/Packages/Env/Sources/Env/StreamWatcher.swift b/Packages/Env/Sources/Env/StreamWatcher.swift index f8690bab..33c2bace 100644 --- a/Packages/Env/Sources/Env/StreamWatcher.swift +++ b/Packages/Env/Sources/Env/StreamWatcher.swift @@ -97,8 +97,8 @@ import OSLog } let rawEvent = try decoder.decode(RawStreamEvent.self, from: data) logger.info("Stream update: \(rawEvent.event)") - if let event = rawEventToEvent(rawEvent: rawEvent) { - Task { @MainActor in + Task { @MainActor in + if let event = self.rawEventToEvent(rawEvent: rawEvent) { self.events.append(event) self.latestEvent = event if let event = event as? StreamEventNotification, event.notification.status?.visibility != .direct { @@ -114,10 +114,13 @@ import OSLog break } - receiveMessage() + Task { @MainActor in + self.receiveMessage() + } case .failure: - DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(retryDelay)) { + Task { @MainActor in + try? await Task.sleep(for: .seconds(self.retryDelay)) self.retryDelay += 30 self.stopWatching() self.connect() diff --git a/Packages/Lists/Sources/Lists/Create/ListCreateView.swift b/Packages/Lists/Sources/Lists/Create/ListCreateView.swift index 344263da..ad2eed09 100644 --- a/Packages/Lists/Sources/Lists/Create/ListCreateView.swift +++ b/Packages/Lists/Sources/Lists/Create/ListCreateView.swift @@ -42,6 +42,7 @@ public struct ListCreateView: View { CancelToolbarItem() ToolbarItem { Button { + let client = client Task { isSaving = true let _: Models.List = try await client.post(endpoint: Lists.createList(title: title, diff --git a/Packages/Models/Sources/Models/Stream/StreamEvent.swift b/Packages/Models/Sources/Models/Stream/StreamEvent.swift index 1ba79d7d..c2a4dd6e 100644 --- a/Packages/Models/Sources/Models/Stream/StreamEvent.swift +++ b/Packages/Models/Sources/Models/Stream/StreamEvent.swift @@ -1,17 +1,17 @@ import Foundation -public struct RawStreamEvent: Decodable { +public struct RawStreamEvent: Decodable, Sendable{ public let event: String public let stream: [String] public let payload: String } -public protocol StreamEvent: Identifiable { +public protocol StreamEvent: Identifiable, Sendable { var date: Date { get } var id: String { get } } -public struct StreamEventUpdate: StreamEvent { +public struct StreamEventUpdate: StreamEvent, Sendable { public let date = Date() public var id: String { status.id } public let status: Status @@ -20,7 +20,7 @@ public struct StreamEventUpdate: StreamEvent { } } -public struct StreamEventStatusUpdate: StreamEvent { +public struct StreamEventStatusUpdate: StreamEvent, Sendable { public let date = Date() public var id: String { status.id + (status.editedAt?.asDate.description ?? "") } public let status: Status @@ -29,7 +29,7 @@ public struct StreamEventStatusUpdate: StreamEvent { } } -public struct StreamEventDelete: StreamEvent { +public struct StreamEventDelete: StreamEvent, Sendable { public let date = Date() public var id: String { status + date.description } public let status: String @@ -38,7 +38,7 @@ public struct StreamEventDelete: StreamEvent { } } -public struct StreamEventNotification: StreamEvent { +public struct StreamEventNotification: StreamEvent, Sendable { public let date = Date() public var id: String { notification.id } public let notification: Notification @@ -47,7 +47,7 @@ public struct StreamEventNotification: StreamEvent { } } -public struct StreamEventConversation: StreamEvent { +public struct StreamEventConversation: StreamEvent, Sendable { public let date = Date() public var id: String { conversation.id } public let conversation: Conversation diff --git a/Packages/Notifications/Sources/Notifications/NotificationsListView.swift b/Packages/Notifications/Sources/Notifications/NotificationsListView.swift index 42b878b0..65d2f3a9 100644 --- a/Packages/Notifications/Sources/Notifications/NotificationsListView.swift +++ b/Packages/Notifications/Sources/Notifications/NotificationsListView.swift @@ -65,7 +65,7 @@ public struct NotificationsListView: View { Button { viewModel.selectedType = nil Task { - await viewModel.fetchNotifications() + await viewModel.fetchNotifications(viewModel.selectedType) } } label: { Label("notifications.navigation-title", systemImage: "bell.fill") @@ -75,7 +75,7 @@ public struct NotificationsListView: View { Button { viewModel.selectedType = type Task { - await viewModel.fetchNotifications() + await viewModel.fetchNotifications(viewModel.selectedType) } } label: { Label { @@ -116,27 +116,27 @@ public struct NotificationsListView: View { viewModel.loadSelectedType() } Task { - await viewModel.fetchNotifications() + await viewModel.fetchNotifications(viewModel.selectedType) await viewModel.fetchPolicy() } } .refreshable { SoundEffectManager.shared.playSound(.pull) HapticManager.shared.fireHaptic(.dataRefresh(intensity: 0.3)) - await viewModel.fetchNotifications() + await viewModel.fetchNotifications(viewModel.selectedType) HapticManager.shared.fireHaptic(.dataRefresh(intensity: 0.7)) SoundEffectManager.shared.playSound(.refresh) } .onChange(of: watcher.latestEvent?.id) { if let latestEvent = watcher.latestEvent { - viewModel.handleEvent(event: latestEvent) + viewModel.handleEvent(selectedType: viewModel.selectedType, event: latestEvent) } } .onChange(of: scenePhase) { _, newValue in switch newValue { case .active: Task { - await viewModel.fetchNotifications() + await viewModel.fetchNotifications(viewModel.selectedType) } default: break @@ -202,7 +202,7 @@ public struct NotificationsListView: View { EmptyView() case .hasNextPage: NextPageView { - try await viewModel.fetchNextPage() + try await viewModel.fetchNextPage(viewModel.selectedType) } .listRowInsets(.init(top: .layoutPadding, leading: .layoutPadding + 4, @@ -219,7 +219,7 @@ public struct NotificationsListView: View { message: "notifications.error.message", buttonTitle: "action.retry") { - await viewModel.fetchNotifications() + await viewModel.fetchNotifications(viewModel.selectedType) } #if !os(visionOS) .listRowBackground(theme.primaryBackgroundColor) diff --git a/Packages/Notifications/Sources/Notifications/NotificationsPolicyView.swift b/Packages/Notifications/Sources/Notifications/NotificationsPolicyView.swift index fc637edb..ba82ba9b 100644 --- a/Packages/Notifications/Sources/Notifications/NotificationsPolicyView.swift +++ b/Packages/Notifications/Sources/Notifications/NotificationsPolicyView.swift @@ -20,28 +20,28 @@ struct NotificationsPolicyView: View { Toggle(isOn: .init(get: { policy?.filterNotFollowing == true }, set: { newValue in policy?.filterNotFollowing = newValue - Task { await updatePolicy() } + Task { await updatePolicy(client) } }), label: { Text("notifications.content-filter.peopleYouDontFollow") }) Toggle(isOn: .init(get: { policy?.filterNotFollowers == true }, set: { newValue in policy?.filterNotFollowers = newValue - Task { await updatePolicy() } + Task { await updatePolicy(client) } }), label: { Text("notifications.content-filter.peopleNotFollowingYou") }) Toggle(isOn: .init(get: { policy?.filterNewAccounts == true }, set: { newValue in policy?.filterNewAccounts = newValue - Task { await updatePolicy() } + Task { await updatePolicy(client) } }), label: { Text("notifications.content-filter.newAccounts") }) Toggle(isOn: .init(get: { policy?.filterPrivateMentions == true }, set: { newValue in policy?.filterPrivateMentions = newValue - Task { await updatePolicy() } + Task { await updatePolicy(client) } }), label: { Text("notifications.content-filter.privateMentions") }) @@ -57,14 +57,14 @@ struct NotificationsPolicyView: View { .toolbar { CloseToolbarItem() } .disabled(policy == nil || isUpdating) .task { - await getPolicy() + await getPolicy(client) } } .presentationDetents([.medium]) .presentationBackground(.thinMaterial) } - private func getPolicy() async { + private func getPolicy(_ client: Client) async { defer { isUpdating = false } @@ -76,7 +76,7 @@ struct NotificationsPolicyView: View { } } - private func updatePolicy() async { + private func updatePolicy(_ client: Client) async { if let policy { defer { isUpdating = false diff --git a/Packages/Notifications/Sources/Notifications/NotificationsViewModel.swift b/Packages/Notifications/Sources/Notifications/NotificationsViewModel.swift index 5197e413..33eff9d7 100644 --- a/Packages/Notifications/Sources/Notifications/NotificationsViewModel.swift +++ b/Packages/Notifications/Sources/Notifications/NotificationsViewModel.swift @@ -78,7 +78,7 @@ import SwiftUI private var consolidatedNotifications: [ConsolidatedNotification] = [] - func fetchNotifications() async { + func fetchNotifications(_ selectedType: Models.Notification.NotificationType?) async { guard let client, let currentAccount else { return } do { var nextPageState: State.PagingState = .hasNextPage @@ -150,7 +150,7 @@ import SwiftUI return allNotifications } - func fetchNextPage() async throws { + func fetchNextPage(_ selectedType: Models.Notification.NotificationType?) async throws { guard let client else { return } guard let lastId = consolidatedNotifications.last?.notificationIds.last else { return } let newNotifications: [Models.Notification] @@ -185,7 +185,7 @@ import SwiftUI policy = try? await client?.get(endpoint: Notifications.policy) } - func handleEvent(event: any StreamEvent) { + func handleEvent(selectedType: Models.Notification.NotificationType?, event: any StreamEvent) { Task { // Check if the event is a notification, // if it is not already in the list, diff --git a/Packages/Notifications/Sources/Notifications/Requests/NotificationsRequestsListView.swift b/Packages/Notifications/Sources/Notifications/Requests/NotificationsRequestsListView.swift index 65ddb46c..3ef60f8a 100644 --- a/Packages/Notifications/Sources/Notifications/Requests/NotificationsRequestsListView.swift +++ b/Packages/Notifications/Sources/Notifications/Requests/NotificationsRequestsListView.swift @@ -32,7 +32,7 @@ public struct NotificationsRequestsListView: View { message: "notifications.error.message", buttonTitle: "action.retry") { - await fetchRequests() + await fetchRequests(client) } #if !os(visionOS) .listRowBackground(theme.primaryBackgroundColor) @@ -43,13 +43,13 @@ public struct NotificationsRequestsListView: View { NotificationsRequestsRowView(request: request) .swipeActions { Button { - Task { await acceptRequest(request) } + Task { await acceptRequest(client, request) } } label: { Label("account.follow-request.accept", systemImage: "checkmark") } Button { - Task { await dismissRequest(request) } + Task { await dismissRequest(client, request) } } label: { Label("account.follow-request.reject", systemImage: "xmark") } @@ -66,14 +66,14 @@ public struct NotificationsRequestsListView: View { .navigationTitle("notifications.content-filter.requests.title") .navigationBarTitleDisplayMode(.inline) .task { - await fetchRequests() + await fetchRequests(client) } .refreshable { - await fetchRequests() + await fetchRequests(client) } } - private func fetchRequests() async { + private func fetchRequests(_ client: Client) async { do { viewState = try .requests(await client.get(endpoint: Notifications.requests)) } catch { @@ -81,13 +81,13 @@ public struct NotificationsRequestsListView: View { } } - private func acceptRequest(_ request: NotificationsRequest) async { + private func acceptRequest(_ client: Client, _ request: NotificationsRequest) async { _ = try? await client.post(endpoint: Notifications.acceptRequest(id: request.id)) - await fetchRequests() + await fetchRequests(client) } - private func dismissRequest(_ request: NotificationsRequest) async { + private func dismissRequest(_ client: Client, _ request: NotificationsRequest) async { _ = try? await client.post(endpoint: Notifications.dismissRequest(id: request.id)) - await fetchRequests() + await fetchRequests(client) } } diff --git a/Packages/StatusKit/Sources/StatusKit/Row/StatusRowView.swift b/Packages/StatusKit/Sources/StatusKit/Row/StatusRowView.swift index 7b0c2c7b..eb895ad0 100644 --- a/Packages/StatusKit/Sources/StatusKit/Row/StatusRowView.swift +++ b/Packages/StatusKit/Sources/StatusKit/Row/StatusRowView.swift @@ -199,6 +199,7 @@ public struct StatusRowView: View { isPresented: $isBlockConfirmationPresented) { Button("account.action.block", role: .destructive) { + let client = client Task { do { let operationAccount = viewModel.status.reblog?.account ?? viewModel.status.account