mv-experiment #1
|
@ -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 client
|
||||||
return .success(client)
|
|
||||||
} catch {
|
|
||||||
return .failure
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
ready = true
|
if !ready {
|
||||||
print("Fetched \(credentials.count) credentials")
|
ready = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
@ -40,11 +41,14 @@ 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() {
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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: [])))
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
import Foundation
|
|
||||||
|
|
||||||
extension Bool {
|
|
||||||
var intValue: Int {
|
|
||||||
self ? 1 : 0
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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,
|
||||||
|
|
|
@ -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: [])
|
router.dismissScreen()
|
||||||
|
|
||||||
switch result {
|
// let result = await rostersStore.addRoster(ownerCredentials, contactJID: contactJID, name: nil, groups: [])
|
||||||
case .success:
|
//
|
||||||
router.dismissScreen()
|
// switch result {
|
||||||
|
// case .success:
|
||||||
case .connectionError:
|
// router.dismissScreen()
|
||||||
showErrorAlert(subtitle: L10n.Contacts.Add.connectionError)
|
//
|
||||||
|
// case .connectionError:
|
||||||
case .serverError:
|
// showErrorAlert(subtitle: L10n.Contacts.Add.connectionError)
|
||||||
showErrorAlert(subtitle: L10n.Contacts.Add.serverError)
|
//
|
||||||
}
|
// case .serverError:
|
||||||
|
// showErrorAlert(subtitle: L10n.Contacts.Add.serverError)
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
private func showErrorAlert(subtitle: String) {
|
private func showErrorAlert(subtitle: String) {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -20,8 +20,5 @@ struct RootView: View {
|
||||||
StartScreen()
|
StartScreen()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.task {
|
|
||||||
clientsStore.startFetching()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue