2024-11-18 14:53:52 +00:00
//
// A d d C o n t a c t M e n u . s w i f t
// M o n a l
//
// C r e a t e d b y J a n o n 2 7 . 1 0 . 2 2 .
// C o p y r i g h t © 2 0 2 2 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 MobileCoreServices
import UniformTypeIdentifiers
struct AddContactMenu : View {
var delegate : SheetDismisserProtocol
2024-11-22 16:34:56 +00:00
private static let jidFaultyPattern = " ^([^@]+@)?.+( \\ ..{2,})?$ "
2024-11-18 14:53:52 +00:00
@ State private var enabledAccounts : [ xmpp ]
@ State private var selectedAccount : Int
2024-11-22 16:34:56 +00:00
@ State private var scannedFingerprints : [ NSNumber : Data ] ? = nil
2024-11-18 14:53:52 +00:00
@ State private var importScannedFingerprints : Bool = false
@ State private var toAdd : String = " "
@ State private var showInvitationError = false
@ State private var showAlert = false
// n o t e : d i s m i s s L a b e l i s n o t a c c e s s e d b u t d e f i n e d a t t h e . a l e r t ( ) s e c t i o n
@ State private var alertPrompt = AlertPrompt ( dismissLabel : Text ( " Close " ) )
2024-11-22 16:34:56 +00:00
@ State private var invitationResult : [ String : AnyObject ] ? = nil
2024-11-18 14:53:52 +00:00
@ StateObject private var overlay = LoadingOverlayState ( )
@ State private var showQRCodeScanner = false
@ State private var success = false
2024-11-22 16:34:56 +00:00
@ State private var newContact : MLContact ?
2024-11-18 14:53:52 +00:00
@ State private var isEditingJid = false
2024-11-22 16:34:56 +00:00
private let dismissWithNewContact : ( MLContact ) -> Void
2024-11-18 14:53:52 +00:00
private let preauthToken : String ?
2024-11-22 16:34:56 +00:00
init ( delegate : SheetDismisserProtocol , dismissWithNewContact : @ escaping ( MLContact ) -> Void , prefillJid : String = " " , preauthToken : String ? = nil , prefillAccount : xmpp ? = nil , omemoFingerprints : [ NSNumber : Data ] ? = nil ) {
2024-11-18 14:53:52 +00:00
self . delegate = delegate
self . dismissWithNewContact = dismissWithNewContact
2024-11-22 16:34:56 +00:00
// s e l f . t o A d d = S t a t e ( w r a p p e d V a l u e : p r e f i l l J i d )
toAdd = prefillJid
2024-11-18 14:53:52 +00:00
self . preauthToken = preauthToken
2024-11-22 16:34:56 +00:00
// o n l y d i s p l a y o m e m o u i p a r t i f t h e r e a r e a n y f i n g e r p r i n t s ( t h e c h e c k s b e l o w t e s t f o r n i l , n o t f o r 0 )
2024-11-18 14:53:52 +00:00
if omemoFingerprints ? . count ? ? 0 > 0 {
2024-11-22 16:34:56 +00:00
scannedFingerprints = omemoFingerprints
2024-11-18 14:53:52 +00:00
}
2024-11-22 16:34:56 +00:00
2024-11-18 14:53:52 +00:00
let enabledAccounts = MLXMPPManager . sharedInstance ( ) . connectedXMPP as ! [ xmpp ]
self . enabledAccounts = enabledAccounts
2024-11-22 16:34:56 +00:00
selectedAccount = enabledAccounts . first != nil ? 0 : - 1
2024-11-18 14:53:52 +00:00
if let prefillAccount = prefillAccount {
for index in enabledAccounts . indices {
2024-11-22 16:34:56 +00:00
if enabledAccounts [ index ] . accountID . isEqual ( to : prefillAccount . accountID ) {
selectedAccount = index
2024-11-18 14:53:52 +00:00
}
}
}
}
2024-11-22 16:34:56 +00:00
// FIXME: d u p l i c a t e c o d e f r o m W e l c o m e L o g I n . s w i f t , m a y b e m o v e t o S w i f t u i H e l p e r s
2024-11-18 14:53:52 +00:00
private var toAddEmptyAlert : Bool {
alertPrompt . title = Text ( " No Empty Values! " )
alertPrompt . message = Text ( " Please make sure you have entered a valid jid. " )
return toAddEmpty
}
private var toAddInvalidAlert : Bool {
alertPrompt . title = Text ( " Invalid Credentials! " )
alertPrompt . message = Text ( " The jid you want to add should be in in the format user@domain.tld. " )
return toAddInvalid
}
2024-11-22 16:34:56 +00:00
2024-11-18 14:53:52 +00:00
private func errorAlert ( title : Text , message : Text = Text ( " " ) ) {
alertPrompt . title = title
alertPrompt . message = message
showAlert = true
}
2024-11-22 16:34:56 +00:00
2024-11-18 14:53:52 +00:00
private func successAlert ( title : Text , message : Text ) {
alertPrompt . title = title
alertPrompt . message = message
2024-11-22 16:34:56 +00:00
success = true // < d i s m i s s e n t i r e v i e w o n c l o s e
2024-11-18 14:53:52 +00:00
showAlert = true
}
2024-11-22 16:34:56 +00:00
2024-11-18 14:53:52 +00:00
private var toAddEmpty : Bool {
2024-11-22 16:34:56 +00:00
toAdd . isEmpty
2024-11-18 14:53:52 +00:00
}
2024-11-22 16:34:56 +00:00
2024-11-18 14:53:52 +00:00
private var toAddInvalid : Bool {
2024-11-22 16:34:56 +00:00
toAdd . range ( of : AddContactMenu . jidFaultyPattern , options : . regularExpression ) = = nil
2024-11-18 14:53:52 +00:00
}
2024-11-22 16:34:56 +00:00
func trustFingerprints ( _ fingerprints : [ NSNumber : Data ] ? , for jid : String , on account : xmpp ) {
// w e d o n ' t u n t r u s t o t h e r d e v i c e s n o t i n c l u d e d i n h e r e , b e c a u s e c o n v e r s a t i o n s o n l y e x p o r t s i t s o w n f i n g e r p r i n t
2024-11-18 14:53:52 +00:00
if let fingerprints = fingerprints {
for ( deviceId , fingerprint ) in fingerprints {
2024-11-22 16:34:56 +00:00
let address = SignalAddress ( name : jid , deviceId : deviceId . int32Value )
let knownDevices = Array ( account . omemo . knownDevices ( forAddressName : jid ) )
2024-11-18 14:53:52 +00:00
if ! knownDevices . contains ( deviceId ) {
2024-11-22 16:34:56 +00:00
account . omemo . addIdentityManually ( address , identityKey : fingerprint )
2024-11-18 14:53:52 +00:00
assert ( account . omemo . getIdentityFor ( address ) = = fingerprint , " The stored and created fingerprint should match " )
}
2024-11-22 16:34:56 +00:00
// t r u s t d e v i c e / f i n g e r p r i n t i f f i n g e r p r i n t s m a t c h
let knownFingerprintHex = HelperTools . signalHexKey ( with : account . omemo . getIdentityFor ( address ) )
let addedFingerprintHex = HelperTools . signalHexKey ( with : fingerprint )
2024-11-18 14:53:52 +00:00
if knownFingerprintHex . uppercased ( ) = = addedFingerprintHex . uppercased ( ) {
2024-11-22 16:34:56 +00:00
account . omemo . updateTrust ( true , for : address )
2024-11-18 14:53:52 +00:00
}
}
}
}
2024-11-22 16:34:56 +00:00
2024-11-18 14:53:52 +00:00
func addJid ( jid : String ) {
2024-11-22 16:34:56 +00:00
let account = enabledAccounts [ selectedAccount ]
2024-11-18 14:53:52 +00:00
let contact = MLContact . createContact ( fromJid : jid , andAccountID : account . accountID )
if contact . isInRoster {
2024-11-22 16:34:56 +00:00
newContact = contact
// i m p o r t o m e m o f i n g e r p r i n t s a s m a n u a l l y t r u s t e d , i f r e q u e s t e d
trustFingerprints ( importScannedFingerprints ? scannedFingerprints : [ : ] , for : jid , on : account )
// o n l y a l e r t o f a l r e a d y k n o w n c o n t a c t i f w e d i d n o t i m p o r t t h e o m e m o f i n g e r p r i n t s
if ! importScannedFingerprints || scannedFingerprints ? . count ? ? 0 = = 0 {
if enabledAccounts . count > 1 {
success = true
2024-11-18 14:53:52 +00:00
successAlert ( title : Text ( " Already present " ) , message : Text ( " This contact is already in the contact list of the selected account " ) )
} else {
2024-11-22 16:34:56 +00:00
success = true
2024-11-18 14:53:52 +00:00
successAlert ( title : Text ( " Already present " ) , message : Text ( " This contact is already in your contact list " ) )
}
}
return
}
2024-11-22 16:34:56 +00:00
showPromisingLoadingOverlay ( overlay , headline : NSLocalizedString ( " Adding... " , comment : " " ) , description : " " ) {
2024-11-18 14:53:52 +00:00
account . checkJidType ( jid )
} . done { type in
let type = type as ! String
if type = = " account " {
let contact = MLContact . createContact ( fromJid : jid , andAccountID : account . accountID )
self . newContact = contact
2024-11-22 16:34:56 +00:00
MLXMPPManager . sharedInstance ( ) . add ( contact , withPreauthToken : preauthToken )
// i m p o r t o m e m o f i n g e r p r i n t s a s m a n u a l l y t r u s t e d , i f r e q u e s t e d
trustFingerprints ( self . importScannedFingerprints ? self . scannedFingerprints : [ : ] , for : jid , on : account )
2024-11-18 14:53:52 +00:00
successAlert ( title : Text ( " Permission Requested " ) , message : Text ( " The new contact will be added to your contacts list when the person you've added has approved your request. " ) )
} else if type = = " muc " {
2024-11-22 16:34:56 +00:00
showPromisingLoadingOverlay ( overlay , headlineView : Text ( " Adding Group/Channel... " ) , descriptionView : Text ( " " ) ) {
promisifyMucAction ( account : account , mucJid : jid ) {
2024-11-18 14:53:52 +00:00
account . joinMuc ( jid )
}
} . done { _ in
self . newContact = MLContact . createContact ( fromJid : jid , andAccountID : account . accountID )
successAlert ( title : Text ( " Success! " ) , message : Text ( " Successfully joined group/channel \( jid ) ! " ) )
} . catch { error in
2024-11-22 16:34:56 +00:00
errorAlert ( title : Text ( " Error entering group/channel! " ) , message : Text ( " \( String ( describing : error ) ) " ) )
2024-11-18 14:53:52 +00:00
}
}
} . catch { error in
errorAlert ( title : Text ( " Error " ) , message : Text ( error . localizedDescription ) )
}
}
var body : some View {
2024-11-22 16:34:56 +00:00
let account = enabledAccounts [ selectedAccount ]
2024-11-18 14:53:52 +00:00
let splitJid = HelperTools . splitJid ( account . connectionProperties . identity . jid )
Form {
if enabledAccounts . isEmpty {
Text ( " Please make sure at least one account has connected before trying to add a contact or channel. " )
. foregroundColor ( . secondary )
2024-11-22 16:34:56 +00:00
} else {
if ! DataLayer . sharedInstance ( ) . allContactRequests ( ) . isEmpty {
2024-11-18 14:53:52 +00:00
ContactRequestsMenu ( )
}
2024-11-22 16:34:56 +00:00
Section ( header : Text ( " Contact and Group/Channel Jids are usually in the format: name@domain.tld " ) ) {
2024-11-18 14:53:52 +00:00
if enabledAccounts . count > 1 {
Picker ( " Use account " , selection : $ selectedAccount ) {
ForEach ( Array ( self . enabledAccounts . enumerated ( ) ) , id : \ . element ) { idx , account in
Text ( account . connectionProperties . identity . jid ) . tag ( idx )
}
}
. pickerStyle ( . menu )
}
TextField ( NSLocalizedString ( " Contact-, Group- or Channel-Jid " , comment : " placeholder when adding jid " ) , text : $ toAdd , onEditingChanged : { isEditingJid = $0 } )
. textInputAutocapitalization ( . never )
. autocapitalization ( . none )
. autocorrectionDisabled ( )
. keyboardType ( . emailAddress )
2024-11-22 16:34:56 +00:00
. addClearButton ( isEditing : isEditingJid , text : $ toAdd )
2024-11-18 14:53:52 +00:00
. disabled ( scannedFingerprints != nil )
. foregroundColor ( scannedFingerprints != nil ? . secondary : . primary )
. onChange ( of : toAdd ) { _ in toAdd = toAdd . replacingOccurrences ( of : " " , with : " " ) }
2024-11-22 16:34:56 +00:00
if scannedFingerprints != nil && ! scannedFingerprints ! . isEmpty {
2024-11-18 14:53:52 +00:00
Section ( header : Text ( " A contact was scanned through the QR code scanner " ) ) {
Toggle ( isOn : $ importScannedFingerprints ) {
Text ( " Import and trust OMEMO fingerprints from QR code " )
}
}
}
2024-11-22 16:34:56 +00:00
2024-11-18 14:53:52 +00:00
if scannedFingerprints != nil {
Button ( action : {
toAdd = " "
importScannedFingerprints = true
scannedFingerprints = nil
} , label : {
Text ( " Clear scanned contact " )
. foregroundColor ( . red )
} )
}
2024-11-22 16:34:56 +00:00
2024-11-18 14:53:52 +00:00
HStack {
Spacer ( )
2024-11-22 16:34:56 +00:00
2024-11-18 14:53:52 +00:00
Button ( action : {
showAlert = toAddEmptyAlert || toAddInvalidAlert
if ! showAlert {
let jidComponents = HelperTools . splitJid ( toAdd )
if jidComponents [ " host " ] = = nil || jidComponents [ " host " ] ! . isEmpty {
errorAlert ( title : Text ( " Error " ) , message : Text ( " Something went wrong while parsing your input... " ) )
showAlert = true
return
}
// u s e t h e c a n o n i z e d j i d f r o m n o w o n ( l o w e r c a s e d , r e s o u r c e r e m o v e d e t c . )
addJid ( jid : jidComponents [ " user " ] ! )
}
} ) {
scannedFingerprints = = nil ? Text ( " Add " ) : Text ( " Add scanned contact " )
}
. disabled ( toAddEmpty || toAddInvalid )
. buttonStyle ( MonalProminentButtonStyle ( ) )
}
}
2024-11-22 16:34:56 +00:00
if DataLayer . sharedInstance ( ) . allContactRequests ( ) . isEmpty {
2024-11-18 14:53:52 +00:00
Section {
ContactRequestsMenu ( )
}
}
}
}
. padding ( )
. alert ( isPresented : $ showAlert ) {
2024-11-22 16:34:56 +00:00
Alert ( title : alertPrompt . title , message : alertPrompt . message , dismissButton : . default ( Text ( " Close " ) , action : {
2024-11-18 14:53:52 +00:00
showAlert = false
if self . success = = true {
if self . newContact != nil {
self . dismissWithNewContact ( newContact ! )
} else {
self . delegate . dismiss ( )
}
}
} ) )
}
2024-11-22 16:34:56 +00:00
. richAlert ( isPresented : $ invitationResult , title : Text ( " Invitation for \( splitJid [ " host " ] ! ) created " ) ) { data in
2024-11-18 14:53:52 +00:00
VStack {
Image ( uiImage : createQrCode ( value : data [ " landing " ] as ! String ) )
. interpolation ( . none )
. resizable ( )
. scaledToFit ( )
. aspectRatio ( 1 , contentMode : . fit )
if let expires = data [ " expires " ] as ? Date {
2024-11-22 16:34:56 +00:00
Text ( " This invitation will expire on \( expires . formatted ( date : . numeric , time : . shortened ) ) " )
. font ( . footnote )
. multilineTextAlignment ( . leading )
. frame ( maxWidth : . infinity , alignment : . leading )
2024-11-18 14:53:52 +00:00
}
}
2024-11-22 16:34:56 +00:00
} buttons : { data in
2024-11-18 14:53:52 +00:00
Button ( action : {
2024-11-22 16:34:56 +00:00
UIPasteboard . general . setValue ( data [ " landing " ] as ! String , forPasteboardType : UTType . utf8PlainText . identifier as String )
2024-11-18 14:53:52 +00:00
invitationResult = nil
} ) {
ShareLink ( " Share invitation link " , item : URL ( string : data [ " landing " ] as ! String ) ! )
}
Button ( action : {
invitationResult = nil
} ) {
Text ( " Close " )
. frame ( maxWidth : . infinity )
}
}
. sheet ( isPresented : $ showQRCodeScanner ) {
NavigationStack {
MLQRCodeScanner ( handleClose : {
self . showQRCodeScanner = false
} )
. navigationTitle ( " QR-Code Scanner " )
. navigationBarTitleDisplayMode ( . inline )
. toolbar ( content : {
ToolbarItem ( placement : . navigationBarLeading , content : {
Button ( action : {
self . showQRCodeScanner = false
} , label : {
Text ( " Close " )
} )
} )
} )
}
}
. navigationBarTitle ( Text ( " Add Contact or Channel " ) , displayMode : . inline )
. toolbar ( content : {
ToolbarItemGroup ( placement : . navigationBarTrailing ) {
if account . connectionProperties . discoveredAdhocCommands [ " urn:xmpp:invite#invite " ] != nil {
Button ( action : {
2024-11-22 16:34:56 +00:00
DDLogVerbose ( " Trying to create invitation for: \( String ( describing : splitJid [ " host " ] ! ) ) " )
2024-11-18 14:53:52 +00:00
showLoadingOverlay ( overlay , headline : NSLocalizedString ( " Creating invitation... " , comment : " " ) )
account . createInvitation ( completion : {
2024-11-22 16:34:56 +00:00
let result = $0 as ! [ String : AnyObject ]
2024-11-18 14:53:52 +00:00
DispatchQueue . main . async {
hideLoadingOverlay ( overlay )
2024-11-22 16:34:56 +00:00
DDLogVerbose ( " Got invitation result: \( String ( describing : result ) ) " )
2024-11-18 14:53:52 +00:00
if result [ " success " ] as ! Bool = = true {
invitationResult = result
} else {
2024-11-22 16:34:56 +00:00
errorAlert ( title : Text ( " Failed to create invitation for \( splitJid [ " host " ] ! ) " ) , message : Text ( result [ " error " ] as ! String ) )
2024-11-18 14:53:52 +00:00
}
}
} )
} , label : {
Image ( systemName : " square.and.arrow.up " )
} )
}
Button ( action : {
self . showQRCodeScanner = true
} , label : {
Image ( systemName : " camera.fill " )
} )
}
} )
. addLoadingOverlay ( overlay )
}
}
struct AddContactMenu_Previews : PreviewProvider {
static var delegate = SheetDismisserProtocol ( )
static var previews : some View {
2024-11-22 16:34:56 +00:00
AddContactMenu ( delegate : delegate , dismissWithNewContact : { _ in
2024-11-18 14:53:52 +00:00
} )
}
}