2024-11-18 14:53:52 +00:00
//
// C o n t a c t s V i e w . s w i f t
// M o n a l
//
// C r e a t e d b y M a t t h e w F e n n e l l < m a t t h e w @ f e n n e l l . d e v > o n 1 0 / 0 8 / 2 0 2 4 .
// C o p y r i g h t © 2 0 2 4 m o n a l - i m . o r g . A l l r i g h t s r e s e r v e d .
//
import SwiftUI
struct ContactViewEntry : View {
private let contact : MLContact
@ Binding private var selectedContactForContactDetails : ObservableKVOWrapper < MLContact > ?
2024-11-22 16:34:56 +00:00
private let dismissWithContact : ( MLContact ) -> Void
2024-11-18 14:53:52 +00:00
@ State private var shouldPresentRemoveContactAlert : Bool = false
private var removeContactButtonText : String {
2024-11-22 16:34:56 +00:00
if ! isDeletable {
2024-11-18 14:53:52 +00:00
return " Cannot delete notes to self "
}
return contact . isMuc ? " Remove Conversation " : " Remove Contact "
}
private var removeContactConfirmationTitle : String {
contact . isMuc ? " Leave this converstion? " : " Remove \( contact . contactJid ) from contacts? "
}
private var removeContactConfirmationDetail : String {
contact . isMuc ? " " : " They will no longer see when you are online. They may not be able to access your encryption keys. "
}
private var isDeletable : Bool {
! contact . isSelfChat
}
2024-11-22 16:34:56 +00:00
init ( contact : MLContact , selectedContactForContactDetails : Binding < ObservableKVOWrapper < MLContact > ? > , dismissWithContact : @ escaping ( MLContact ) -> Void ) {
2024-11-18 14:53:52 +00:00
self . contact = contact
2024-11-22 16:34:56 +00:00
_selectedContactForContactDetails = selectedContactForContactDetails
2024-11-18 14:53:52 +00:00
self . dismissWithContact = dismissWithContact
}
var body : some View {
// A p p l e ' s l i s t d i v i d e r s o n l y e x t e n d a s f a r l e f t a s t h e l e f t - m o s t t e x t i n t h e v i e w .
// T h i s m e a n s , b y d e f a u l t , t h a t t h e d i v i d e r s o n t h i s s c r e e n w o u l d n o t e x t e n d a l l t h e w a y t o t h e l e f t o f t h e v i e w .
// T h i s c o m b i n a t i o n o f H S t a c k w i t h s p a c i n g o f 0 , a n d e m p t y t e x t a t t h e l e f t o f t h e v i e w , i s a w o r k a r o u n d t o o v e r r i d e t h i s b e h a v i o u r .
// S e e h t t p s : / / s t a c k o v e r f l o w . c o m / a / 7 6 6 9 8 9 0 9
HStack ( spacing : 0 ) {
Text ( " " ) . frame ( maxWidth : 0 )
Button ( action : { dismissWithContact ( contact ) } ) {
HStack {
ContactEntry ( contact : ObservableKVOWrapper < MLContact > ( contact ) )
Spacer ( )
Button {
selectedContactForContactDetails = ObservableKVOWrapper < MLContact > ( contact )
} label : {
Image ( systemName : " info.circle " )
. imageScale ( . large )
}
. accessibilityLabel ( " Open contact details " )
}
}
}
. swipeActions ( allowsFullSwipe : false ) {
// W e d o n o t u s e a B u t t o n w i t h d e s t r u c t i v e r o l e h e r e a s w e w o u l d l i k e t o d i s p l a y t h e c o n f i r m a t i o n d i a l o g f i r s t .
// A d e s t r u c t i v e r o l e w o u l d d i s m i s s t h e r o w i m m e d i a t e l y , w i t h o u t w a i t i n g f o r t h e c o n f i r m a t i o n .
Button ( removeContactButtonText ) {
shouldPresentRemoveContactAlert = true
}
. tint ( isDeletable ? . red : . gray )
. disabled ( ! isDeletable )
}
. confirmationDialog ( removeContactConfirmationTitle , isPresented : $ shouldPresentRemoveContactAlert , titleVisibility : . visible ) {
Button ( role : . cancel ) { } label : {
Text ( " No " )
}
Button ( role : . destructive ) {
MLXMPPManager . sharedInstance ( ) . remove ( contact )
} label : {
Text ( " Yes " )
}
} message : {
Text ( removeContactConfirmationDetail )
}
}
}
struct ContactsView : View {
@ ObservedObject private var contacts : Contacts
private let delegate : SheetDismisserProtocol
2024-11-22 16:34:56 +00:00
private let dismissWithContact : ( MLContact ) -> Void
2024-11-18 14:53:52 +00:00
@ State private var searchText : String = " "
@ State private var selectedContactForContactDetails : ObservableKVOWrapper < MLContact > ? = nil
2024-11-22 16:34:56 +00:00
init ( contacts : Contacts , delegate : SheetDismisserProtocol , dismissWithContact : @ escaping ( MLContact ) -> Void ) {
2024-11-18 14:53:52 +00:00
self . contacts = contacts
self . delegate = delegate
self . dismissWithContact = dismissWithContact
}
private static func shouldDisplayContact ( contact : MLContact ) -> Bool {
2024-11-22 16:34:56 +00:00
#if IS_QUICKSY
return true
#endif
2024-11-18 14:53:52 +00:00
return contact . isSubscribedTo || contact . hasOutgoingContactRequest || contact . isSubscribedFrom
}
private var contactList : [ MLContact ] {
2024-11-22 16:34:56 +00:00
contacts . contacts
2024-11-18 14:53:52 +00:00
. filter ( ContactsView . shouldDisplayContact )
. sorted { ContactsView . sortingCriteria ( $0 ) < ContactsView . sortingCriteria ( $1 ) }
}
private var searchResults : [ MLContact ] {
if searchText . isEmpty { return contactList }
return contactList . filter { searchMatchesContact ( contact : $0 , search : searchText ) }
}
private static func sortingCriteria ( _ contact : MLContact ) -> ( String , String ) {
2024-11-22 16:34:56 +00:00
( contact . contactDisplayName . lowercased ( ) , contact . contactJid . lowercased ( ) )
2024-11-18 14:53:52 +00:00
}
private func searchMatchesContact ( contact : MLContact , search : String ) -> Bool {
let jid = contact . contactJid . lowercased ( )
let name = contact . contactDisplayName . lowercased ( )
let search = search . lowercased ( )
return jid . contains ( search ) || name . contains ( search )
}
var body : some View {
List {
ForEach ( searchResults , id : \ . self ) { contact in
ContactViewEntry ( contact : contact , selectedContactForContactDetails : $ selectedContactForContactDetails , dismissWithContact : dismissWithContact )
}
}
. animation ( . default , value : contactList )
. navigationTitle ( " Contacts " )
. listStyle ( . plain )
. searchable ( text : $ searchText , placement : . navigationBarDrawer ( displayMode : . always ) )
. autocorrectionDisabled ( )
. textInputAutocapitalization ( . never )
. keyboardType ( . emailAddress )
. overlay {
if contactList . isEmpty {
ContentUnavailableShimView ( " You need friends for this ride " , systemImage : " figure.wave " , description : Text ( " Add new contacts with the + button above. Your friends will pop up here when they can talk " ) )
} else if searchResults . isEmpty {
ContentUnavailableShimView . search
}
}
. toolbar {
ToolbarItemGroup ( placement : . topBarTrailing ) {
NavigationLink ( destination : CreateGroupMenu ( delegate : SheetDismisserProtocol ( ) ) ) {
Image ( systemName : " person.3.fill " )
}
. accessibilityLabel ( " Create contact group " )
NavigationLink ( destination : AddContactMenu ( delegate : SheetDismisserProtocol ( ) , dismissWithNewContact : dismissWithContact ) ) {
Image ( systemName : " person.fill.badge.plus " )
. overlay { NumberlessBadge ( $ contacts . requestCount ) }
}
. accessibilityLabel ( contacts . requestCount > 0 ? " Add contact (contact requests pending) " : " Add New Contact " )
}
}
. sheet ( item : $ selectedContactForContactDetails ) { selectedContact in
2024-11-22 16:34:56 +00:00
AnyView ( AddTopLevelNavigation ( withDelegate : delegate , to : ContactDetails ( delegate : delegate , contact : selectedContact ) ) )
2024-11-18 14:53:52 +00:00
}
}
}
class Contacts : ObservableObject {
@ Published var contacts : Set < MLContact >
@ Published var requestCount : Int
private var subscriptions : Set < AnyCancellable > = Set ( )
init ( ) {
2024-11-22 16:34:56 +00:00
contacts = Set ( DataLayer . sharedInstance ( ) . contactList ( ) )
requestCount = DataLayer . sharedInstance ( ) . allContactRequests ( ) . count
2024-11-18 14:53:52 +00:00
subscriptions = [
NotificationCenter . default . publisher ( for : NSNotification . Name ( " kMonalContactRemoved " ) )
. receive ( on : DispatchQueue . main )
2024-11-22 16:34:56 +00:00
. sink { _ in self . refreshContacts ( ) } ,
2024-11-18 14:53:52 +00:00
NotificationCenter . default . publisher ( for : NSNotification . Name ( " kMonalContactRefresh " ) )
. receive ( on : DispatchQueue . main )
2024-11-22 16:34:56 +00:00
. sink { _ in self . refreshContacts ( ) }
2024-11-18 14:53:52 +00:00
]
}
private func refreshContacts ( ) {
2024-11-22 16:34:56 +00:00
contacts = Set ( DataLayer . sharedInstance ( ) . contactList ( ) )
requestCount = DataLayer . sharedInstance ( ) . allContactRequests ( ) . count
2024-11-18 14:53:52 +00:00
}
}