mv-experiment #1

Merged
fmodf merged 88 commits from mv-experiment into develop 2024-09-03 15:13:59 +00:00
9 changed files with 78 additions and 181 deletions
Showing only changes of commit 684b37247a - Show all commits

View file

@ -1,5 +1,6 @@
import Combine import Combine
import Foundation import Foundation
import GRDB
import Martin import Martin
enum ClientState: Equatable { enum ClientState: Equatable {
@ -16,6 +17,24 @@ final class Client: ObservableObject {
@Published private(set) var state: ClientState = .enabled(.disconnected) @Published private(set) var state: ClientState = .enabled(.disconnected)
@Published private(set) var credentials: Credentials @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 connection: XMPPClient
private var connectionCancellable: AnyCancellable? private var connectionCancellable: AnyCancellable?
@ -69,19 +88,10 @@ extension Client {
} }
extension Client { extension Client {
enum ClientLoginResult { static func tryLogin(with credentials: Credentials) async throws -> Client {
case success(Client)
case failure
}
static func tryLogin(with credentials: Credentials) async -> ClientLoginResult {
let client = Client(credentials: credentials) let client = Client(credentials: credentials)
do {
try await client.connection.loginAndWait() try await client.connection.loginAndWait()
return .success(client) return client
} catch {
return .failure
}
} }
} }

View file

@ -9,15 +9,16 @@ final class ClientsStore: ObservableObject {
@Published private(set) var ready = false @Published private(set) var ready = false
@Published private(set) var clients: [Client] = [] @Published private(set) var clients: [Client] = []
private let observation = ValueObservation.tracking(Credentials.fetchAll) private let credentialsObservation = ValueObservation.tracking(Credentials.fetchAll)
func startFetching() { init() {
Task { Task {
do { do {
for try await credentials in observation.values(in: Database.shared.dbQueue) { for try await creds in credentialsObservation.values(in: Database.shared.dbQueue) {
processCredentials(credentials) processCredentials(creds)
if !ready {
ready = true ready = true
print("Fetched \(credentials.count) credentials") }
} }
} catch {} } catch {}
} }
@ -40,12 +41,15 @@ final class ClientsStore: ObservableObject {
} }
extension ClientsStore { 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) clients.append(client)
Task(priority: .background) {
try? await client.credentials.save() try? await client.credentials.save()
} }
}
func reconnectAll() { func reconnectAll() {
Task { Task {
@ -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
}
}
}

View file

@ -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: [])))
// }
}
}

View file

@ -4,7 +4,7 @@ import SwiftUI
@main @main
@MainActor @MainActor
struct ConversationsClassic: App { struct ConversationsClassic: App {
private var clientsStore = ClientsStore() private let clientsStore = ClientsStore.shared
init() { 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 // 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

View file

@ -1,7 +0,0 @@
import Foundation
extension Bool {
var intValue: Int {
self ? 1 : 0
}
}

View file

@ -106,17 +106,13 @@ struct LoginScreen: View {
LoadingScreen() LoadingScreen()
} }
// login with fake timeout defer {
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)
router.dismissModal() router.dismissModal()
}
case .failure: do {
try await clientsStore.tryLogin(jidStr, pass)
} catch {
router.dismissModal() router.dismissModal()
router.showAlert( router.showAlert(
.alert, .alert,

View file

@ -3,7 +3,6 @@ import SwiftUI
struct AddContactOrChannelScreen: View { struct AddContactOrChannelScreen: View {
@Environment(\.router) var router @Environment(\.router) var router
@EnvironmentObject var clientsStore: ClientsStore @EnvironmentObject var clientsStore: ClientsStore
@EnvironmentObject var rostersStore: RostersStore
enum Field { enum Field {
case account case account
@ -130,18 +129,20 @@ struct AddContactOrChannelScreen: View {
router.dismissModal() router.dismissModal()
} }
let result = await rostersStore.addRoster(ownerCredentials, contactJID: contactJID, name: nil, groups: [])
switch result {
case .success:
router.dismissScreen() router.dismissScreen()
case .connectionError: // let result = await rostersStore.addRoster(ownerCredentials, contactJID: contactJID, name: nil, groups: [])
showErrorAlert(subtitle: L10n.Contacts.Add.connectionError) //
// switch result {
case .serverError: // case .success:
showErrorAlert(subtitle: L10n.Contacts.Add.serverError) // router.dismissScreen()
} //
// case .connectionError:
// showErrorAlert(subtitle: L10n.Contacts.Add.connectionError)
//
// case .serverError:
// showErrorAlert(subtitle: L10n.Contacts.Add.serverError)
// }
} }
private func showErrorAlert(subtitle: String) { private func showErrorAlert(subtitle: String) {

View file

@ -3,7 +3,9 @@ import SwiftUI
struct ContactsScreen: View { struct ContactsScreen: View {
@Environment(\.router) var router @Environment(\.router) var router
@EnvironmentObject var clientsStore: ClientsStore @EnvironmentObject var clientsStore: ClientsStore
@StateObject var rostersStore = RostersStore() // @StateObject var rostersStore = RostersStore()
@State private var rosters: [Roster] = []
var body: some View { var body: some View {
ZStack { ZStack {
@ -20,17 +22,18 @@ struct ContactsScreen: View {
image: Image(systemName: "plus"), image: Image(systemName: "plus"),
action: { action: {
router.showScreen(.fullScreenCover) { _ in router.showScreen(.fullScreenCover) { _ in
AddContactOrChannelScreen() Text("")
.environmentObject(rostersStore) // AddContactOrChannelScreen()
// .environmentObject(rostersStore)
} }
} }
) )
) )
// Contacts list // Contacts list
if !rostersStore.rosters.isEmpty { if !rosters.isEmpty {
List { List {
ForEach(rostersStore.rosters) { roster in ForEach(rosters) { roster in
ContactsScreenRow( ContactsScreenRow(
roster: roster roster: roster
) )
@ -43,9 +46,9 @@ struct ContactsScreen: View {
} }
} }
} }
// .task { .task {
// await fetchRosters() rosters = await clientsStore.actualRosters
// } }
// .loadingIndicator(isShowingLoader) // .loadingIndicator(isShowingLoader)
// .fullScreenCover(isPresented: $addPanelPresented) { // .fullScreenCover(isPresented: $addPanelPresented) {
// AddContactOrChannelScreen(isPresented: $addPanelPresented) // AddContactOrChannelScreen(isPresented: $addPanelPresented)

View file

@ -20,8 +20,5 @@ struct RootView: View {
StartScreen() StartScreen()
} }
} }
.task {
clientsStore.startFetching()
}
} }
} }