import SwiftUI struct ContactsScreen: View { @Environment(\.router) var router @EnvironmentObject var clientsStore: ClientsStore var body: some View { ZStack { // Background color Color.Material.Background.light .ignoresSafeArea() // Content VStack(spacing: 0) { // Header SharedNavigationBar( centerText: .init(text: L10n.Contacts.title), rightButton: .init( image: Image(systemName: "plus"), action: { router.showScreen(.fullScreenCover) { _ in AddContactOrChannelScreen() } } ) ) // Contacts list if !clientsStore.actualRosters.isEmpty { List { ForEach(elements.indices, id: \.self) { index in let element = elements[index] if let roster = element as? Roster { ContactsScreenRow( roster: roster ) } else if let bareJid = element as? String { SharedSectionTitle(text: bareJid) } } } .listStyle(.plain) .background(Color.Material.Background.light) } else { Spacer() } } } } private var elements: [Any] { if clientsStore.clients.filter({ $0.credentials.isActive }).count == 1 { return clientsStore.actualRosters } else { var result: [Any] = [] for roster in clientsStore.actualRosters { if result.isEmpty { result.append(roster.bareJid) } else if let last = result.last as? Roster, last.bareJid != roster.bareJid { result.append(roster.bareJid) } result.append(roster) } return result } } } private struct ContactsScreenRow: View { @Environment(\.router) var router @EnvironmentObject var clientsStore: ClientsStore var roster: Roster var body: some View { SharedListRow( iconType: .charCircle(roster.name?.firstLetter ?? roster.contactBareJid.firstLetter), text: roster.contactBareJid, controlType: .none ) .onTapGesture { startChat() } .swipeActions(edge: .trailing, allowsFullSwipe: false) { Button { router.showAlert(.confirmationDialog, title: L10n.Contacts.deleteContact, subtitle: L10n.Contacts.Delete.message) { deleteConfirmation } } label: { Label("", systemImage: "trash") } .tint(Color.red) } .contextMenu { Button(L10n.Contacts.sendMessage, systemImage: "message") { startChat() } Divider() Button(L10n.Contacts.editContact) { print("Edit contact") } Button(L10n.Contacts.selectContact) { print("Select contact") } Divider() Button(L10n.Contacts.deleteContact, systemImage: "trash", role: .destructive) { router.showAlert(.confirmationDialog, title: L10n.Contacts.deleteContact, subtitle: L10n.Contacts.Delete.message) { deleteConfirmation } } } } @ViewBuilder private var deleteConfirmation: some View { Button(role: .destructive) { Task { await deleteFromDevice() } } label: { Text(L10n.Contacts.Delete.deleteFromDevice) } Button(role: .destructive) { Task { await deleteCompletely() } } label: { Text(L10n.Contacts.Delete.deleteCompletely) } Button(role: .cancel) {} label: { Text(L10n.Global.cancel) } } private func deleteFromDevice() async { router.showModal { LoadingScreen() } defer { router.dismissModal() } var roster = roster try? await roster.setLocallyDeleted(true) } private func deleteCompletely() async { router.showModal { LoadingScreen() } defer { router.dismissModal() } do { try await clientsStore.deleteRoster(roster) } catch { router.showAlert( .alert, title: L10n.Global.Error.title, subtitle: L10n.Contacts.Delete.error ) { Button(L10n.Global.ok, role: .cancel) {} } } } private func startChat() { Task { router.showModal { LoadingScreen() } defer { router.dismissModal() } do { let (messages, attachments, settings) = try await clientsStore.conversationStores(for: roster) router.showScreen(.push) { _ in ConversationScreen(messagesStore: messages, attachments: attachments, settings: settings) .navigationBarHidden(true) } } catch { router.showAlert( .alert, title: L10n.Global.Error.title, subtitle: L10n.Conversation.startError ) { Button(L10n.Global.ok, role: .cancel) {} } } } } }