From a93b9d47c2273b65b3ec7f23e940569ef91e623f Mon Sep 17 00:00:00 2001 From: fmodf Date: Sun, 11 Aug 2024 19:07:02 +0200 Subject: [PATCH] wip --- .../AppData/Client/Client.swift | 8 +- .../AppData/Model/Credentials.swift | 9 +- .../AppData/Model/Roster.swift | 9 +- .../AppData/Stores/RostersStore.swift | 77 +++++- .../Resources/Strings/Localizable.strings | 85 +++---- .../Contacts/AddContactOrChannelScreen.swift | 225 +++++++++--------- .../View/Main/Contacts/ContactsScreen.swift | 15 +- 7 files changed, 253 insertions(+), 175 deletions(-) diff --git a/ConversationsClassic/AppData/Client/Client.swift b/ConversationsClassic/AppData/Client/Client.swift index fddbecb..c3a656a 100644 --- a/ConversationsClassic/AppData/Client/Client.swift +++ b/ConversationsClassic/AppData/Client/Client.swift @@ -42,11 +42,11 @@ final class Client: ObservableObject { } } - func addRoster(_ roster: Roster) async throws { + func addRoster(_ jid: String, name: String?, groups: [String]) async throws { _ = try await connection.module(.roster).addItem( - jid: JID(roster.contactBareJid), - name: roster.name, - groups: roster.data.groups + jid: JID(jid), + name: name, + groups: groups ) } diff --git a/ConversationsClassic/AppData/Model/Credentials.swift b/ConversationsClassic/AppData/Model/Credentials.swift index 06ab1a9..32cca11 100644 --- a/ConversationsClassic/AppData/Model/Credentials.swift +++ b/ConversationsClassic/AppData/Model/Credentials.swift @@ -26,8 +26,7 @@ struct Credentials: DBStorable, Hashable { } } -// extension Account: UniversalInputSelectionElement { -// var text: String? { bareJid } -// var icon: Image? { nil } -// } -// +extension Credentials: UniversalInputSelectionElement { + var text: String? { bareJid } + var icon: Image? { nil } +} diff --git a/ConversationsClassic/AppData/Model/Roster.swift b/ConversationsClassic/AppData/Model/Roster.swift index 44cc474..1eb0873 100644 --- a/ConversationsClassic/AppData/Model/Roster.swift +++ b/ConversationsClassic/AppData/Model/Roster.swift @@ -62,10 +62,11 @@ extension Roster: Equatable { } extension Roster { - static func fetchAll(for jid: String) async throws -> [Roster] { - let rosters = try await Database.shared.dbQueue.read { db in - try Roster.filter(Column("bareJid") == jid).fetchAll(db) + mutating func setLocallyDeleted(_ value: Bool) async throws { + locallyDeleted = value + let copy = self + try? await Database.shared.dbQueue.write { db in + try copy.save(db) } - return rosters } } diff --git a/ConversationsClassic/AppData/Stores/RostersStore.swift b/ConversationsClassic/AppData/Stores/RostersStore.swift index f0747da..192ba20 100644 --- a/ConversationsClassic/AppData/Stores/RostersStore.swift +++ b/ConversationsClassic/AppData/Stores/RostersStore.swift @@ -5,6 +5,7 @@ import GRDB @MainActor final class RostersStore: ObservableObject { @Published private(set) var rosters: [Roster] = [] + @Published private(set) var locallyDeletedRosters: [Roster] = [] private var cancellable: AnyCancellable? @@ -19,6 +20,7 @@ final class RostersStore: ObservableObject { .catch { _ in Just([]) } cancellable = clientsPublisher + .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 }) @@ -31,7 +33,80 @@ final class RostersStore: ObservableObject { } private func handleUpdates(clientStates: [ClientState], rosters: [Roster]) { - self.rosters = rosters + 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(ownerJid: String, 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 == ownerJid && $0.contactBareJid == contactJID }), exists.locallyDeleted { + try? await exists.setLocallyDeleted(false) + return .success + } + + // add new roster + // check that client is enabled and connected + guard let client = ClientsStore.shared.clients.first(where: { $0.credentials.bareJid == ownerJid }), 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/Resources/Strings/Localizable.strings b/ConversationsClassic/Resources/Strings/Localizable.strings index 954cbcc..dd4cd68 100644 --- a/ConversationsClassic/Resources/Strings/Localizable.strings +++ b/ConversationsClassic/Resources/Strings/Localizable.strings @@ -24,8 +24,15 @@ "Tabs.Name.conversations" = "Chats"; "Tabs.Name.settings" = "Settings"; +// MARK: Contacts screen +"Contacts.title" = "Contacts"; +// MARK: Add contact/channel screen +"Contacts.Add.title" = "Add Contact"; +"Contacts.Add.explanation" = "Contact or group/channel name are usually JID in format name@domain.ltd (like email)"; +"Contacts.Add.serverError" = "Contact not added. Server returns error."; +"Contacts.Add.connectionError" = "You need to be connected to internet for add contact/channel"; @@ -44,55 +51,51 @@ //"Login.Error.serverError" = "Server error. Check internet connection"; // MARK: Contacts screen -"Contacts.title" = "Contacts"; -"Contacts.sendMessage" = "Send message"; -"Contacts.editContact" = "Edit contact"; -"Contacts.selectContact" = "Select contact"; -"Contacts.deleteContact" = "Delete contact"; -"Contacts.Add.title" = "Add Contact"; -"Contacts.Add.explanation" = "Contact or group/channel name are usually JID in format name@domain.ltd (like email)"; -"Contacts.Add.error" = "Contact not added. Server returns error."; -"Contacts.Delete.title" = "Delete contact"; -"Contacts.Delete.message" = "You can delete contact from this device (contact will be available on other devices), or delete it completely"; -"Contacts.Delete.deleteFromDevice" = "Delete from device"; -"Contacts.Delete.deleteCompletely" = "Delete completely"; -"Contacts.Delete.error" = "Contact not deleted. Server returns error."; +//"Contacts.sendMessage" = "Send message"; +//"Contacts.editContact" = "Edit contact"; +//"Contacts.selectContact" = "Select contact"; +//"Contacts.deleteContact" = "Delete contact"; +//"Contacts.Delete.title" = "Delete contact"; +//"Contacts.Delete.message" = "You can delete contact from this device (contact will be available on other devices), or delete it completely"; +//"Contacts.Delete.deleteFromDevice" = "Delete from device"; +//"Contacts.Delete.deleteCompletely" = "Delete completely"; +//"Contacts.Delete.error" = "Contact not deleted. Server returns error."; // MARK: Chats screen -"Chats.title" = "Chats"; +//"Chats.title" = "Chats"; -"Chat.title" = "Chat"; -"Chat.textfieldPrompt" = "Type a message"; +//"Chat.title" = "Chat"; +//"Chat.textfieldPrompt" = "Type a message"; -"Chats.Create.Main.title" = "Create"; -"Chats.Create.Main.createGroup" = "Create public group"; -"Chats.Create.Main.createPrivateGroup" = "Create private group"; -"Chats.Create.Main.findGroup" = "Find public group"; +//"Chats.Create.Main.title" = "Create"; +//"Chats.Create.Main.createGroup" = "Create public group"; +//"Chats.Create.Main.createPrivateGroup" = "Create private group"; +//"Chats.Create.Main.findGroup" = "Find public group"; // MARK: Accounts add screen -"Accounts.Add.or" = "or"; -"Accounts.Add.Exist.title" = "Add existing\naccount"; -"Accounts.Add.Exist.Prompt.jid" = "Enter your XMPP ID"; -"Accounts.Add.Exist.Prompt.password" = "Enter password"; -"Accounts.Add.Exist.Hint.jid" = "user@domain.im"; -"Accounts.Add.Exist.Hint.password" = "password"; -"Accounts.Add.Exist.Btn.link" = "create a new one"; -"Accounts.Add.Exist.Btn.main" = "Continue"; -"Accounts.Add.Exist.loginError" = "Wrong login or password"; +//"Accounts.Add.or" = "or"; +//"Accounts.Add.Exist.title" = "Add existing\naccount"; +//"Accounts.Add.Exist.Prompt.jid" = "Enter your XMPP ID"; +//"Accounts.Add.Exist.Prompt.password" = "Enter password"; +//"Accounts.Add.Exist.Hint.jid" = "user@domain.im"; +//"Accounts.Add.Exist.Hint.password" = "password"; +//"Accounts.Add.Exist.Btn.link" = "create a new one"; +//"Accounts.Add.Exist.Btn.main" = "Continue"; +//"Accounts.Add.Exist.loginError" = "Wrong login or password"; // MARK: Server connecting indicator -"ServerConnectingIndicator.State.connecting" = "Connecting to server"; -"ServerConnectingIndicator.State.connected" = "Connected"; -"ServerConnectingIndicator.State.error" = "Server unreachable. Check internet connection and server name"; +//"ServerConnectingIndicator.State.connecting" = "Connecting to server"; +//"ServerConnectingIndicator.State.connected" = "Connected"; +//"ServerConnectingIndicator.State.error" = "Server unreachable. Check internet connection and server name"; // MARK: Attachments -"Attachment.Prompt.main" = "Select attachment"; -"Attachment.Tab.media" = "Media"; -"Attachment.Tab.files" = "Files"; -"Attachment.Tab.location" = "Location"; -"Attachment.Tab.contacts" = "Contacts"; -"Attachment.Send.media" = "Send media"; -"Attachment.Send.location" = "Send location"; -"Attachment.Send.contact" = "Send contact"; -"Attachment.Downloading.retry" = "Retry"; +//"Attachment.Prompt.main" = "Select attachment"; +//"Attachment.Tab.media" = "Media"; +//"Attachment.Tab.files" = "Files"; +//"Attachment.Tab.location" = "Location"; +//"Attachment.Tab.contacts" = "Contacts"; +//"Attachment.Send.media" = "Send media"; +//"Attachment.Send.location" = "Send location"; +//"Attachment.Send.contact" = "Send contact"; +//"Attachment.Downloading.retry" = "Retry"; diff --git a/ConversationsClassic/View/Main/Contacts/AddContactOrChannelScreen.swift b/ConversationsClassic/View/Main/Contacts/AddContactOrChannelScreen.swift index e0d0e0d..d3e826d 100644 --- a/ConversationsClassic/View/Main/Contacts/AddContactOrChannelScreen.swift +++ b/ConversationsClassic/View/Main/Contacts/AddContactOrChannelScreen.swift @@ -2,21 +2,18 @@ import SwiftUI struct AddContactOrChannelScreen: View { @Environment(\.router) var router + @EnvironmentObject var clientsStore: ClientsStore + @EnvironmentObject var rostersStore: RostersStore - // enum Field { - // case account - // case contact - // } - // - // @FocusState private var focus: Field? - // - // @Binding var isPresented: Bool - // @State private var contactJID: String = "" - // // @State private var ownerAccount: Account? - // - // @State private var isShowingLoader = false - // @State private var isShowingAlert = false - // @State private var errorMsg = "" + enum Field { + case account + case contact + } + + @FocusState private var focus: Field? + + @State private var ownerCredentials: Credentials? + @State private var contactJID: String = "" var body: some View { ZStack { @@ -43,107 +40,117 @@ struct AddContactOrChannelScreen: View { ) ) - // VStack(spacing: 16) { - // // Explanation text - // - // Text(L10n.Contacts.Add.explanation) - // .font(.body3) - // .foregroundColor(.Material.Shape.separator) - // .multilineTextAlignment(.center) - // .padding(.top, 16) - // - // // Account selector - // HStack(spacing: 0) { - // Text("Use account:") - // .font(.body2) - // .foregroundColor(.Material.Text.main) - // .frame(alignment: .leading) - // Spacer() - // } - // // UniversalInputCollection.DropDownMenu( - // // prompt: "Use account", - // // elements: store.state.accountsState.accounts, - // // selected: $ownerAccount, - // // focus: $focus, - // // fieldType: .account - // // ) - // - // // Contact text input - // HStack(spacing: 0) { - // Text("Contact JID:") - // .font(.body2) - // .foregroundColor(.Material.Text.main) - // .frame(alignment: .leading) - // Spacer() - // } - // UniversalInputCollection.TextField( - // prompt: "Contact or channel JID", - // text: $contactJID, - // focus: $focus, - // fieldType: .contact, - // contentType: .emailAddress, - // keyboardType: .emailAddress, - // submitLabel: .done, - // action: { - // focus = .account - // } - // ) - // - // // Save button - // Button { - // navigation.flow = .main(.contacts(.list)) - // } label: { - // Text(L10n.Global.save) - // } - // .buttonStyle(PrimaryButtonStyle()) - // .disabled(!inputValid) - // .padding(.top) - // Spacer() - // } + // Content + VStack(spacing: 16) { + // Explanation text + Text(L10n.Contacts.Add.explanation) + .font(.body3) + .foregroundColor(.Material.Shape.separator) + .multilineTextAlignment(.center) + .padding(.top, 16) + + // Account selector + HStack(spacing: 0) { + Text("Use account:") + .font(.body2) + .foregroundColor(.Material.Text.main) + .frame(alignment: .leading) + Spacer() + } + UniversalInputCollection.DropDownMenu( + prompt: "Use account", + elements: activeClientsCredentials, + selected: $ownerCredentials, + focus: $focus, + fieldType: .account + ) + + // Contact text input + HStack(spacing: 0) { + Text("Contact JID:") + .font(.body2) + .foregroundColor(.Material.Text.main) + .frame(alignment: .leading) + Spacer() + } + UniversalInputCollection.TextField( + prompt: "Contact or channel JID", + text: $contactJID, + focus: $focus, + fieldType: .contact, + contentType: .emailAddress, + keyboardType: .emailAddress, + submitLabel: .done, + action: { + focus = .account + } + ) + + // Save button + Button { + Task { + await save() + } + } label: { + Text(L10n.Global.save) + } + .buttonStyle(PrimaryButtonStyle()) + .disabled(!inputValid) + .padding(.top) + Spacer() + } .padding(.horizontal, 32) } } - // .onAppear { - // if let exists = store.state.accountsState.accounts.first, exists.isActive { - // ownerAccount = exists - // } - // } - // .loadingIndicator(isShowingLoader) - // .alert(isPresented: $isShowingAlert) { - // Alert( - // title: Text(L10n.Global.Error.title), - // message: Text(errorMsg), - // dismissButton: .default(Text(L10n.Global.ok)) - // ) - // } - // .onChange(of: store.state.rostersState.newAddedRosterJid) { jid in - // if jid != nil, isShowingLoader { - // isShowingLoader = false - // isPresented = false - // } - // } - // .onChange(of: store.state.rostersState.newAddedRosterError) { error in - // if let error = error, isShowingLoader { - // isShowingLoader = false - // errorMsg = error - // isShowingAlert = true - // } - // } + .onAppear { + if let exists = activeClientsCredentials.first { + ownerCredentials = exists + } + } + } + + private var activeClientsCredentials: [Credentials] { + clientsStore.clients + .filter { $0.state != .disabled } + .map { $0.credentials } } private var inputValid: Bool { - true - // ownerAccount != nil && !contactJID.isEmpty && UniversalInputCollection.Validators.isEmail(contactJID) + ownerCredentials != nil && !contactJID.isEmpty && UniversalInputCollection.Validators.isEmail(contactJID) } - // private func save() { - // 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: []))) - // } - // } + private func save() async { + guard let ownerCredentials = ownerCredentials else { return } + + router.showModal { + LoadingScreen() + } + + defer { + router.dismissModal() + } + + let result = await rostersStore.addRoster(ownerJid: ownerCredentials.bareJid, 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) { + router.showAlert( + .alert, + title: L10n.Global.Error.title, + subtitle: subtitle + ) { + Button(L10n.Global.ok, role: .cancel) {} + } + } } diff --git a/ConversationsClassic/View/Main/Contacts/ContactsScreen.swift b/ConversationsClassic/View/Main/Contacts/ContactsScreen.swift index 8a79778..a4c19c0 100644 --- a/ConversationsClassic/View/Main/Contacts/ContactsScreen.swift +++ b/ConversationsClassic/View/Main/Contacts/ContactsScreen.swift @@ -5,11 +5,6 @@ struct ContactsScreen: View { @EnvironmentObject var clientsStore: ClientsStore @StateObject var rostersStore = RostersStore(clientsPublisher: ClientsStore.shared.$clients) - // @State private var addPanelPresented = false - // @State private var isErrorAlertPresented = false - // @State private var errorAlertMessage = "" - // @State private var isShowingLoader = false - var body: some View { ZStack { // Background color @@ -26,6 +21,7 @@ struct ContactsScreen: View { action: { router.showScreen(.fullScreenCover) { _ in AddContactOrChannelScreen() + .environmentObject(rostersStore) } } ) @@ -37,9 +33,6 @@ struct ContactsScreen: View { ForEach(rostersStore.rosters) { roster in ContactsScreenRow( roster: roster - // isErrorAlertPresented: $isErrorAlertPresented, - // errorAlertMessage: $errorAlertMessage, - // isShowingLoader: $isShowingLoader ) } } @@ -103,9 +96,9 @@ private struct ContactsScreenRow: View { iconType: .charCircle(roster.name?.firstLetter ?? roster.contactBareJid.firstLetter), text: roster.contactBareJid ) - // .onTapGesture { - // store.dispatch(.chatsAction(.startChat(accountJid: roster.bareJid, participantJid: roster.contactBareJid))) - // } + .onTapGesture { + // store.dispatch(.chatsAction(.startChat(accountJid: roster.bareJid, participantJid: roster.contactBareJid))) + } // .onLongPressGesture { // isShowingMenu.toggle() // }