import SwiftUI struct ContactsScreen: View { @EnvironmentObject var store: AppStore @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 Color.Material.Background.light .ignoresSafeArea() // Content VStack(spacing: 0) { // Header ContactsScreenHeader(addPanelPresented: $addPanelPresented) // Contacts list let rosters = store.state.rostersState.rosters.filter { !$0.locallyDeleted } if !rosters.isEmpty { List { ForEach(rosters) { roster in ContactsScreenRow( roster: roster, isErrorAlertPresented: $isErrorAlertPresented, errorAlertMessage: $errorAlertMessage, isShowingLoader: $isShowingLoader ) } } .listStyle(.plain) .background(Color.Material.Background.light) } else { Spacer() } // Tab bar SharedTabBar() } } .loadingIndicator(isShowingLoader) .fullScreenCover(isPresented: $addPanelPresented) { AddContactOrChannelScreen(isPresented: $addPanelPresented) } .alert(isPresented: $isErrorAlertPresented) { Alert( title: Text(L10n.Global.Error.title), message: Text(errorAlertMessage), dismissButton: .default(Text(L10n.Global.ok)) ) } } } private struct ContactsScreenHeader: View { @Binding var addPanelPresented: Bool var body: some View { ZStack { // bg Color.Material.Background.dark .ignoresSafeArea() // title Text(L10n.Contacts.title) .font(.head2) .foregroundColor(Color.Material.Text.main) HStack { Spacer() Image(systemName: "plus") .foregroundColor(Color.Material.Elements.active) .tappablePadding(.symmetric(12)) { addPanelPresented = true } } .padding(.horizontal, 16) } .frame(height: 44) } } private struct ContactsScreenRow: View { @EnvironmentObject var store: AppStore var roster: Roster @State private var isShowingMenu = false @State private var isDeleteAlertPresented = false @Binding var isErrorAlertPresented: Bool @Binding var errorAlertMessage: String @Binding var isShowingLoader: Bool var body: some View { VStack(spacing: 0) { HStack(spacing: 8) { ZStack { Circle() .frame(width: 44, height: 44) .foregroundColor(.red) Text(roster.name?.firstLetter ?? roster.contactBareJid.firstLetter) .foregroundColor(.white) .font(.body1) } Text(roster.contactBareJid) .foregroundColor(Color.Material.Text.main) .font(.body2) Spacer() } .padding(.horizontal, 16) .padding(.vertical, 4) Rectangle() .frame(maxWidth: .infinity) .frame(height: 1) .foregroundColor(.Material.Background.dark) } .sharedListRow() .onTapGesture { store.dispatch(.chatsAction(.startChat(accountJid: roster.bareJid, participantJid: roster.contactBareJid))) } .onLongPressGesture { isShowingMenu.toggle() } .swipeActions(edge: .trailing, allowsFullSwipe: false) { Button { isDeleteAlertPresented = true } label: { Label(L10n.Contacts.sendMessage, systemImage: "trash") } .tint(Color.red) } .contextMenu { Button(L10n.Contacts.sendMessage, systemImage: "message") { store.dispatch(.chatsAction(.startChat(accountJid: roster.bareJid, participantJid: roster.contactBareJid))) } Divider() Button(L10n.Contacts.editContact) { print("Edit contact") } Button(L10n.Contacts.selectContact) { print("Select contact") } Divider() Button(L10n.Contacts.deleteContact, systemImage: "trash", role: .destructive) { isDeleteAlertPresented = true } } .actionSheet(isPresented: $isDeleteAlertPresented) { ActionSheet( title: Text(L10n.Contacts.Delete.title), message: Text(L10n.Contacts.Delete.message), buttons: [ .destructive(Text(L10n.Contacts.Delete.deleteFromDevice)) { store.dispatch(.rostersAction(.markRosterAsLocallyDeleted(ownerJID: roster.bareJid, contactJID: roster.contactBareJid))) }, .destructive(Text(L10n.Contacts.Delete.deleteCompletely)) { isShowingLoader = true store.dispatch(.rostersAction(.deleteRoster(ownerJID: roster.bareJid, contactJID: roster.contactBareJid))) }, .cancel(Text(L10n.Global.cancel)) ] ) } .onChange(of: store.state.rostersState.rosters) { _ in endOfDeleting() } .onChange(of: store.state.rostersState.deleteRosterError) { _ in endOfDeleting() } } private func endOfDeleting() { if isShowingLoader { isShowingLoader = false if let error = store.state.rostersState.deleteRosterError { errorAlertMessage = error isErrorAlertPresented = true } } } }