diff --git a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionScene.swift b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionScene.swift index 42f2f3225..acc01f73a 100644 --- a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionScene.swift +++ b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionScene.swift @@ -80,13 +80,14 @@ final class ShareExtensionViewModel: ObservableObject { .store(in: &subscriptions) // Using viewerPublisher to get fast feedback for auth/network errors - services.dataService.viewerPublisher() - .sink { [weak self] completion in - guard case let .failure(error) = completion else { return } - self?.debugText = "saveArticleError: \(error)" - self?.status = .failed(error: .unknown(description: "")) - } receiveValue: { _ in } - .store(in: &subscriptions) + Task { + do { + _ = try await services.dataService.fetchViewer() + } catch { + debugText = "saveArticleError: \(error)" + status = .failed(error: .unknown(description: "")) + } + } } } diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift index 4ee67a777..ed61a4341 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift @@ -5,7 +5,7 @@ import SwiftUI import Utils import Views -final class HomeFeedViewModel: ObservableObject { +@MainActor final class HomeFeedViewModel: ObservableObject { var currentDetailViewModel: LinkItemDetailViewModel? /// Track progress updates to be committed when user navigates back to grid view @@ -43,7 +43,7 @@ final class HomeFeedViewModel: ObservableObject { // Check if user has scrolled to the last five items in the list if let itemIndex = itemIndex, itemIndex > thresholdIndex, items.count < thresholdIndex + 10 { - loadItems(dataService: dataService, isRefresh: false) + Task { await loadItems(dataService: dataService, isRefresh: false) } } } @@ -61,12 +61,9 @@ final class HomeFeedViewModel: ObservableObject { isLoading = true // Cache the viewer + if dataService.currentViewer == nil { - dataService.viewerPublisher().sink( - receiveCompletion: { _ in }, - receiveValue: { _ in } - ) - .store(in: &subscriptions) + Task { _ = try? await dataService.fetchViewer() } } dataService.libraryItemsPublisher( diff --git a/apple/OmnivoreKit/Sources/App/Views/LinkItemDetailView.swift b/apple/OmnivoreKit/Sources/App/Views/LinkItemDetailView.swift index d267389e3..6ae0f7ad9 100644 --- a/apple/OmnivoreKit/Sources/App/Views/LinkItemDetailView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/LinkItemDetailView.swift @@ -9,7 +9,7 @@ enum PDFProvider { static var pdfViewerProvider: ((URL, FeedItem) -> AnyView)? } -final class LinkItemDetailViewModel: ObservableObject { +@MainActor final class LinkItemDetailViewModel: ObservableObject { let homeFeedViewModel: HomeFeedViewModel @Published var item: FeedItem @Published var webAppWrapperViewModel: WebAppWrapperViewModel? @@ -45,31 +45,22 @@ final class LinkItemDetailViewModel: ObservableObject { .store(in: &subscriptions) } - func loadWebAppWrapper(dataService: DataService, rawAuthCookie: String?) { - // Attempt to get `Viewer` from DataService - if let currentViewer = dataService.currentViewer { + func loadWebAppWrapper(dataService: DataService, rawAuthCookie: String?) async { + let viewer: Viewer? = await { + if let currentViewer = dataService.currentViewer { + return currentViewer + } + + return try? await dataService.fetchViewer() + }() + + if let viewer = viewer { createWebAppWrapperViewModel( - username: currentViewer.username, + username: viewer.username, dataService: dataService, rawAuthCookie: rawAuthCookie ) - return } - - dataService.viewerPublisher().sink( - receiveCompletion: { completion in - guard case let .failure(error) = completion else { return } - print(error) - }, - receiveValue: { [weak self] viewer in - self?.createWebAppWrapperViewModel( - username: viewer.username, - dataService: dataService, - rawAuthCookie: rawAuthCookie - ) - } - ) - .store(in: &subscriptions) } private func createWebAppWrapperViewModel(username: String, dataService: DataService, rawAuthCookie: String?) { @@ -265,8 +256,8 @@ struct LinkItemDetailView: View { navBar Spacer() } - .onAppear { - viewModel.loadWebAppWrapper( + .task { + await viewModel.loadWebAppWrapper( dataService: dataService, rawAuthCookie: authenticator.omnivoreAuthCookieString ) @@ -311,8 +302,8 @@ struct LinkItemDetailView: View { Text("Loading...") Spacer() } - .onAppear { - viewModel.loadWebAppWrapper( + .task { + await viewModel.loadWebAppWrapper( dataService: dataService, rawAuthCookie: authenticator.omnivoreAuthCookieString ) diff --git a/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift b/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift index 80d6eef94..aab2f65ab 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift @@ -5,12 +5,10 @@ import SwiftUI import Utils import Views -final class ProfileContainerViewModel: ObservableObject { +@MainActor final class ProfileContainerViewModel: ObservableObject { @Published var isLoading = false @Published var profileCardData = ProfileCardData() - var subscriptions = Set() - var appVersionString: String { if let appVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String { return "Omnivore Version \(appVersion)" @@ -19,18 +17,14 @@ final class ProfileContainerViewModel: ObservableObject { } } - func loadProfileData(dataService: DataService) { - dataService.viewerPublisher().sink( - receiveCompletion: { _ in }, - receiveValue: { [weak self] viewer in - self?.profileCardData = ProfileCardData( - name: viewer.name, - username: viewer.username, - imageURL: viewer.profileImageURL.flatMap { URL(string: $0) } - ) - } + func loadProfileData(dataService: DataService) async { + guard let viewer = try? await dataService.fetchViewer() else { return } + + profileCardData = ProfileCardData( + name: viewer.name, + username: viewer.username, + imageURL: viewer.profileImageURL.flatMap { URL(string: $0) } ) - .store(in: &subscriptions) } } @@ -59,7 +53,9 @@ struct ProfileView: View { Group { Section { ProfileCard(data: viewModel.profileCardData) - .onAppear { viewModel.loadProfileData(dataService: dataService) } + .task { + await viewModel.loadProfileData(dataService: dataService) + } } Section { diff --git a/apple/OmnivoreKit/Sources/App/Views/RootView/RootView.swift b/apple/OmnivoreKit/Sources/App/Views/RootView/RootView.swift index 0d4af5f7c..135be02c1 100644 --- a/apple/OmnivoreKit/Sources/App/Views/RootView/RootView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/RootView/RootView.swift @@ -91,10 +91,10 @@ struct InnerRootView: View { if viewModel.webLinkPath != nil { viewModel.webLinkPath = nil DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) { - viewModel.onOpenURL(url: url) + Task { await viewModel.onOpenURL(url: url) } } } else { - viewModel.onOpenURL(url: url) + Task { await viewModel.onOpenURL(url: url) } } } } diff --git a/apple/OmnivoreKit/Sources/App/Views/RootView/RootViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/RootView/RootViewModel.swift index fa8762ec0..a8afdf799 100644 --- a/apple/OmnivoreKit/Sources/App/Views/RootView/RootViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/RootView/RootViewModel.swift @@ -20,8 +20,6 @@ public final class RootViewModel: ObservableObject { @Published var snackbarMessage: String? @Published var showSnackbar = false - public var subscriptions = Set() - public init() { registerFonts() @@ -57,7 +55,7 @@ public final class RootViewModel: ObservableObject { ) } - func onOpenURL(url: URL) { + @MainActor func onOpenURL(url: URL) async { guard let linkRequestID = DeepLink.make(from: url)?.linkRequestID else { return } if let username = services.dataService.currentViewer?.username { @@ -66,17 +64,10 @@ public final class RootViewModel: ObservableObject { return } - services.dataService.viewerPublisher().sink( - receiveCompletion: { completion in - guard case let .failure(error) = completion else { return } - print(error) - }, - receiveValue: { [weak self] viewer in - let path = self?.linkRequestPath(username: viewer.username, requestID: linkRequestID) ?? "" - self?.webLinkPath = SafariWebLinkPath(id: UUID(), path: path) - } - ) - .store(in: &subscriptions) + if let viewer = try? await services.dataService.fetchViewer() { + let path = linkRequestPath(username: viewer.username, requestID: linkRequestID) + webLinkPath = SafariWebLinkPath(id: UUID(), path: path) + } } func triggerPushNotificationRequestIfNeeded() { diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Queries/ViewerFetcher.swift b/apple/OmnivoreKit/Sources/Services/DataService/Queries/ViewerFetcher.swift new file mode 100644 index 000000000..509ff01a7 --- /dev/null +++ b/apple/OmnivoreKit/Sources/Services/DataService/Queries/ViewerFetcher.swift @@ -0,0 +1,85 @@ +import Combine +import Foundation +import Models +import SwiftGraphQL +import Utils + +public extension DataService { + func fetchViewer() async throws -> Viewer { + let selection = Selection { + Viewer( + username: try $0.profile( + selection: .init { try $0.username() } + ), + name: try $0.name(), + profileImageURL: try $0.profile( + selection: .init { try $0.pictureUrl() } + ), + userID: try $0.id() + ) + } + + let query = Selection.Query { + try $0.me(selection: selection.nonNullOrFail) + } + + let path = appEnvironment.graphqlPath + let headers = networker.defaultHeaders + + return try await withCheckedThrowingContinuation { continuation in + send(query, to: path, headers: headers) { [weak self] result in + switch result { + case let .success(payload): + self?.currentViewer = payload.data + if UserDefaults.standard.string(forKey: Keys.userIdKey) == nil { + UserDefaults.standard.setValue(payload.data.userID, forKey: Keys.userIdKey) + DataService.registerIntercomUser?(payload.data.userID) + } + continuation.resume(returning: payload.data) + case .failure: + continuation.resume(throwing: BasicError.message(messageText: "http error")) + } + } + } + } +} + +extension DataService { + @available(*, deprecated, message: "use async version instead") + func internalViewerPublisher() -> AnyPublisher { + let selection = Selection { + Viewer( + username: try $0.profile( + selection: .init { try $0.username() } + ), + name: try $0.name(), + profileImageURL: try $0.profile( + selection: .init { try $0.pictureUrl() } + ), + userID: try $0.id() + ) + } + + let query = Selection.Query { + try $0.me(selection: selection.nonNullOrFail) + } + + let path = appEnvironment.graphqlPath + let headers = networker.defaultHeaders + + return Deferred { + Future { [weak self] promise in + send(query, to: path, headers: headers) { result in + switch result { + case let .success(payload): + self?.currentViewer = payload.data + promise(.success(payload.data)) + case .failure: + promise(.failure(.message(messageText: "http error"))) + } + } + } + } + .eraseToAnyPublisher() + } +} diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Queries/ViewerPublisher.swift b/apple/OmnivoreKit/Sources/Services/DataService/Queries/ViewerPublisher.swift deleted file mode 100644 index 7b91e2c50..000000000 --- a/apple/OmnivoreKit/Sources/Services/DataService/Queries/ViewerPublisher.swift +++ /dev/null @@ -1,59 +0,0 @@ -import Combine -import Foundation -import Models -import SwiftGraphQL -import Utils - -public extension DataService { - func viewerPublisher() -> AnyPublisher { - internalViewerPublisher() - .handleEvents(receiveOutput: { - // Persist ID so AppDelegate can use it to register Intercom users at launch time - if UserDefaults.standard.string(forKey: Keys.userIdKey) == nil { - UserDefaults.standard.setValue($0.userID, forKey: Keys.userIdKey) - DataService.registerIntercomUser?($0.userID) - } - }) - .receive(on: DispatchQueue.main) - .eraseToAnyPublisher() - } -} - -extension DataService { - func internalViewerPublisher() -> AnyPublisher { - let selection = Selection { - Viewer( - username: try $0.profile( - selection: .init { try $0.username() } - ), - name: try $0.name(), - profileImageURL: try $0.profile( - selection: .init { try $0.pictureUrl() } - ), - userID: try $0.id() - ) - } - - let query = Selection.Query { - try $0.me(selection: selection.nonNullOrFail) - } - - let path = appEnvironment.graphqlPath - let headers = networker.defaultHeaders - - return Deferred { - Future { [weak self] promise in - send(query, to: path, headers: headers) { result in - switch result { - case let .success(payload): - self?.currentViewer = payload.data - promise(.success(payload.data)) - case .failure: - promise(.failure(.message(messageText: "http error"))) - } - } - } - } - .eraseToAnyPublisher() - } -}