another.im-ios/ConversationsClassic/AppData/Store/ClientsStore.swift

238 lines
8 KiB
Swift
Raw Normal View History

import Combine
import Foundation
import GRDB
2024-10-15 17:47:51 +00:00
enum ClientsListState {
case empty
case allDisabled
case haveSomeEnabled
}
@MainActor
final class ClientsStore: ObservableObject {
static let shared = ClientsStore()
@Published private(set) var ready = false
@Published private(set) var clients: [Client] = []
@Published private(set) var actualRosters: [Roster] = []
@Published private(set) var actualChats: [Chat] = []
2024-10-15 17:47:51 +00:00
@Published private(set) var listState: ClientsListState = .empty
private var credentialsCancellable: AnyCancellable?
private var rostersCancellable: AnyCancellable?
private var chatsCancellable: AnyCancellable?
init() {
credentialsCancellable = ValueObservation
.tracking { db in
try Credentials.fetchAll(db)
}
.publisher(in: Database.shared.dbQueue)
.catch { _ in Just([]) }
.sink { [weak self] creds in
self?.processCredentials(creds)
}
}
private func processCredentials(_ credentials: [Credentials]) {
let existsJids = Set(clients.map { $0.credentials.bareJid })
let credentialsJids = Set(credentials.map { $0.bareJid })
let forAdd = credentials.filter { !existsJids.contains($0.bareJid) }
let newClients = forAdd.map { Client(credentials: $0) }
let forRemove = clients.filter { !credentialsJids.contains($0.credentials.bareJid) }
forRemove.forEach { $0.disconnect() }
var updatedClients = clients.filter { credentialsJids.contains($0.credentials.bareJid) }
updatedClients.append(contentsOf: newClients)
clients = updatedClients
2024-10-15 17:47:51 +00:00
// for creds in credentials {
// if let client = client(for: creds) {
// client.credentials = creds
// }
// }
if !ready {
ready = true
}
resubscribeRosters()
resubscribeChats()
2024-10-15 17:47:51 +00:00
reconnectNeeded()
if credentials.isEmpty {
listState = .empty
} else if credentials.allSatisfy({ !$0.isActive }) {
listState = .allDisabled
} else {
listState = .haveSomeEnabled
}
objectWillChange.send()
}
private func client(for credentials: Credentials) -> Client? {
clients.first { $0.credentials == credentials }
}
}
// MARK: - Login/Connections
extension ClientsStore {
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)
try? await client.credentials.save()
}
2024-10-15 17:47:51 +00:00
private func reconnectNeeded() {
Task {
await withTaskGroup(of: Void.self) { taskGroup in
for client in clients {
2024-10-15 17:47:51 +00:00
if !client.credentials.isActive && client.state == .enabled(.connected) {
2024-10-15 10:17:26 +00:00
taskGroup.addTask {
2024-10-15 17:47:51 +00:00
client.disconnect()
2024-10-15 10:17:26 +00:00
}
}
2024-10-15 17:47:51 +00:00
if client.credentials.isActive && client.state != .enabled(.connected) {
2024-10-15 10:17:26 +00:00
taskGroup.addTask {
2024-10-15 17:47:51 +00:00
await client.connect()
2024-10-15 10:17:26 +00:00
}
}
}
}
}
}
}
// MARK: - Manage Rosters
extension ClientsStore {
func addRoster(_ credentials: Credentials, contactJID: String, name: String?, groups: [String]) async throws {
// check that roster exist in db as locally deleted and undelete it
let deletedLocally = await Roster.allDeletedLocally
if var roster = deletedLocally.first(where: { $0.contactBareJid == contactJID }) {
try await roster.setLocallyDeleted(false)
return
}
// add new roster
guard let client = client(for: credentials) else {
throw AppError.clientNotFound
}
try await client.addRoster(contactJID, name: name, groups: groups)
}
func deleteRoster(_ roster: Roster) async throws {
guard let client = clients.first(where: { $0.credentials.bareJid == roster.bareJid }) else {
throw AppError.clientNotFound
}
try await client.deleteRoster(roster)
}
}
extension ClientsStore {
func addRosterForNewChatIfNeeded(_ chat: Chat) async throws {
let exists = try? await chat.fetchRoster()
if exists == nil {
guard let client = clients.first(where: { $0.credentials.bareJid == chat.account }) else {
throw AppError.clientNotFound
}
try await addRoster(client.credentials, contactJID: chat.participant, name: nil, groups: [])
2024-09-16 12:10:35 +00:00
// Hack here. Because we want to show chat immediately after adding roster (without waiting for server
// response and update rosters list) we need to write it to db manually
try await client.addRosterLocally(chat.participant, name: nil, groups: [])
}
}
}
// MARK: - Produce stores for conversation
extension ClientsStore {
2024-09-20 15:32:10 +00:00
// swiftlint:disable:next large_tuple
2024-10-15 11:39:23 +00:00
func conversationStores(for roster: Roster) async throws -> (MessagesStore, AttachmentsStore, ChatSettingsStore) {
while !ready {
await Task.yield()
}
guard let client = clients.first(where: { $0.credentials.bareJid == roster.bareJid }) else {
throw AppError.clientNotFound
}
let conversationStore = MessagesStore(roster: roster, client: client)
let attachmentsStore = AttachmentsStore(roster: roster, client: client)
2024-10-15 11:39:23 +00:00
let settingsStore = ChatSettingsStore(roster: roster, client: client)
2024-09-20 15:32:10 +00:00
return (conversationStore, attachmentsStore, settingsStore)
}
2024-09-20 15:32:10 +00:00
// swiftlint:disable:next large_tuple
2024-10-15 11:39:23 +00:00
func conversationStores(for chat: Chat) async throws -> (MessagesStore, AttachmentsStore, ChatSettingsStore) {
while !ready {
await Task.yield()
}
guard let client = clients.first(where: { $0.credentials.bareJid == chat.account }) else {
throw AppError.clientNotFound
}
let roster = try await chat.fetchRoster()
let conversationStore = MessagesStore(roster: roster, client: client)
let attachmentsStore = AttachmentsStore(roster: roster, client: client)
2024-10-15 11:39:23 +00:00
let settingsStore = ChatSettingsStore(roster: roster, client: client)
2024-09-20 15:32:10 +00:00
return (conversationStore, attachmentsStore, settingsStore)
}
}
// MARK: - Subscriptions
private extension ClientsStore {
private func resubscribeRosters() {
let clientsJids = clients
.filter { $0.state != .disabled }
.map { $0.credentials.bareJid }
rostersCancellable = ValueObservation.tracking { db in
try Roster
.filter(clientsJids.contains(Column("bareJid")))
.filter(Column("locallyDeleted") == false)
.fetchAll(db)
}
.publisher(in: Database.shared.dbQueue)
.catch { _ in Just([]) }
.sink { [weak self] rosters in
self?.actualRosters = rosters
}
}
func resubscribeChats() {
let clientsJids = clients
.filter { $0.state != .disabled }
.map { $0.credentials.bareJid }
chatsCancellable = ValueObservation.tracking { db in
try Chat
.filter(clientsJids.contains(Column("account")))
.fetchAll(db)
}
.publisher(in: Database.shared.dbQueue)
.catch { _ in Just([]) }
.sink { [weak self] chats in
self?.actualChats = chats
}
}
}
2024-10-04 15:58:04 +00:00
// MARK: - Remove all data for debug
#if DEBUG
extension ClientsStore {
func flushAllData() {
clients.forEach { $0.disconnect() }
clients.removeAll()
actualRosters.removeAll()
actualChats.removeAll()
}
}
#endif