mv-experiment #1
|
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
return rosters
|
||||
mutating func setLocallyDeleted(_ value: Bool) async throws {
|
||||
locallyDeleted = value
|
||||
let copy = self
|
||||
try? await Database.shared.dbQueue.write { db in
|
||||
try copy.save(db)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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: [])))
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
.onTapGesture {
|
||||
// store.dispatch(.chatsAction(.startChat(accountJid: roster.bareJid, participantJid: roster.contactBareJid)))
|
||||
// }
|
||||
}
|
||||
// .onLongPressGesture {
|
||||
// isShowingMenu.toggle()
|
||||
// }
|
||||
|
|
Loading…
Reference in a new issue