diff --git a/ConversationsClassic/AppData/Client/Client.swift b/ConversationsClassic/AppData/Client/Client.swift index aa28f02..0367046 100644 --- a/ConversationsClassic/AppData/Client/Client.swift +++ b/ConversationsClassic/AppData/Client/Client.swift @@ -1,5 +1,6 @@ import Combine import Foundation +import GRDB import Martin enum ClientState: Equatable { @@ -16,6 +17,24 @@ final class Client: ObservableObject { @Published private(set) var state: ClientState = .enabled(.disconnected) @Published private(set) var credentials: Credentials + var rosters: [Roster] { + get async { + // Fetching only non-locally-deleted rosters and only for active credentials + if credentials.isActive { + let creds = credentials + let rosters = try? await Database.shared.dbQueue.read { db in + try Roster + .filter(Column("bareJid") == creds.bareJid) + .filter(Column("locallyDeleted") == false) + .fetchAll(db) + } + return rosters ?? [] + } else { + return [] + } + } + } + private var connection: XMPPClient private var connectionCancellable: AnyCancellable? @@ -69,19 +88,10 @@ extension Client { } extension Client { - enum ClientLoginResult { - case success(Client) - case failure - } - - static func tryLogin(with credentials: Credentials) async -> ClientLoginResult { + static func tryLogin(with credentials: Credentials) async throws -> Client { let client = Client(credentials: credentials) - do { - try await client.connection.loginAndWait() - return .success(client) - } catch { - return .failure - } + try await client.connection.loginAndWait() + return client } } diff --git a/ConversationsClassic/AppData/Stores/ClientsStore.swift b/ConversationsClassic/AppData/Store/ClientsStore.swift similarity index 57% rename from ConversationsClassic/AppData/Stores/ClientsStore.swift rename to ConversationsClassic/AppData/Store/ClientsStore.swift index e16a294..a11af86 100644 --- a/ConversationsClassic/AppData/Stores/ClientsStore.swift +++ b/ConversationsClassic/AppData/Store/ClientsStore.swift @@ -9,15 +9,16 @@ final class ClientsStore: ObservableObject { @Published private(set) var ready = false @Published private(set) var clients: [Client] = [] - private let observation = ValueObservation.tracking(Credentials.fetchAll) + private let credentialsObservation = ValueObservation.tracking(Credentials.fetchAll) - func startFetching() { + init() { Task { do { - for try await credentials in observation.values(in: Database.shared.dbQueue) { - processCredentials(credentials) - ready = true - print("Fetched \(credentials.count) credentials") + for try await creds in credentialsObservation.values(in: Database.shared.dbQueue) { + processCredentials(creds) + if !ready { + ready = true + } } } catch {} } @@ -40,11 +41,14 @@ final class ClientsStore: ObservableObject { } extension ClientsStore { - func addNewClient(_ client: Client) { + func tryLogin(_ jidStr: String, _ pass: String) async throws { + // login with fake timeout + async let sleep: Void? = try? await Task.sleep(nanoseconds: 1 * NSEC_PER_SEC) + async let request = try await Client.tryLogin(with: .init(bareJid: jidStr, pass: pass, isActive: true)) + let client = try await(request, sleep).0 + clients.append(client) - Task(priority: .background) { - try? await client.credentials.save() - } + try? await client.credentials.save() } func reconnectAll() { @@ -59,3 +63,15 @@ extension ClientsStore { } } } + +extension ClientsStore { + var actualRosters: [Roster] { + get async { + var allRosters: [Roster] = [] + for client in clients { + allRosters.append(contentsOf: await client.rosters) + } + return allRosters + } + } +} diff --git a/ConversationsClassic/AppData/Stores/RostersStore.swift b/ConversationsClassic/AppData/Stores/RostersStore.swift deleted file mode 100644 index b709903..0000000 --- a/ConversationsClassic/AppData/Stores/RostersStore.swift +++ /dev/null @@ -1,119 +0,0 @@ -import Combine -import Foundation -import GRDB - -@MainActor -final class RostersStore: ObservableObject { - @Published private(set) var rosters: [Roster] = [] - @Published private(set) var locallyDeletedRosters: [Roster] = [] - - private var cancellable: AnyCancellable? - - init() { - let rostersPublisher = ValueObservation.tracking(Roster.fetchAll) - .publisher(in: Database.shared.dbQueue) - .receive(on: DispatchQueue.main) - .catch { _ in Just([]) } - - cancellable = ClientsStore.shared.$clients - .map { $0.filter { $0.state != .disabled } } // look rosters only for enabled clients - .flatMap { clients in - Publishers.MergeMany(clients.map { $0.$state }) - .prepend(clients.map { $0.state }) - .collect() - } - .combineLatest(rostersPublisher) - .sink { [weak self] clientStates, rosters in - self?.handleUpdates(clientStates: clientStates, rosters: rosters) - } - } - - private func handleUpdates(clientStates: [ClientState], rosters: [Roster]) { - self.rosters = rosters.filter { !$0.locallyDeleted } - locallyDeletedRosters = rosters.filter { $0.locallyDeleted } - print("Client States: \(clientStates.count), Rosters: \(rosters.count)") - } -} - -extension RostersStore { - enum RosterAddResult { - case success - case connectionError - case serverError - } - - func addRoster(_ owner: Credentials, contactJID: String, name _: String?, groups _: [String]) async -> RosterAddResult { - // check if such roster was already exists or locally deleted - if var exists = rosters.first(where: { $0.bareJid == owner.bareJid && $0.contactBareJid == contactJID }), exists.locallyDeleted { - try? await exists.setLocallyDeleted(false) - return .success - } - - // add new roster - // check that client is enabled and connected - let clientStorage = ClientsStore.shared - while !clientStorage.ready { - await Task.yield() - } - print("!! Clients: \(clientStorage.clients.count)") - guard let client = clientStorage.clients.first(where: { $0.credentials == owner }) else { - return .connectionError - } - guard client.state == .enabled(.connected) else { - return .connectionError - } - // guard let client = ClientsStore.shared.clients.first(where: { $0.credentials == owner }), client.state == .enabled(.connected) else { - // return .connectionError - // } - - // add roster - do { - try await client.addRoster(contactJID, name: nil, groups: []) - return .success - } catch { - return .serverError - } - - // guard let client = clientsStore.clients.first(where: { $0.credentials == ownerCredentials }), client.state == .enabled(.connected) else { - // router.showAlert( - // .alert, - // title: L10n.Global.Error.title, - // subtitle: L10n.Contacts.Add.connectionError - // ) { - // Button(L10n.Global.ok, role: .cancel) {} - // } - // return - // } - // - // router.showModal { - // LoadingScreen() - // } - // - // do { - // try await client.addRoster(contactJID, name: nil, groups: []) - // } catch { - // router.showAlert( - // .alert, - // title: L10n.Global.Error.title, - // subtitle: L10n.Contacts.Add.serverError - // ) { - // Button(L10n.Global.ok, role: .cancel) {} - // } - // return - // } - // - // router.dismissModal() - // router.dismissScreen() - - // client.addRoster(jid: ) - - // guard let ownerAccount else { return } - // if let exists = store.state.rostersState.rosters.first(where: { $0.bareJid == ownerAccount.bareJid && $0.contactBareJid == contactJID }), exists.locallyDeleted { - // store.dispatch(.rostersAction(.unmarkRosterAsLocallyDeleted(ownerJID: ownerAccount.bareJid, contactJID: contactJID))) - // isPresented = false - // } else { - // isShowingLoader = true - // store.dispatch(.rostersAction(.addRoster(ownerJID: ownerAccount.bareJid, contactJID: contactJID, name: nil, groups: []))) - // } - } -} diff --git a/ConversationsClassic/ConversationsClassicApp.swift b/ConversationsClassic/ConversationsClassicApp.swift index 698b045..bf24165 100644 --- a/ConversationsClassic/ConversationsClassicApp.swift +++ b/ConversationsClassic/ConversationsClassicApp.swift @@ -4,7 +4,7 @@ import SwiftUI @main @MainActor struct ConversationsClassic: App { - private var clientsStore = ClientsStore() + private let clientsStore = ClientsStore.shared init() { // There's a bug on iOS 17 where sheet may not load with large title, even if modifiers are set, which causes some tests to fail diff --git a/ConversationsClassic/Helpers/Bool+Extensions.swift b/ConversationsClassic/Helpers/Bool+Extensions.swift deleted file mode 100644 index b26674c..0000000 --- a/ConversationsClassic/Helpers/Bool+Extensions.swift +++ /dev/null @@ -1,7 +0,0 @@ -import Foundation - -extension Bool { - var intValue: Int { - self ? 1 : 0 - } -} diff --git a/ConversationsClassic/View/Entering/LoginScreen.swift b/ConversationsClassic/View/Entering/LoginScreen.swift index 288705e..665357a 100644 --- a/ConversationsClassic/View/Entering/LoginScreen.swift +++ b/ConversationsClassic/View/Entering/LoginScreen.swift @@ -106,17 +106,13 @@ struct LoginScreen: View { LoadingScreen() } - // login with fake timeout - async let sleep: Void? = try? await Task.sleep(nanoseconds: 1 * NSEC_PER_SEC) - async let request = await Client.tryLogin(with: .init(bareJid: jidStr, pass: pass, isActive: true)) - let result = await(request, sleep).0 - - switch result { - case .success(let client): - clientsStore.addNewClient(client) + defer { router.dismissModal() + } - case .failure: + do { + try await clientsStore.tryLogin(jidStr, pass) + } catch { router.dismissModal() router.showAlert( .alert, diff --git a/ConversationsClassic/View/Main/Contacts/AddContactOrChannelScreen.swift b/ConversationsClassic/View/Main/Contacts/AddContactOrChannelScreen.swift index 4c48c9e..3263e34 100644 --- a/ConversationsClassic/View/Main/Contacts/AddContactOrChannelScreen.swift +++ b/ConversationsClassic/View/Main/Contacts/AddContactOrChannelScreen.swift @@ -3,7 +3,6 @@ import SwiftUI struct AddContactOrChannelScreen: View { @Environment(\.router) var router @EnvironmentObject var clientsStore: ClientsStore - @EnvironmentObject var rostersStore: RostersStore enum Field { case account @@ -130,18 +129,20 @@ struct AddContactOrChannelScreen: View { router.dismissModal() } - let result = await rostersStore.addRoster(ownerCredentials, contactJID: contactJID, name: nil, groups: []) + router.dismissScreen() - switch result { - case .success: - router.dismissScreen() - - case .connectionError: - showErrorAlert(subtitle: L10n.Contacts.Add.connectionError) - - case .serverError: - showErrorAlert(subtitle: L10n.Contacts.Add.serverError) - } + // let result = await rostersStore.addRoster(ownerCredentials, contactJID: contactJID, name: nil, groups: []) + // + // switch result { + // case .success: + // router.dismissScreen() + // + // case .connectionError: + // showErrorAlert(subtitle: L10n.Contacts.Add.connectionError) + // + // case .serverError: + // showErrorAlert(subtitle: L10n.Contacts.Add.serverError) + // } } private func showErrorAlert(subtitle: String) { diff --git a/ConversationsClassic/View/Main/Contacts/ContactsScreen.swift b/ConversationsClassic/View/Main/Contacts/ContactsScreen.swift index c7d673f..a6eae7e 100644 --- a/ConversationsClassic/View/Main/Contacts/ContactsScreen.swift +++ b/ConversationsClassic/View/Main/Contacts/ContactsScreen.swift @@ -3,7 +3,9 @@ import SwiftUI struct ContactsScreen: View { @Environment(\.router) var router @EnvironmentObject var clientsStore: ClientsStore - @StateObject var rostersStore = RostersStore() + // @StateObject var rostersStore = RostersStore() + + @State private var rosters: [Roster] = [] var body: some View { ZStack { @@ -20,17 +22,18 @@ struct ContactsScreen: View { image: Image(systemName: "plus"), action: { router.showScreen(.fullScreenCover) { _ in - AddContactOrChannelScreen() - .environmentObject(rostersStore) + Text("") + // AddContactOrChannelScreen() + // .environmentObject(rostersStore) } } ) ) // Contacts list - if !rostersStore.rosters.isEmpty { + if !rosters.isEmpty { List { - ForEach(rostersStore.rosters) { roster in + ForEach(rosters) { roster in ContactsScreenRow( roster: roster ) @@ -43,9 +46,9 @@ struct ContactsScreen: View { } } } - // .task { - // await fetchRosters() - // } + .task { + rosters = await clientsStore.actualRosters + } // .loadingIndicator(isShowingLoader) // .fullScreenCover(isPresented: $addPanelPresented) { // AddContactOrChannelScreen(isPresented: $addPanelPresented) diff --git a/ConversationsClassic/View/RootView.swift b/ConversationsClassic/View/RootView.swift index 8215a79..61b3638 100644 --- a/ConversationsClassic/View/RootView.swift +++ b/ConversationsClassic/View/RootView.swift @@ -20,8 +20,5 @@ struct RootView: View { StartScreen() } } - .task { - clientsStore.startFetching() - } } }