This commit is contained in:
fmodf 2024-08-11 19:07:02 +02:00
parent b72ff93ebe
commit a93b9d47c2
7 changed files with 253 additions and 175 deletions

View file

@ -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( _ = try await connection.module(.roster).addItem(
jid: JID(roster.contactBareJid), jid: JID(jid),
name: roster.name, name: name,
groups: roster.data.groups groups: groups
) )
} }

View file

@ -26,8 +26,7 @@ struct Credentials: DBStorable, Hashable {
} }
} }
// extension Account: UniversalInputSelectionElement { extension Credentials: UniversalInputSelectionElement {
// var text: String? { bareJid } var text: String? { bareJid }
// var icon: Image? { nil } var icon: Image? { nil }
// } }
//

View file

@ -62,10 +62,11 @@ extension Roster: Equatable {
} }
extension Roster { extension Roster {
static func fetchAll(for jid: String) async throws -> [Roster] { mutating func setLocallyDeleted(_ value: Bool) async throws {
let rosters = try await Database.shared.dbQueue.read { db in locallyDeleted = value
try Roster.filter(Column("bareJid") == jid).fetchAll(db) let copy = self
try? await Database.shared.dbQueue.write { db in
try copy.save(db)
} }
return rosters
} }
} }

View file

@ -5,6 +5,7 @@ import GRDB
@MainActor @MainActor
final class RostersStore: ObservableObject { final class RostersStore: ObservableObject {
@Published private(set) var rosters: [Roster] = [] @Published private(set) var rosters: [Roster] = []
@Published private(set) var locallyDeletedRosters: [Roster] = []
private var cancellable: AnyCancellable? private var cancellable: AnyCancellable?
@ -19,6 +20,7 @@ final class RostersStore: ObservableObject {
.catch { _ in Just([]) } .catch { _ in Just([]) }
cancellable = clientsPublisher cancellable = clientsPublisher
.map { $0.filter { $0.state != .disabled } } // look rosters only for enabled clients
.flatMap { clients in .flatMap { clients in
Publishers.MergeMany(clients.map { $0.$state }) Publishers.MergeMany(clients.map { $0.$state })
.prepend(clients.map { $0.state }) .prepend(clients.map { $0.state })
@ -31,7 +33,80 @@ final class RostersStore: ObservableObject {
} }
private func handleUpdates(clientStates: [ClientState], rosters: [Roster]) { 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)") 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: [])))
// }
}
}

View file

@ -24,8 +24,15 @@
"Tabs.Name.conversations" = "Chats"; "Tabs.Name.conversations" = "Chats";
"Tabs.Name.settings" = "Settings"; "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"; //"Login.Error.serverError" = "Server error. Check internet connection";
// MARK: Contacts screen // MARK: Contacts screen
"Contacts.title" = "Contacts"; //"Contacts.sendMessage" = "Send message";
"Contacts.sendMessage" = "Send message"; //"Contacts.editContact" = "Edit contact";
"Contacts.editContact" = "Edit contact"; //"Contacts.selectContact" = "Select contact";
"Contacts.selectContact" = "Select contact"; //"Contacts.deleteContact" = "Delete contact";
"Contacts.deleteContact" = "Delete contact"; //"Contacts.Delete.title" = "Delete contact";
"Contacts.Add.title" = "Add Contact"; //"Contacts.Delete.message" = "You can delete contact from this device (contact will be available on other devices), or delete it completely";
"Contacts.Add.explanation" = "Contact or group/channel name are usually JID in format name@domain.ltd (like email)"; //"Contacts.Delete.deleteFromDevice" = "Delete from device";
"Contacts.Add.error" = "Contact not added. Server returns error."; //"Contacts.Delete.deleteCompletely" = "Delete completely";
"Contacts.Delete.title" = "Delete contact"; //"Contacts.Delete.error" = "Contact not deleted. Server returns error.";
"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 // MARK: Chats screen
"Chats.title" = "Chats"; //"Chats.title" = "Chats";
"Chat.title" = "Chat"; //"Chat.title" = "Chat";
"Chat.textfieldPrompt" = "Type a message"; //"Chat.textfieldPrompt" = "Type a message";
"Chats.Create.Main.title" = "Create"; //"Chats.Create.Main.title" = "Create";
"Chats.Create.Main.createGroup" = "Create public group"; //"Chats.Create.Main.createGroup" = "Create public group";
"Chats.Create.Main.createPrivateGroup" = "Create private group"; //"Chats.Create.Main.createPrivateGroup" = "Create private group";
"Chats.Create.Main.findGroup" = "Find public group"; //"Chats.Create.Main.findGroup" = "Find public group";
// MARK: Accounts add screen // MARK: Accounts add screen
"Accounts.Add.or" = "or"; //"Accounts.Add.or" = "or";
"Accounts.Add.Exist.title" = "Add existing\naccount"; //"Accounts.Add.Exist.title" = "Add existing\naccount";
"Accounts.Add.Exist.Prompt.jid" = "Enter your XMPP ID"; //"Accounts.Add.Exist.Prompt.jid" = "Enter your XMPP ID";
"Accounts.Add.Exist.Prompt.password" = "Enter password"; //"Accounts.Add.Exist.Prompt.password" = "Enter password";
"Accounts.Add.Exist.Hint.jid" = "user@domain.im"; //"Accounts.Add.Exist.Hint.jid" = "user@domain.im";
"Accounts.Add.Exist.Hint.password" = "password"; //"Accounts.Add.Exist.Hint.password" = "password";
"Accounts.Add.Exist.Btn.link" = "create a new one"; //"Accounts.Add.Exist.Btn.link" = "create a new one";
"Accounts.Add.Exist.Btn.main" = "Continue"; //"Accounts.Add.Exist.Btn.main" = "Continue";
"Accounts.Add.Exist.loginError" = "Wrong login or password"; //"Accounts.Add.Exist.loginError" = "Wrong login or password";
// MARK: Server connecting indicator // MARK: Server connecting indicator
"ServerConnectingIndicator.State.connecting" = "Connecting to server"; //"ServerConnectingIndicator.State.connecting" = "Connecting to server";
"ServerConnectingIndicator.State.connected" = "Connected"; //"ServerConnectingIndicator.State.connected" = "Connected";
"ServerConnectingIndicator.State.error" = "Server unreachable. Check internet connection and server name"; //"ServerConnectingIndicator.State.error" = "Server unreachable. Check internet connection and server name";
// MARK: Attachments // MARK: Attachments
"Attachment.Prompt.main" = "Select attachment"; //"Attachment.Prompt.main" = "Select attachment";
"Attachment.Tab.media" = "Media"; //"Attachment.Tab.media" = "Media";
"Attachment.Tab.files" = "Files"; //"Attachment.Tab.files" = "Files";
"Attachment.Tab.location" = "Location"; //"Attachment.Tab.location" = "Location";
"Attachment.Tab.contacts" = "Contacts"; //"Attachment.Tab.contacts" = "Contacts";
"Attachment.Send.media" = "Send media"; //"Attachment.Send.media" = "Send media";
"Attachment.Send.location" = "Send location"; //"Attachment.Send.location" = "Send location";
"Attachment.Send.contact" = "Send contact"; //"Attachment.Send.contact" = "Send contact";
"Attachment.Downloading.retry" = "Retry"; //"Attachment.Downloading.retry" = "Retry";

View file

@ -2,21 +2,18 @@ import SwiftUI
struct AddContactOrChannelScreen: View { struct AddContactOrChannelScreen: View {
@Environment(\.router) var router @Environment(\.router) var router
@EnvironmentObject var clientsStore: ClientsStore
@EnvironmentObject var rostersStore: RostersStore
// enum Field { enum Field {
// case account case account
// case contact case contact
// } }
//
// @FocusState private var focus: Field? @FocusState private var focus: Field?
//
// @Binding var isPresented: Bool @State private var ownerCredentials: Credentials?
// @State private var contactJID: String = "" @State private var contactJID: String = ""
// // @State private var ownerAccount: Account?
//
// @State private var isShowingLoader = false
// @State private var isShowingAlert = false
// @State private var errorMsg = ""
var body: some View { var body: some View {
ZStack { ZStack {
@ -43,107 +40,117 @@ struct AddContactOrChannelScreen: View {
) )
) )
// VStack(spacing: 16) { // Content
// // Explanation text VStack(spacing: 16) {
// // Explanation text
// Text(L10n.Contacts.Add.explanation) Text(L10n.Contacts.Add.explanation)
// .font(.body3) .font(.body3)
// .foregroundColor(.Material.Shape.separator) .foregroundColor(.Material.Shape.separator)
// .multilineTextAlignment(.center) .multilineTextAlignment(.center)
// .padding(.top, 16) .padding(.top, 16)
//
// // Account selector // Account selector
// HStack(spacing: 0) { HStack(spacing: 0) {
// Text("Use account:") Text("Use account:")
// .font(.body2) .font(.body2)
// .foregroundColor(.Material.Text.main) .foregroundColor(.Material.Text.main)
// .frame(alignment: .leading) .frame(alignment: .leading)
// Spacer() Spacer()
// } }
// // UniversalInputCollection.DropDownMenu( UniversalInputCollection.DropDownMenu(
// // prompt: "Use account", prompt: "Use account",
// // elements: store.state.accountsState.accounts, elements: activeClientsCredentials,
// // selected: $ownerAccount, selected: $ownerCredentials,
// // focus: $focus, focus: $focus,
// // fieldType: .account fieldType: .account
// // ) )
//
// // Contact text input // Contact text input
// HStack(spacing: 0) { HStack(spacing: 0) {
// Text("Contact JID:") Text("Contact JID:")
// .font(.body2) .font(.body2)
// .foregroundColor(.Material.Text.main) .foregroundColor(.Material.Text.main)
// .frame(alignment: .leading) .frame(alignment: .leading)
// Spacer() Spacer()
// } }
// UniversalInputCollection.TextField( UniversalInputCollection.TextField(
// prompt: "Contact or channel JID", prompt: "Contact or channel JID",
// text: $contactJID, text: $contactJID,
// focus: $focus, focus: $focus,
// fieldType: .contact, fieldType: .contact,
// contentType: .emailAddress, contentType: .emailAddress,
// keyboardType: .emailAddress, keyboardType: .emailAddress,
// submitLabel: .done, submitLabel: .done,
// action: { action: {
// focus = .account focus = .account
// } }
// ) )
//
// // Save button // Save button
// Button { Button {
// navigation.flow = .main(.contacts(.list)) Task {
// } label: { await save()
// Text(L10n.Global.save) }
// } } label: {
// .buttonStyle(PrimaryButtonStyle()) Text(L10n.Global.save)
// .disabled(!inputValid) }
// .padding(.top) .buttonStyle(PrimaryButtonStyle())
// Spacer() .disabled(!inputValid)
// } .padding(.top)
Spacer()
}
.padding(.horizontal, 32) .padding(.horizontal, 32)
} }
} }
// .onAppear { .onAppear {
// if let exists = store.state.accountsState.accounts.first, exists.isActive { if let exists = activeClientsCredentials.first {
// ownerAccount = exists ownerCredentials = exists
// } }
// } }
// .loadingIndicator(isShowingLoader) }
// .alert(isPresented: $isShowingAlert) {
// Alert( private var activeClientsCredentials: [Credentials] {
// title: Text(L10n.Global.Error.title), clientsStore.clients
// message: Text(errorMsg), .filter { $0.state != .disabled }
// dismissButton: .default(Text(L10n.Global.ok)) .map { $0.credentials }
// )
// }
// .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
// }
// }
} }
private var inputValid: Bool { private var inputValid: Bool {
true ownerCredentials != nil && !contactJID.isEmpty && UniversalInputCollection.Validators.isEmail(contactJID)
// ownerAccount != nil && !contactJID.isEmpty && UniversalInputCollection.Validators.isEmail(contactJID)
} }
// private func save() { private func save() async {
// guard let ownerAccount else { return } guard let ownerCredentials = ownerCredentials 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))) router.showModal {
// isPresented = false LoadingScreen()
// } else { }
// isShowingLoader = true
// store.dispatch(.rostersAction(.addRoster(ownerJID: ownerAccount.bareJid, contactJID: contactJID, name: nil, groups: []))) 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) {}
}
}
} }

