Merge pull request #429 from omnivore-app/chore/swift-async
Convert ViewerPublisher to async fetchViewer
This commit is contained in:
@ -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: ""))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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
|
||||
)
|
||||
|
||||
@ -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<AnyCancellable>()
|
||||
|
||||
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 {
|
||||
|
||||
@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,8 +20,6 @@ public final class RootViewModel: ObservableObject {
|
||||
@Published var snackbarMessage: String?
|
||||
@Published var showSnackbar = false
|
||||
|
||||
public var subscriptions = Set<AnyCancellable>()
|
||||
|
||||
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() {
|
||||
|
||||
@ -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, Objects.User> {
|
||||
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<Viewer, BasicError> {
|
||||
let selection = Selection<Viewer, Objects.User> {
|
||||
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()
|
||||
}
|
||||
}
|
||||
@ -1,59 +0,0 @@
|
||||
import Combine
|
||||
import Foundation
|
||||
import Models
|
||||
import SwiftGraphQL
|
||||
import Utils
|
||||
|
||||
public extension DataService {
|
||||
func viewerPublisher() -> AnyPublisher<Viewer, BasicError> {
|
||||
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<Viewer, BasicError> {
|
||||
let selection = Selection<Viewer, Objects.User> {
|
||||
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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user