View file

@ -5,11 +5,6 @@ struct ContactsScreen: View {
@EnvironmentObject var clientsStore: ClientsStore @EnvironmentObject var clientsStore: ClientsStore
@StateObject var rostersStore = RostersStore(clientsPublisher: ClientsStore.shared.$clients) @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 { var body: some View {
ZStack { ZStack {
// Background color // Background color
@ -26,6 +21,7 @@ struct ContactsScreen: View {
action: { action: {
router.showScreen(.fullScreenCover) { _ in router.showScreen(.fullScreenCover) { _ in
AddContactOrChannelScreen() AddContactOrChannelScreen()
.environmentObject(rostersStore)
} }
} }
) )
@ -37,9 +33,6 @@ struct ContactsScreen: View {
ForEach(rostersStore.rosters) { roster in ForEach(rostersStore.rosters) { roster in
ContactsScreenRow( ContactsScreenRow(
roster: roster roster: roster
// isErrorAlertPresented: $isErrorAlertPresented,
// errorAlertMessage: $errorAlertMessage,
// isShowingLoader: $isShowingLoader
) )
} }
} }
@ -103,9 +96,9 @@ private struct ContactsScreenRow: View {
iconType: .charCircle(roster.name?.firstLetter ?? roster.contactBareJid.firstLetter), iconType: .charCircle(roster.name?.firstLetter ?? roster.contactBareJid.firstLetter),
text: roster.contactBareJid text: roster.contactBareJid
) )
// .onTapGesture { .onTapGesture {
// store.dispatch(.chatsAction(.startChat(accountJid: roster.bareJid, participantJid: roster.contactBareJid))) // store.dispatch(.chatsAction(.startChat(accountJid: roster.bareJid, participantJid: roster.contactBareJid)))
// } }
// .onLongPressGesture { // .onLongPressGesture {
// isShowingMenu.toggle() // isShowingMenu.toggle()
// } // }