diff --git a/Monal/Classes/AccountSettings.swift b/Monal/Classes/AccountSettings.swift new file mode 100644 index 0000000000..d3f1d07c48 --- /dev/null +++ b/Monal/Classes/AccountSettings.swift @@ -0,0 +1,425 @@ +// +// AccountSettings.swift +// Monal +// +// Created by lissine on 2/11/2024. +// Copyright © 2024 monal-im.org. All rights reserved. +// + +class AccountSettingsModel: ObservableObject { + let jid: String + @Published var rosterName: String + @Published var statusMessage: String + @Published var avatar: UIImage? + @Published var account: xmpp? + @Published var accountEnabled: Bool + + init(accountID: NSNumber?) { + guard accountID != nil, + let settings = DataLayer.sharedInstance().details(forAccount: accountID!) + else { + self.jid = "" + self.rosterName = "" + self.statusMessage = "" + self.account = nil + self.avatar = nil + self.accountEnabled = false + return + } + self.jid = "\(settings["username"]!)@\(settings["domain"]!)" + self.rosterName = settings["rosterName"] as? String ?? "" + self.statusMessage = settings["statusMessage"] as? String ?? "" + self.avatar = MLImageManager.sharedInstance().getIconFor(MLContact.createContact(fromJid: self.jid, andAccountID: accountID!)) + self.account = MLXMPPManager.sharedInstance().getEnabledAccount(forID: accountID!) + self.accountEnabled = settings["enabled"] as? Bool ?? false + } +} +struct AccountSettings: View { + let accountID: NSNumber? + var delegate: SheetDismisserProtocol + + @ObservedObject var model: AccountSettingsModel + + @State private var showingClearHistoryConfirmation = false + @State private var showingRemoveAccountConfirmation = false + @State private var showingDeleteAccountConfirmation = false + @State private var showingRemoveAvatarConfirmation = false + + @State private var inputImage: UIImage? + @State private var showingImagePicker = false + + @State private var showAlert = false + @State private var alertPrompt = AlertPrompt(dismissLabel: Text("Close")) + + @StateObject private var overlay = LoadingOverlayState() + + init(accountID: NSNumber?, delegate: SheetDismisserProtocol) { + self.accountID = accountID + self.delegate = delegate + self.model = AccountSettingsModel(accountID: accountID) + } + + private var ownContact: MLContact { + return MLContact.createContact(fromJid: self.model.jid, andAccountID: self.accountID!) + } + + // This function is called after removing / deleting an account + private func handlePostAccountRemoval(executionStartTime: Double) { + DispatchQueue.main.asyncAfter(deadline: .now() + executionStartTime) { + delegate.dismiss() + + // We want to start fresh instead of doing a "password migration" - restore directly triggering an SMS + HelperTools.defaultsDB().removeObject(forKey: "Quicksy_phoneNumber") + HelperTools.defaultsDB().removeObject(forKey: "Quicksy_country") + + // Make sure we show account creation view etc. after removing the last account + guard let appDelegate = UIApplication.shared.delegate as? MonalAppDelegate, + let activeChats = appDelegate.activeChats else { + return + } + activeChats.segueToIntroScreensIfNeeded() + } + } + + // TODO: move to a separate file AvatarPicker.swift along with the rest of the AvatarPicking + private func showImagePicker() { +#if targetEnvironment(macCatalyst) + let picker = DocumentPickerViewController( + supportedTypes: [UTType.image], + onPick: { url in + if let imageData = try? Data(contentsOf: url) { + if let loadedImage = UIImage(data: imageData) { + self.inputImage = loadedImage + } + } + }, + onDismiss: { + // do nothing on dismiss + } + ) + UIApplication.shared.windows.first?.rootViewController?.present(picker, animated: true) +#else + showingImagePicker = true +#endif + } + + var body: some View { + Form { + Section(header: Text("")) { + VStack { + Image(uiImage: model.avatar ?? UIImage(named: "noicon")!) + .resizable() + .scaledToFit() + // .clipShape(Circle()) + .onTapGesture { + showImagePicker() + } + + .addTopRight { + Button(action: { + showImagePicker() + }, label: { + Image(systemName: "pencil.circle.fill") + .resizable() + .frame(width: 24.0, height: 24.0) + .accessibilityLabel(Text("Change Avatar")) + }) + .buttonStyle(.borderless) + .offset(x: 8, y: -8) + } + .addTopLeft { + if MLImageManager.sharedInstance().hasIcon(for: self.ownContact) { + Button(action: { + showingRemoveAvatarConfirmation = true + }, label: { + Image(systemName: "xmark.circle.fill") + .resizable() + .frame(width: 24.0, height: 24.0) + .accessibilityLabel(Text("Remove Avatar")) + .symbolRenderingMode(.palette) + .foregroundStyle(.white, .red) + }) + .buttonStyle(.borderless) + .offset(x: -8, y: -8) + } + } + .frame(width: 100, height: 100) + + .shadow(radius: 7) + .confirmationDialog( + "Really remove avatar?", + isPresented: $showingRemoveAvatarConfirmation, + actions: { + Button("Yes", role: .destructive) { + showLoadingOverlay(overlay, headlineView: Text("Removing avatar..."), descriptionView: Text("")) + model.account?.publishAvatar(nil) + } + }, + message: { + Text("This will remove the current avatar image and revert this account to the default one.") + } + ) + + .sheet(isPresented: $showingImagePicker) { + ImagePicker(image: $inputImage) + } + .sheet(isPresented: $inputImage.optionalMappedToBool()) { + ImageCropView(originalImage: inputImage!, configureBlock: { cropViewController in + cropViewController.aspectRatioPreset = .presetSquare + cropViewController.aspectRatioLockEnabled = true + cropViewController.aspectRatioPickerButtonHidden = true + cropViewController.resetAspectRatioEnabled = false + }, onCanceled: { + inputImage = nil + }) { (image, cropRect, angle) in + showLoadingOverlay(overlay, headlineView: Text("Publishing avatar..."), descriptionView: Text("")) + model.account?.publishAvatar(image) + } + } + + } + .frame(maxWidth: .infinity) + .listRowBackground(Color(UIColor.systemGroupedBackground)) + } + + Section(header: Text("Account (\(self.model.jid))")) { + Toggle(isOn: $model.accountEnabled) { + Text("Enabled") + } + .onChange(of: model.accountEnabled) { _ in + guard self.accountID != nil else { return } + if model.accountEnabled { + // Update the enabled status in the DB + DDLogVerbose("Enabling and connecting to account \(model.jid)...") + DataLayer.sharedInstance().enableAccount(forAccountID: self.accountID!) + // Connect: + MLXMPPManager.sharedInstance().connectAccount(self.accountID!) + } else { + // Disconnect account before disabling it in db, to avoid assertions when trying to create MLContact instances + // for the disabled account (for notifications etc.) + + DDLogVerbose("Disconnecting and disabling account \(model.jid)...") + // Disconnect + MLXMPPManager.sharedInstance().disconnectAccount(self.accountID!, withExplicitLogout: true) + + // Delete all SiriKit interactions + HelperTools.removeAllShareInteractions(forAccountID: self.accountID!) + + // Update the enabled status in the DB + DataLayer.sharedInstance().disableAccount(forAccountID: self.accountID!) + } + + // Update model.account to ensure we have a consistent state. Otherwise there would be problems with the General Section. + // NOTE: this makes an account disable / enable trigger two view refreshes, due to the two changes in the ObservableObject not happening at the exact same time + // maybe this entire .onChange can be handled inside the ObservableObject itself + model.account = MLXMPPManager.sharedInstance().getEnabledAccount(forID: accountID!) + + // trigger view updates to make sure enabled/disabled account state propagates to all ui elements + //TODO: replace with MLNotificationQueue.currentQueue.post, and consider moving it to inside DataLayer.disableAccount and DataLayer.enableAccount. Though note that the DataLayer doesn't currently handle any notifications. + NotificationCenter.default.post(name: NSNotification.Name("kMonalRefresh"), object: nil) + } + + HStack { + Text("Display Name") + Spacer() + TextField("", text: $model.rosterName) + .onSubmit { + showLoadingOverlay(overlay, headlineView: Text("Updating display name..."), descriptionView: Text("")) + model.account?.publishRosterName(model.rosterName) + } + } + HStack { + Text("Status Message") + Spacer() + TextField("Your status", text: $model.statusMessage) + .onSubmit { + showLoadingOverlay(overlay, headlineView: Text("Updating status message..."), descriptionView: Text("")) + model.account?.publishStatusMessage(model.statusMessage) + } + } + + NavigationLink(destination: LazyClosureView(LoginCredentials(accountID: self.accountID))) { + Text("Login Credentials") + } + + } + .multilineTextAlignment(.trailing) + + + Section(header: Text("General")) { + NavigationLink { + if model.account != nil { + LazyClosureView(ServerDetails(xmppAccount: model.account!)) + } else { + ContentUnavailableShimView("Account Disabled", systemImage: "iphone.homebutton.slash", description: Text("Cannot display server information as the account is disabled.")) + } + } label: { + HStack { + Text("Server Information") + Spacer() + Image(systemName: "info.circle") + .foregroundStyle(Color.accentColor) + } + } + + NavigationLink { + if model.account != nil { + LazyClosureView(EmptyView()) + } else { + ContentUnavailableShimView("Account Disabled", systemImage: "iphone.homebutton.slash", description: Text("Cannot change the password as the account is disabled.")) + } + } label: { + Text("Change Password") + } + + NavigationLink { + if accountID != nil { + LazyClosureView( + OmemoKeysView(omemoKeys: OmemoKeysForChat(viewContact: ObservableKVOWrapper(self.ownContact))) + ) + } else { + ContentUnavailableShimView("Non-existing Account", systemImage: "iphone.homebutton.slash", description: Text("Cannot display keys as the account doesn't exist.")) + } + } label: { + Text("Encryption Keys (OMEMO)") + } + + NavigationLink { + if model.account != nil { + LazyClosureView(BlockedUsers(xmppAccount: model.account!)) + } else { + ContentUnavailableShimView("Account Disabled", systemImage: "iphone.homebutton.slash", description: Text("Cannot display blocked addresses as the account is disabled.")) + } + } label: { + Text("Blocked Users") + } + } + + Section { + Button(role: .destructive, action: {showingClearHistoryConfirmation = true}) { + Text("Clear Chat History") + .frame(maxWidth: .infinity, alignment: .center) + } + .confirmationDialog( + "Clear Chat History", + isPresented: $showingClearHistoryConfirmation, + actions: { + Button("Confirm", role: .destructive) { + // Handle the clearing of chat history + guard accountID != nil else { return } + DataLayer.sharedInstance().clearMessages(self.accountID!) + + // TODO: replace with MLNotificationQueue.currentQueue.post, and consider moving it inside DataLayer.clearMessages. Though note that the DataLayer doesn't currently handle any notifications. + NotificationCenter.default.post(name: NSNotification.Name("kMonalRefresh"), object: nil) + + // TODO: show an indicator of success. + } + }, + message: { + Text("This will clear the whole chat history of this account from this device.") + } + ) + + Button(role: .destructive, action: {showingRemoveAccountConfirmation = true}) { + Text("Remove account from this device") + .frame(maxWidth: .infinity, alignment: .center) + } + .confirmationDialog( + "Remove Account", + isPresented: $showingRemoveAccountConfirmation, + actions: { + Button("Confirm", role: .destructive) { + // Handle the removal of the account + guard accountID != nil else { return } + DDLogVerbose("Removing accountID \(self.accountID)") + MLXMPPManager.sharedInstance().removeAccount(forAccountID: self.accountID!) + // TODO: add something to show success to the user + + handlePostAccountRemoval(executionStartTime: 0.3) + } + }, + message: { + Text("This will remove this account and the associated data from this device.") + } + ) + + Button( + role: .destructive, + action: { + guard let accountState = model.account?.accountState.rawValue, + accountState >= xmppState.stateBound.rawValue else { + alertPrompt.title = Text("Error Deleting Account") + alertPrompt.message = Text("Your account must be enabled and connected, to be deleted from the server!") + showAlert = true + return + } + showingDeleteAccountConfirmation = true + }, + label: { + Text("Delete Account on server") + .frame(maxWidth: .infinity, alignment: .center) + } + ) + .confirmationDialog( + "Delete Account", + isPresented: $showingDeleteAccountConfirmation, + actions: { + Button("Confirm", role: .destructive) { + // Handle the deletion of the account + DDLogVerbose("Deleting account on server: \(model.account)") + model.account?.removeFromServer { error in + DispatchQueue.main.async { + if (error != nil) { + alertPrompt.title = Text("Error Deleting Account") + alertPrompt.message = Text(error!) + showAlert = true + } else { + // TODO: something to indicate success + + handlePostAccountRemoval(executionStartTime: 0.3) + } + } + } + } + }, + message: { + Text("This will delete this account and the associated data from the server and this device. Data might still be retained on other devices, though.") + } + ) + + } + .alignmentGuide(.listRowSeparatorLeading) { _ in + return 0 + } + + } + .alert( + alertPrompt.title, + isPresented: $showAlert, + actions: { Button("Close"){} }, + message: { alertPrompt.message } + ) + .navigationTitle("Account Settings") + .navigationBarTitleDisplayMode(.inline) + .addLoadingOverlay(overlay) + .onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("kMonalAccountSettingsRefresh")).receive(on: RunLoop.main)) { notification in + guard let notificationAccountID = notification.userInfo?["accountID"] as? NSNumber, + notificationAccountID.intValue == model.account?.accountID.intValue else { + return + } + + DispatchQueue.main.async { + // Reload roster name, status message and avatar from the DB + if let settings = DataLayer.sharedInstance().details(forAccount: self.accountID!) { + model.rosterName = settings["rosterName"] as? String ?? "" + model.statusMessage = settings["statusMessage"] as? String ?? "" + model.avatar = MLImageManager.sharedInstance().getIconFor(self.ownContact) + } + + DDLogVerbose("Got server-side account-settings (display name, status message and avatar) update from account \(model.account!)...") + hideLoadingOverlay(overlay) + } + } + + } +} diff --git a/Monal/Classes/DataLayer.h b/Monal/Classes/DataLayer.h index c2670ca1d5..769ba52632 100644 --- a/Monal/Classes/DataLayer.h +++ b/Monal/Classes/DataLayer.h @@ -150,6 +150,8 @@ extern NSString* const kMessageTypeFiletransfer; -(NSNumber*) enabledAccountCnts; -(NSArray*) enabledAccountList; -(BOOL) isAccountEnabled:(NSNumber*) accountID; +-(BOOL) disableAccountForAccountID:(NSNumber*) accountID; +-(BOOL) enableAccountForAccountID:(NSNumber*) accountID; -(BOOL) doesAccountExistUser:(NSString*) user andDomain:(NSString *) domain; -(NSNumber* _Nullable) accountIDForUser:(NSString*) user andDomain:(NSString *) domain; diff --git a/Monal/Classes/DataLayer.m b/Monal/Classes/DataLayer.m index 1b6cc9270d..5d3bf71797 100644 --- a/Monal/Classes/DataLayer.m +++ b/Monal/Classes/DataLayer.m @@ -191,6 +191,20 @@ -(BOOL) isAccountEnabled:(NSNumber*) accountID }]; } +-(BOOL) disableAccountForAccountID:(NSNumber*) accountID +{ + return [self.db boolReadTransaction:^{ + return [self.db executeNonQuery:@"UPDATE account SET enabled=0 WHERE account_id=?;" andArguments:@[accountID]]; + }]; +} + +-(BOOL) enableAccountForAccountID:(NSNumber*) accountID +{ + return [self.db boolReadTransaction:^{ + return [self.db executeNonQuery:@"UPDATE account SET enabled=1 WHERE account_id=?;" andArguments:@[accountID]]; + }]; +} + -(NSNumber*) accountIDForUser:(NSString*) user andDomain:(NSString*) domain { if(!user && !domain) diff --git a/Monal/Classes/GeneralSettings.swift b/Monal/Classes/GeneralSettings.swift index d489fda43b..652acdcc93 100644 --- a/Monal/Classes/GeneralSettings.swift +++ b/Monal/Classes/GeneralSettings.swift @@ -265,8 +265,8 @@ struct SecuritySettings: View { 259200: NSLocalizedString("3 days", comment:"Message autdelete time"), 604800: NSLocalizedString("1 week", comment:"Message autdelete time"), 2419200: NSLocalizedString("4 weeks", comment:"Message autdelete time"), - 5184000: NSLocalizedString("2 month", comment:"Message autdelete time"), //based on 30 days per month - 7776000: NSLocalizedString("3 month", comment:"Message autdelete time"), //based on 30 days per month + 5184000: NSLocalizedString("2 months", comment:"Message autdelete time"), //based on 30 days per month + 7776000: NSLocalizedString("3 months", comment:"Message autdelete time"), //based on 30 days per month ] init() { diff --git a/Monal/Classes/LoginCredentials.swift b/Monal/Classes/LoginCredentials.swift new file mode 100644 index 0000000000..0e52c031c6 --- /dev/null +++ b/Monal/Classes/LoginCredentials.swift @@ -0,0 +1,148 @@ +// +// LoginCredentials.swift +// Monal +// +// Created by lissine on 2/11/2024. +// Copyright © 2024 monal-im.org. All rights reserved. +// + +import SAMKeychain + +struct LoginCredentials: View { + @Environment(\.presentationMode) var presentationMode + let accountID: NSNumber? + let jid: String + let resource: String + @State private var password: String + @State private var hardcodedServer: String + @State private var hardcodedPort: String + @State private var allowPlainAuth: Bool + @State private var forceDirectTLS: Bool + + init(accountID: NSNumber?) { + self.accountID = accountID + guard accountID != nil, + let settings = DataLayer.sharedInstance().details(forAccount: accountID!) else { + self.jid = "" + self.resource = "" + self.hardcodedServer = "" + self.hardcodedPort = "5222" + self.allowPlainAuth = false + self.forceDirectTLS = false + self.password = "" + return + } + + self.jid = "\(settings["username"]!)@\(settings["domain"]!)" + self.resource = settings["resource"] as? String ?? "" + _hardcodedServer = State(initialValue: settings["server"] as? String ?? "") + _hardcodedPort = State(initialValue: "\(settings["other_port"] ?? 5222)") + _allowPlainAuth = State(initialValue: settings["plain_activated"] as? Bool ?? false) + _forceDirectTLS = State(initialValue: settings["directTLS"] as? Bool ?? false) + _password = State(initialValue: SAMKeychain.password(forService: kMonalKeychainName, account: self.accountID!.stringValue) ?? "") + } + + var body: some View { + Form { + Section(header: Text("")) { + HStack { + Text("XMPP ID") + Spacer() + Text(self.jid) + } + HStack { + Text("Password") + Spacer() +#if IS_QUICKSY + TextField("Password", text: $password) + .textInputAutocapitalization(.never) + .disableAutocorrection(true) +#else + SecureField("Password", text: $password) +#endif + } + } + + Section(header: Text("Advanced")) { + HStack { + Text("Server") + Spacer() + TextField("Optional Hardcoded Hostname", text: $hardcodedServer) + .textInputAutocapitalization(.never) + .disableAutocorrection(true) + } + + if !hardcodedServer.isEmpty { + HStack { + Text("Port") + Spacer() + TextField("Optional Hardcoded Port", text: $hardcodedPort) + .keyboardType(.numberPad) + .onDisappear { + hardcodedPort = "5222" + } + } + + Toggle(isOn: $forceDirectTLS) { + Text("Always use direct TLS, not STARTTLS") + } + .multilineTextAlignment(.leading) + .onDisappear { + forceDirectTLS = false + } + } + + Toggle(isOn: $allowPlainAuth) { + Text("Allow MITM-prone PLAIN authentication") + } + // The plain auth setting is read only, to prevent downgrades. TODO: allow upgrading this setting using the SCRAM preload list + .disabled(true) + .multilineTextAlignment(.leading) + + HStack { + Text("Resource") + Spacer() + Text(self.resource) + } + } + + } + .multilineTextAlignment(.trailing) + .navigationTitle("Login Credetials") + .navigationBarBackButtonHidden() + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Abort") { + self.presentationMode.wrappedValue.dismiss() + } + } + ToolbarItem(placement: .confirmationAction) { + Button("Done") { + guard accountID != nil else { + self.presentationMode.wrappedValue.dismiss() + return + } + + var settings = DataLayer.sharedInstance().details(forAccount: accountID!) as! [String: Any] + settings["plain_activated"] = self.allowPlainAuth + settings["directTLS"] = self.forceDirectTLS + settings["server"] = self.hardcodedServer + settings["other_port"] = self.hardcodedPort + // Save the updated settings in the DB + DataLayer.sharedInstance().updateAccoun(with: settings) + // Save the password in the Keychain + if !self.password.isEmpty { + MLXMPPManager.sharedInstance().updatePassword(self.password, forAccount: self.accountID!) + } + self.presentationMode.wrappedValue.dismiss() + + // Disconnect and reconnect so the new credentials / settings take effect + MLXMPPManager.sharedInstance().disconnectAccount(self.accountID!, withExplicitLogout: true) + MLXMPPManager.sharedInstance().connectAccount(self.accountID!) + } + // the jid can be empty if this view was somehow accessed while accountID is nil + .disabled(password.isEmpty || jid.isEmpty) + } + } + } +} diff --git a/Monal/Classes/MLConstants.h b/Monal/Classes/MLConstants.h index 199900bcf5..eb3215b093 100644 --- a/Monal/Classes/MLConstants.h +++ b/Monal/Classes/MLConstants.h @@ -184,6 +184,7 @@ static inline NSString* _Nonnull LocalizationNotNeeded(NSString* _Nonnull s) #define kMonalRefresh @"kMonalRefresh" #define kMonalContactRefresh @"kMonalContactRefresh" +#define kMonalAccountSettingsRefresh @"kMonalAccountSettingsRefresh" #define kMonalXmppUserSoftWareVersionRefresh @"kMonalXmppUserSoftWareVersionRefresh" #define kMonalBlockListRefresh @"kMonalBlockListRefresh" #define kMonalContactRemoved @"kMonalContactRemoved" diff --git a/Monal/Classes/MLMAMPrefTableViewController.h b/Monal/Classes/MLMAMPrefTableViewController.h deleted file mode 100644 index c589fbd101..0000000000 --- a/Monal/Classes/MLMAMPrefTableViewController.h +++ /dev/null @@ -1,16 +0,0 @@ -// -// MLMAMPrefTableViewController.h -// Monal -// -// Created by Anurodh Pokharel on 5/17/18. -// Copyright © 2018 Monal.im. All rights reserved. -// - -#import -#import "xmpp.h" - -@interface MLMAMPrefTableViewController : UITableViewController - -@property (nonatomic, weak) xmpp *xmppAccount; - -@end diff --git a/Monal/Classes/MLMAMPrefTableViewController.m b/Monal/Classes/MLMAMPrefTableViewController.m deleted file mode 100644 index f296fc627c..0000000000 --- a/Monal/Classes/MLMAMPrefTableViewController.m +++ /dev/null @@ -1,110 +0,0 @@ -// -// MLMAMPrefTableViewController.m -// Monal -// -// Created by Anurodh Pokharel on 5/17/18. -// Copyright © 2018 Monal.im. All rights reserved. -// - -#import "MLMAMPrefTableViewController.h" - -@interface MLMAMPrefTableViewController () -@property (nonatomic, strong) NSMutableArray* mamPref; -@property (nonatomic, strong) NSString* currentPref; -@end - -@implementation MLMAMPrefTableViewController - --(void) viewDidLoad -{ - [super viewDidLoad]; - - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updatePrefs:) name:kMLMAMPref object:nil]; - [self.xmppAccount getMAMPrefs]; -} - --(void) didReceiveMemoryWarning -{ - [super didReceiveMemoryWarning]; - // Dispose of any resources that can be recreated. -} - --(void) viewWillAppear:(BOOL)animated -{ - [super viewWillAppear:animated]; - self.navigationItem.title = self.xmppAccount.connectionProperties.identity.domain; - self.mamPref = [NSMutableArray new]; - [self.mamPref addObject:@{@"Title":NSLocalizedString(@"Always archive", @""), @"Description":NSLocalizedString(@"All messages are archived by default.", @""), @"value":@"always"}]; - [self.mamPref addObject:@{@"Title":NSLocalizedString(@"Never archive", @""), @"Description":NSLocalizedString(@"Messages never archived by default.", @""), @"value":@"never"}]; - [self.mamPref addObject:@{@"Title":NSLocalizedString(@"Only contacts", @""), @"Description":NSLocalizedString(@"Archive only if the contact is in contact list.", @""), @"value":@"roster"}]; -} - --(void) dealloc -{ - [[NSNotificationCenter defaultCenter] removeObserver:self]; -} - - --(void) updatePrefs:(NSNotification *) notification -{ - dispatch_async(dispatch_get_main_queue(), ^{ - NSDictionary* dic = (NSDictionary*)notification.userInfo; - self.currentPref = [dic objectForKey:@"mamPref"]; - [self.tableView reloadData]; - }); -} - -#pragma mark - Table view data source - --(NSInteger) numberOfSectionsInTableView:(UITableView *)tableView -{ - return 1; -} - --(NSInteger) tableView:(UITableView*) tableView numberOfRowsInSection:(NSInteger) section -{ - return self.mamPref.count; -} - - --(UITableViewCell*) tableView:(UITableView*) tableView cellForRowAtIndexPath:(NSIndexPath*) indexPath -{ - UITableViewCell* cell = [tableView dequeueReusableCellWithIdentifier:@"serverCell" forIndexPath:indexPath]; - NSDictionary* dic = [self.mamPref objectAtIndex:indexPath.row]; - cell.textLabel.text = dic[@"Title"]; - cell.detailTextLabel.text = dic[@"Description"]; - - if([dic[@"value"] isEqualToString:self.currentPref]) - cell.accessoryType = UITableViewCellAccessoryCheckmark; - else - cell.accessoryType = UITableViewCellAccessoryNone; - - return cell; -} - --(void) tableView:(UITableView*) tableView didSelectRowAtIndexPath:(NSIndexPath*) indexPath -{ - [tableView deselectRowAtIndexPath:indexPath animated:NO]; - switch(indexPath.row) - { - case 0: - self.currentPref = @"always"; - break; - case 1: - self.currentPref = @"never"; - break; - case 2: - self.currentPref = @"roster"; - break; - } - [self.xmppAccount setMAMPrefs:self.currentPref]; - [self.tableView reloadData]; -} - - --(NSString*) tableView:(UITableView*) tableView titleForHeaderInSection:(NSInteger) section -{ - return NSLocalizedString(@"Select Message Archive Management (MAM) Preferences ", @""); -} - -@end diff --git a/Monal/Classes/MLPubSubProcessor.m b/Monal/Classes/MLPubSubProcessor.m index f2005e70ef..c9e65d338b 100644 --- a/Monal/Classes/MLPubSubProcessor.m +++ b/Monal/Classes/MLPubSubProcessor.m @@ -162,6 +162,12 @@ @implementation MLPubSubProcessor [[MLNotificationQueue currentQueue] postNotificationName:kMonalContactRefresh object:account userInfo:@{ @"contact": [MLContact createContactFromJid:jid andAccountID:account.accountID] }]; + if ([jid isEqualToString:account.connectionProperties.identity.jid]) + { + [[MLNotificationQueue currentQueue] postNotificationName:kMonalAccountSettingsRefresh object:account userInfo:@{ + @"accountID": account.accountID + }]; + } DDLogInfo(@"Avatar of '%@' fetched and updated successfully", jid); } else @@ -184,6 +190,10 @@ @implementation MLPubSubProcessor NSMutableDictionary* accountDic = [[NSMutableDictionary alloc] initWithDictionary:[[DataLayer sharedInstance] detailsForAccount:account.accountID] copyItems:YES]; accountDic[kRosterName] = [data[itemId] findFirst:@"{http://jabber.org/protocol/nick}nick#"]; [[DataLayer sharedInstance] updateAccounWithDictionary:accountDic]; + //TODO: post a notification here so the view can refresh + [[MLNotificationQueue currentQueue] postNotificationName:kMonalAccountSettingsRefresh object:account userInfo:@{ + @"accountID": account.accountID + }]; } else //roster name of contact { @@ -211,6 +221,14 @@ @implementation MLPubSubProcessor NSMutableDictionary* accountDic = [[NSMutableDictionary alloc] initWithDictionary:[[DataLayer sharedInstance] detailsForAccount:account.accountID] copyItems:NO]; accountDic[kRosterName] = @""; [[DataLayer sharedInstance] updateAccounWithDictionary:accountDic]; + + //delete cache to make sure the image will be regenerated + [[MLImageManager sharedInstance] purgeCacheForContact:account.connectionProperties.identity.jid andAccount:account.accountID]; + //TODO: post a notification here so the view can refresh + [[MLNotificationQueue currentQueue] postNotificationName:kMonalAccountSettingsRefresh object:account userInfo:@{ + @"accountID": account.accountID + }]; + } else { @@ -777,6 +795,13 @@ @implementation MLPubSubProcessor return; } DDLogDebug(@"Removed avatar from pep"); + + //delete cache to make sure the image will be regenerated + [[MLImageManager sharedInstance] purgeCacheForContact:account.connectionProperties.identity.jid andAccount:account.accountID]; + //post notification + [[MLNotificationQueue currentQueue] postNotificationName:kMonalAccountSettingsRefresh object:account userInfo:@{ + @"accountID": account.accountID + }]; $$ $$class_handler(avatarMetadataPublished, $$ID(xmpp*, account), $$BOOL(success), $_ID(XMPPIQ*, errorIq), $_ID(NSString*, errorReason)) @@ -787,6 +812,12 @@ @implementation MLPubSubProcessor return; } DDLogDebug(@"Published avatar metadata to pep"); + //delete cache to make sure the image will be regenerated + [[MLImageManager sharedInstance] purgeCacheForContact:account.connectionProperties.identity.jid andAccount:account.accountID]; + + //[[MLNotificationQueue currentQueue] postNotificationName:kMonalAccountSettingsRefresh object:account userInfo:@{ + // @"accountID": account.accountID + //}]; $$ $$class_handler(avatarDataPublished, $$ID(xmpp*, account), $$BOOL(success), $_ID(XMPPIQ*, errorIq), $_ID(NSString*, errorReason), $$ID(NSString*, imageHash), $$UINTEGER(imageBytesLen)) diff --git a/Monal/Classes/MLSettingsTableViewController.m b/Monal/Classes/MLSettingsTableViewController.m index 326b120323..0cc5b13ee5 100644 --- a/Monal/Classes/MLSettingsTableViewController.m +++ b/Monal/Classes/MLSettingsTableViewController.m @@ -12,7 +12,6 @@ #import "HelperTools.h" #import "DataLayer.h" #import "MLXMPPManager.h" -#import "XMPPEdit.h" #import "MonalAppDelegate.h" #import "ActiveChatsViewController.h" #import @@ -215,21 +214,6 @@ -(void) prepareForSegue:(UIStoryboardSegue*) segue sender:(id) sender [web initViewWithUrl:[NSURL URLWithString:@"https://github.com/monal-im/Monal/wiki/FAQ---Frequently-Asked-Questions"]]; } - else if([segue.identifier isEqualToString:@"editXMPP"]) - { - XMPPEdit* editor = (XMPPEdit*) segue.destinationViewController.childViewControllers.firstObject; // segue.destinationViewController; - - if(self.selected && self.selected.row >= (int) [self getAccountNum]) - { - editor.accountID = [NSNumber numberWithInt:-1]; - } - else - { - MLAssert(self.selected != nil, @"self.selected must not be nil"); - editor.originIndex = self.selected; - editor.accountID = [self getAccountIDByIndex:self.selected.row]; - } - } } -(UITableViewCell*) tableView:(UITableView*) tableView cellForRowAtIndexPath:(NSIndexPath*) indexPath @@ -362,7 +346,11 @@ -(void)tableView:(UITableView*) tableView didSelectRowAtIndexPath:(NSIndexPath*) case kSettingSectionAccounts: { self.selected = indexPath; if(indexPath.row < (int) [self getAccountNum]) - [self performSegueWithIdentifier:@"editXMPP" sender:self]; + { + NSNumber* accountID = [self getAccountIDByIndex:indexPath.row]; + UIViewController* accountSettingsView = [[SwiftuiInterface new] makeAccountSettingsViewFor:accountID]; + [self showDetailViewController:accountSettingsView sender:self]; + } else { switch(indexPath.row - [self getAccountNum]) { diff --git a/Monal/Classes/OmemoKeysView.swift b/Monal/Classes/OmemoKeysView.swift index 7b457a8e9b..fc0bfd577e 100644 --- a/Monal/Classes/OmemoKeysView.swift +++ b/Monal/Classes/OmemoKeysView.swift @@ -475,7 +475,9 @@ class OmemoKeysForChat: ObservableObject { } private static func devicesForContact(contact: ObservableKVOWrapper) -> OmemoKeysForContact { - let account: xmpp = (contact.account as xmpp?)! + guard let account = contact.obj.account else { + return OmemoKeysForContact(devices: []) + } let devicesForContact: Set = account.omemo.knownDevices(forAddressName: contact.contactJid) return OmemoKeysForContact(devices: devicesForContact) } diff --git a/Monal/Classes/SwiftuiHelpers.swift b/Monal/Classes/SwiftuiHelpers.swift index 34d94e208e..e74fe9da93 100644 --- a/Monal/Classes/SwiftuiHelpers.swift +++ b/Monal/Classes/SwiftuiHelpers.swift @@ -178,15 +178,25 @@ extension View { } } -struct TopRight: ViewModifier { +struct TopEdge: ViewModifier { + enum Edge { + case left, right + } + let edge: Edge let overlay: T public func body(content: Content) -> some View { ZStack(alignment: .topLeading) { content VStack { HStack { - Spacer() - overlay + if edge == .left { + overlay + Spacer() + } + else if edge == .right { + Spacer() + overlay + } } Spacer() } @@ -195,10 +205,17 @@ struct TopRight: ViewModifier { } extension View { func addTopRight(view overlayClosure: @autoclosure @escaping () -> T) -> some View { - modifier(TopRight(overlay:overlayClosure())) + modifier(TopEdge(edge: .right, overlay:overlayClosure())) } func addTopRight(@ViewBuilder _ overlayClosure: @escaping () -> some View) -> some View { - modifier(TopRight(overlay:overlayClosure())) + modifier(TopEdge(edge: .right, overlay:overlayClosure())) + } + + func addTopLeft(view overlayClosure: @autoclosure @escaping () -> T) -> some View { + modifier(TopEdge(edge: .left, overlay:overlayClosure())) + } + func addTopLeft(@ViewBuilder _ overlayClosure: @escaping () -> some View) -> some View { + modifier(TopEdge(edge: .left, overlay:overlayClosure())) } } @@ -725,18 +742,7 @@ class SwiftuiInterface : NSObject { host.rootView = AnyView(MediaItemSwipeView(currentItem: currentItem, allItems: allItems)) return host } - - @objc - func makeOwnOmemoKeyView(_ ownContact: MLContact?) -> UIViewController { - let host = UIHostingController(rootView:AnyView(EmptyView())) - if(ownContact == nil) { - host.rootView = AnyView(UIKitWorkaround(OmemoKeysView(omemoKeys: OmemoKeysForChat(viewContact: nil)))) - } else { - host.rootView = AnyView(UIKitWorkaround(OmemoKeysView(omemoKeys: OmemoKeysForChat(viewContact: ObservableKVOWrapper(ownContact!))))) - } - return host - } - + @objc func makeAccountRegistration(_ registerData: [String:AnyObject]?) -> UIViewController { let delegate = SheetDismisserProtocol() @@ -751,16 +757,11 @@ class SwiftuiInterface : NSObject { } @objc - func makeServerDetailsView(for xmppAccount: xmpp) -> UIViewController { - let host = UIHostingController(rootView:AnyView(EmptyView())) - host.rootView = AnyView(ServerDetails(xmppAccount: xmppAccount)) - return host - } - - @objc - func makeBlockedUsersView(for xmppAccount: xmpp) -> UIViewController { + func makeAccountSettingsView(for accountID: NSNumber) -> UIViewController { + let delegate = SheetDismisserProtocol() let host = UIHostingController(rootView:AnyView(EmptyView())) - host.rootView = AnyView(BlockedUsers(xmppAccount: xmppAccount)) + delegate.host = host + host.rootView = AnyView(UIKitWorkaround(AccountSettings(accountID: accountID, delegate: delegate))) return host } diff --git a/Monal/Classes/XMPPEdit.h b/Monal/Classes/XMPPEdit.h deleted file mode 100644 index 1ece9b4ee8..0000000000 --- a/Monal/Classes/XMPPEdit.h +++ /dev/null @@ -1,26 +0,0 @@ -// -// buddylist.h -// SworIM -// -// Created by Anurodh Pokharel on 11/21/08. -// Copyright 2008 __MyCompanyName__. All rights reserved. -// - -#import -#import "DataLayer.h" -@import SAMKeychain; -#import "MLXMPPManager.h" -#import "TOCropViewController.h" - -@interface XMPPEdit: UITableViewController { - IBOutlet UILabel *JIDLabel; -} - -@property (nonatomic, strong) NSNumber* accountID; -@property (nonatomic, strong) NSIndexPath* originIndex; - --(IBAction) save:(id) sender; - -@end - - diff --git a/Monal/Classes/XMPPEdit.m b/Monal/Classes/XMPPEdit.m deleted file mode 100644 index 7c89a94c81..0000000000 --- a/Monal/Classes/XMPPEdit.m +++ /dev/null @@ -1,1136 +0,0 @@ -// -// buddylist.m -// SworIM -// -// Created by Anurodh Pokharel on 11/21/08. -// Copyright 2008 __MyCompanyName__. All rights reserved. -// - -#import "XMPPEdit.h" -#import "xmpp.h" -#import "MBProgressHUD.h" -#import "MLButtonCell.h" -#import "MLImageManager.h" -#import "MLPasswordChangeTableViewController.h" -#import "MLSwitchCell.h" -#import "MLOMEMO.h" -#import "MLNotificationQueue.h" -#import "MonalAppDelegate.h" -#import "ActiveChatsViewController.h" -#import "Monal-Swift.h" - -@import MobileCoreServices; -@import AVFoundation; -@import UniformTypeIdentifiers.UTCoreTypes; -@import Intents; - -enum kSettingSection { - kSettingSectionAvatar, - kSettingSectionAccount, - kSettingSectionGeneral, - kSettingSectionAdvanced, - kSettingSectionEdit, - kSettingSectionCount -}; - -enum kSettingsAvatarRows { - SettingsAvatarRowsCnt -}; - -enum kSettingsAccountRows { - SettingsEnabledRow, - SettingsDisplayNameRow, - SettingsStatusMessageRow, - SettingsServerDetailsRow, - SettingsAccountRowsCnt -}; - -enum kSettingsGeneralRows { - SettingsChangePasswordRow, - SettingsOmemoKeysRow, - SettingsBlockedUsersRow, - SettingsGeneralRowsCnt -}; - -enum kSettingsAdvancedRows { - SettingsJidRow, - SettingsPasswordRow, - SettingsServerRow, - SettingsPortRow, - SettingsDirectTLSRow, - SettingsPlainActivatedRow, - SettingsResourceRow, - SettingsAdvancedRowsCnt -}; - -enum kSettingsEditRows { - SettingsClearHistoryRow, - SettingsRemoveAccountRow, - SettingsDeleteAccountRow, - SettingsEditRowsCnt -}; - -//this will hold all disabled rows of all enums (this is needed because the code below still references these rows) -enum DummySettingsRows { - DummySettingsRowsBegin = 100, -}; - - -@interface MLXMPPConnection () -@property (nonatomic) MLXMPPServer* server; -@property (nonatomic) MLXMPPIdentity* identity; -@end - -@interface XMPPEdit() -@property (nonatomic, strong) DataLayer* db; -@property (nonatomic, strong) NSMutableDictionary* sectionDictionary; - -@property (nonatomic, assign) BOOL editMode; -// Used for QR-Code scanning -@property (nonatomic, strong) NSString* jid; -@property (nonatomic, strong) NSString* password; - -@property (nonatomic, strong) NSString* accountType; - -@property (nonatomic, strong) NSString *rosterName; -@property (nonatomic, strong) NSString *statusMessage; -@property (nonatomic, strong) NSString *resource; -@property (nonatomic, strong) NSString *server; -@property (nonatomic, strong) NSString *port; - -@property (nonatomic, assign) BOOL enabled; -@property (nonatomic, assign) BOOL directTLS; - -@property (nonatomic, weak) UITextField *currentTextField; - -@property (nonatomic, strong) UIDocumentPickerViewController *imagePicker; - -@property (nonatomic, strong) UIImageView *userAvatarImageView; -@property (nonatomic, strong) UIImage *selectedAvatarImage; -@property (nonatomic) BOOL avatarChanged; -@property (nonatomic) BOOL rosterNameChanged; -@property (nonatomic) BOOL statusMessageChanged; -@property (nonatomic) BOOL detailsChanged; - -@property (nonatomic) BOOL plainActivated; - -@property (nonatomic) BOOL deactivateSave; -@end - -@implementation XMPPEdit - --(void) hideKeyboard -{ - [self.currentTextField resignFirstResponder]; -} - -#pragma mark view lifecylce - --(void) viewDidLoad -{ - self.deactivateSave = NO; - [super viewDidLoad]; - - [self.tableView registerNib:[UINib nibWithNibName:@"MLSwitchCell" - bundle:[NSBundle mainBundle]] - forCellReuseIdentifier:@"AccountCell"]; - - - [self.tableView registerNib:[UINib nibWithNibName:@"MLButtonCell" - bundle:[NSBundle mainBundle]] - forCellReuseIdentifier:@"ButtonCell"]; - - _db = [DataLayer sharedInstance]; - - if(self.accountID.intValue != -1) - self.editMode = YES; - - DDLogVerbose(@"got account number %@", self.accountID); - - UITapGestureRecognizer* gestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(hideKeyboard)]; // hides the kkyeboard when you tap outside the editing area - gestureRecognizer.cancelsTouchesInView = false; //this prevents it from blocking the button - [self.tableView addGestureRecognizer:gestureRecognizer]; - - self.avatarChanged = NO; - self.rosterNameChanged = NO; - self.statusMessageChanged = NO; - - //default strings used for edit and new mode - self.sectionDictionary = [NSMutableDictionary new]; - for(int entry = 0; entry < kSettingSectionCount; entry++) - switch(entry) - { - case kSettingSectionAvatar: - self.sectionDictionary[@(entry)] = @""; break; - case kSettingSectionAccount: - self.sectionDictionary[@(entry)] = @""; break; - case kSettingSectionGeneral: - self.sectionDictionary[@(entry)] = NSLocalizedString(@"General", @""); - break; - case kSettingSectionAdvanced: - self.sectionDictionary[@(entry)] = NSLocalizedString(@"Advanced Settings", @""); - break; - case kSettingSectionEdit: - self.sectionDictionary[@(entry)] = @""; - break; - default: - self.sectionDictionary[@(entry)] = @""; - break; - } - - if(self.originIndex && self.originIndex.section == 0) - { - //edit - DDLogVerbose(@"reading account number %@", self.accountID); - NSDictionary* settings = [_db detailsForAccount:self.accountID]; - MLAssert(settings != nil, @"Settings dict should never be nil here!"); - - self.jid = [NSString stringWithFormat:@"%@@%@", [settings objectForKey:@"username"], [settings objectForKey:@"domain"]]; - NSString* pass = [SAMKeychain passwordForService:kMonalKeychainName account:self.accountID.stringValue]; - - if(pass) - self.password = pass; - - self.server = [settings objectForKey:@"server"]; - - self.port = [NSString stringWithFormat:@"%@", [settings objectForKey:@"other_port"]]; - self.resource = [settings objectForKey:kResource]; - - self.enabled = [[settings objectForKey:kEnabled] boolValue]; - - self.directTLS = [[settings objectForKey:@"directTLS"] boolValue]; - - self.rosterName = [settings objectForKey:kRosterName]; - self.statusMessage = [settings objectForKey:@"statusMessage"]; - - self.plainActivated = [[settings objectForKey:kPlainActivated] boolValue]; - - //overwrite account section heading in edit mode - self.sectionDictionary[@(kSettingSectionAccount)] = [NSString stringWithFormat:NSLocalizedString(@"Account (%@)", @""), self.jid]; - } - else - { - self.title = NSLocalizedString(@"New Account", @""); - self.port = @"5222"; - self.resource = [HelperTools encodeRandomResource]; - self.directTLS = NO; - self.rosterName = @""; - self.statusMessage = @""; - self.enabled = YES; - self.plainActivated = NO; - - //overwrite account section heading in new mode - self.sectionDictionary[@(kSettingSectionAccount)] = NSLocalizedString(@"Account (new)", @""); - } -#if TARGET_OS_MACCATALYST - self.imagePicker = [[UIDocumentPickerViewController alloc] initForOpeningContentTypes:@[UTTypeImage]]; - self.imagePicker.allowsMultipleSelection = NO; - self.imagePicker.delegate = self; -#endif -} - --(void) viewWillAppear:(BOOL) animated -{ - [super viewWillAppear:animated]; - DDLogVerbose(@"xmpp edit view will appear"); -} - --(void) viewWillDisappear:(BOOL) animated -{ - [super viewWillDisappear:animated]; - DDLogVerbose(@"xmpp edit view will hide"); -} - --(void) dealloc -{ - [[NSNotificationCenter defaultCenter] removeObserver:self]; -} - --(void) alertWithTitle:(NSString*) title andMsg:(NSString*) msg -{ - dispatch_async(dispatch_get_main_queue(), ^{ - UIAlertController* alert = [UIAlertController alertControllerWithTitle:title message:msg preferredStyle:UIAlertControllerStyleAlert]; - [alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Close", @"") style:UIAlertActionStyleDefault handler:^(UIAlertAction* action __unused) { - [alert dismissViewControllerAnimated:YES completion:nil]; - }]]; - [self presentViewController:alert animated:YES completion:nil]; - }); -} - -#pragma mark actions - --(IBAction) save:(id) sender -{ - if(self.deactivateSave) - { - DDLogWarn(@"Save pressed but already deactivated!"); - return; - } - - NSError* error; - [self.currentTextField resignFirstResponder]; - - DDLogVerbose(@"Saving"); - - NSString* lowerJid = [self.jid.lowercaseString copy]; - NSString* domain; - NSString* user; - - if([lowerJid length] == 0) - { - [self alertWithTitle:NSLocalizedString(@"XMPP ID missing", @"") andMsg:NSLocalizedString(@"You have not entered your XMPP ID yet", @"")]; - return; - } - - if([lowerJid characterAtIndex:0] == '@') - { - //first char =@ means no username in jid - [self alertWithTitle:NSLocalizedString(@"Username missing", @"") andMsg:NSLocalizedString(@"Your entered XMPP ID is missing the username", @"")]; - return; - } - - //check if our keychain contains a password - if(self.enabled && self.password.length == 0) - { - [SAMKeychain passwordForService:kMonalKeychainName account:self.accountID.stringValue error:&error]; - if(error != nil) - { - DDLogError(@"Keychain error: %@", error); - self.enabled = NO; - [self.tableView reloadData]; - [self alertWithTitle:NSLocalizedString(@"Password missing", @"") andMsg:NSLocalizedString(@"Please enter a password below before activating this account.", @"")]; - return; - } - } - - NSArray* elements = [lowerJid componentsSeparatedByString:@"@"]; - - //if it is a JID - if([elements count] > 1) - { - user = [elements objectAtIndex:0]; - domain = [elements objectAtIndex:1]; - } - else - { - user = lowerJid; - domain = @""; - } - if([domain isEqualToString:@""]) - { - [self alertWithTitle:NSLocalizedString(@"Domain missing", @"") andMsg:NSLocalizedString(@"Your entered XMPP ID is missing the domain", @"")]; - return; - } - - NSMutableDictionary* dic = [NSMutableDictionary new]; - [dic setObject:[domain.lowercaseString stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]] forKey:kDomain]; - if(user) - [dic setObject:[user.lowercaseString stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]] forKey:kUsername]; - if(self.server) - [dic setObject:[self.server stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]] forKey:kServer]; - if(self.port) - [dic setObject:self.port forKey:kPort]; - [dic setObject:self.resource forKey:kResource]; - [dic setObject:[NSNumber numberWithBool:self.enabled] forKey:kEnabled]; - [dic setObject:[NSNumber numberWithBool:self.directTLS] forKey:kDirectTLS]; - [dic setObject:self.accountID forKey:kAccountID]; - if(self.rosterName) - [dic setObject:[self.rosterName stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]] forKey:kRosterName]; - if(self.statusMessage) - [dic setObject:[self.statusMessage stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]] forKey:@"statusMessage"]; - - //conversations.im already supports sasl2 and scram ## TODO: use SCRAM preload list - [dic setObject:([domain.lowercaseString isEqualToString:@"conversations.im"] ? @NO : @(self.plainActivated)) forKey:kPlainActivated]; - - if(!self.editMode) - { - - if(([self.jid length] == 0) && - ([self.password length] == 0) - ) - { - //ignoring blank - } - else - { - BOOL accountExists = [[DataLayer sharedInstance] doesAccountExistUser:user andDomain:domain]; - if(!accountExists) - { - DDLogVerbose(@"Creating account: %@", dic); - NSNumber* accountID = [[DataLayer sharedInstance] addAccountWithDictionary:dic]; - if(accountID != nil) - { - self.accountID = accountID; - [SAMKeychain setAccessibilityType:kSecAttrAccessibleAfterFirstUnlock]; - [SAMKeychain setPassword:self.password forService:kMonalKeychainName account:self.accountID.stringValue]; - if(self.enabled) - { - DDLogVerbose(@"Now connecting newly created account: %@", self.accountID); - [[MLXMPPManager sharedInstance] connectAccount:self.accountID]; - xmpp* account = [[MLXMPPManager sharedInstance] getEnabledAccountForID:self.accountID]; - [account publishStatusMessage:self.statusMessage]; - [account publishRosterName:self.rosterName]; - [account publishAvatar:self.selectedAvatarImage]; - } - else - { - DDLogVerbose(@"Making sure newly created account is not connected and deleting all SiriKit interactions: %@", self.accountID); - [[MLXMPPManager sharedInstance] disconnectAccount:self.accountID withExplicitLogout:YES]; - [HelperTools removeAllShareInteractionsForAccountID:self.accountID]; - } - //trigger view updates to make sure enabled/disabled account state propagates to all ui elements - [[MLNotificationQueue currentQueue] postNotificationName:kMonalRefresh object:nil userInfo:nil]; - [self showSuccessHUD]; - } - } - else - [self alertWithTitle:NSLocalizedString(@"Account Exists", @"") andMsg:NSLocalizedString(@"This account already exists in Monal.", @"")]; - } - } - else - { - [dic setObject:[NSNumber numberWithBool:NO] forKey:kNeedsPasswordMigration]; - DDLogVerbose(@"Updating existing account: %@", dic); - //disconnect account before disabling it in db, to avoid assertions when trying to create MLContact instances - //for the disabled account (for notifications etc.) - if(!self.enabled) - { - DDLogVerbose(@"Account is not enabled anymore, deleting all SiriKit interactions and making sure it's disconnected: %@", self.accountID); - [[MLXMPPManager sharedInstance] disconnectAccount:self.accountID withExplicitLogout:YES]; - [HelperTools removeAllShareInteractionsForAccountID:self.accountID]; - } - //this case makes sure we recreate a completely new account instance below (using our new settings) if the account details changed - else if(self.detailsChanged) - [[MLXMPPManager sharedInstance] disconnectAccount:self.accountID withExplicitLogout:NO]; - - DDLogVerbose(@"Now updating DB with account dict..."); - [[DataLayer sharedInstance] updateAccounWithDictionary:dic]; - if(self.password.length) - { - DDLogVerbose(@"Now setting password for account %@ in SAMKeychain...", self.accountID); - [[MLXMPPManager sharedInstance] updatePassword:self.password forAccount:self.accountID]; - } - if(self.enabled) - { - DDLogVerbose(@"Account is (still) enabled, connecting it: %@", self.accountID); - [[MLXMPPManager sharedInstance] connectAccount:self.accountID]; - xmpp* account = [[MLXMPPManager sharedInstance] getEnabledAccountForID:self.accountID]; - if(self.statusMessageChanged) - [account publishStatusMessage:self.statusMessage]; - if(self.rosterNameChanged) - [account publishRosterName:self.rosterName]; - if(self.avatarChanged) - [account publishAvatar:self.selectedAvatarImage]; - } - //trigger view updates to make sure enabled/disabled account state propagates to all ui elements - [[MLNotificationQueue currentQueue] postNotificationName:kMonalRefresh object:nil userInfo:nil]; - [self showSuccessHUD]; - } -} - --(void) showSuccessHUD -{ - dispatch_async(dispatch_get_main_queue(), ^{ - MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:self.view animated:YES]; - hud.mode = MBProgressHUDModeCustomView; - hud.removeFromSuperViewOnHide = YES; - hud.label.text = NSLocalizedString(@"Success", @""); - hud.detailsLabel.text = NSLocalizedString(@"The account has been saved", @""); - UIImage *image = [[UIImage imageNamed:@"success"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; - hud.customView = [[UIImageView alloc] initWithImage:image]; - [hud hideAnimated:YES afterDelay:1.0f]; - - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ - [self dismissViewControllerAnimated:YES completion:nil]; - }); - }); -} - -- (IBAction) removeAccountClicked: (id) sender -{ - UIAlertController* questionAlert =[UIAlertController alertControllerWithTitle:NSLocalizedString(@"Delete Account", @"") message:NSLocalizedString(@"This will remove this account and the associated data from this device.", @"") preferredStyle:UIAlertControllerStyleActionSheet]; - UIAlertAction* noAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"No", @"") style:UIAlertActionStyleCancel handler:^(UIAlertAction* action __unused) { - //do nothing when "no" was pressed - }]; - UIAlertAction* yesAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"Yes", @"") style:UIAlertActionStyleDefault handler:^(UIAlertAction* action __unused) { - DDLogVerbose(@"Removing accountID %@", self.accountID); - self.deactivateSave = YES; - [[MLXMPPManager sharedInstance] removeAccountForAccountID:self.accountID]; - - MBProgressHUD* hud = [MBProgressHUD showHUDAddedTo:self.view animated:YES]; - hud.mode = MBProgressHUDModeCustomView; - hud.removeFromSuperViewOnHide = YES; - hud.label.text = NSLocalizedString(@"Success", @""); - hud.detailsLabel.text = NSLocalizedString(@"The account has been removed", @""); - UIImage* image = [[UIImage imageNamed:@"success"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; - hud.customView = [[UIImageView alloc] initWithImage:image]; - [hud hideAnimated:YES afterDelay:1.0f]; - - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ - [self dismissViewControllerAnimated:YES completion:^{ - //we want to start fresh instead of doing a "password migration"-restore directly triggering an sms - [[HelperTools defaultsDB] removeObjectForKey:@"Quicksy_phoneNumber"]; - [[HelperTools defaultsDB] removeObjectForKey:@"Quicksy_country"]; - //make sure we show account creation view etc. after removing the last account - MonalAppDelegate* appDelegate = (MonalAppDelegate *)[[UIApplication sharedApplication] delegate]; - [appDelegate.activeChats segueToIntroScreensIfNeeded]; - }]; - }); - }]; - [questionAlert addAction:noAction]; - [questionAlert addAction:yesAction]; - - UIPopoverPresentationController* popPresenter = [questionAlert popoverPresentationController]; - popPresenter.sourceView = self.view; - - [self presentViewController:questionAlert animated:YES completion:nil]; -} - --(IBAction) deleteAccountClicked:(id) sender -{ - xmpp* xmppAccount = [[MLXMPPManager sharedInstance] getEnabledAccountForID:self.accountID]; - if(xmppAccount.accountState < kStateBound) - { - UIAlertController* alert = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"Error Removing Account", @"") - message:NSLocalizedString(@"Your account must be enabled and connected, to be removed from the server!", @"") preferredStyle:UIAlertControllerStyleAlert]; - [alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Close", @"") style:UIAlertActionStyleDefault handler:^(UIAlertAction* action __unused) { - [alert dismissViewControllerAnimated:YES completion:nil]; - }]]; - [self presentViewController:alert animated:YES completion:nil]; - return; - } - - UIAlertController* questionAlert =[UIAlertController alertControllerWithTitle:NSLocalizedString(@"Delete Account", @"") message:NSLocalizedString(@"This will delete this account and the associated data from the server and this device. Data might still be retained on other devices, though.", @"") preferredStyle:UIAlertControllerStyleActionSheet]; - UIAlertAction* noAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"No", @"") style:UIAlertActionStyleCancel handler:^(UIAlertAction* action __unused) { - //do nothing when "no" was pressed - }]; - UIAlertAction* yesAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"Yes", @"") style:UIAlertActionStyleDefault handler:^(UIAlertAction* action __unused) { - DDLogVerbose(@"Deleting account on server: %@", xmppAccount); - self.deactivateSave = YES; - [xmppAccount removeFromServerWithCompletion:^(NSString* error) { - dispatch_async(dispatch_get_main_queue(), ^{ - if(error != nil) - { - UIAlertController* alert = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"Error Removing Account", @"") - message:error preferredStyle:UIAlertControllerStyleAlert]; - [alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Close", @"") style:UIAlertActionStyleDefault handler:^(UIAlertAction* action __unused) { - [alert dismissViewControllerAnimated:YES completion:nil]; - }]]; - [self presentViewController:alert animated:YES completion:nil]; - } - else - { - MBProgressHUD* hud = [MBProgressHUD showHUDAddedTo:self.view animated:YES]; - hud.mode = MBProgressHUDModeCustomView; - hud.removeFromSuperViewOnHide = YES; - hud.label.text = NSLocalizedString(@"Success", @""); - hud.detailsLabel.text = NSLocalizedString(@"The account has been deleted", @""); - UIImage* image = [[UIImage imageNamed:@"success"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; - hud.customView = [[UIImageView alloc] initWithImage:image]; - [hud hideAnimated:YES afterDelay:1.0f]; - - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ - [self dismissViewControllerAnimated:YES completion:^{ - //we want to start fresh instead of doing a "password migration"-restore directly triggering an sms - [[HelperTools defaultsDB] removeObjectForKey:@"Quicksy_phoneNumber"]; - [[HelperTools defaultsDB] removeObjectForKey:@"Quicksy_country"]; - //make sure we show account creation view etc. after removing the last account - MonalAppDelegate* appDelegate = (MonalAppDelegate *)[[UIApplication sharedApplication] delegate]; - [appDelegate.activeChats segueToIntroScreensIfNeeded]; - }]; - }); - } - }); - }]; - }]; - [questionAlert addAction:noAction]; - [questionAlert addAction:yesAction]; - - UIPopoverPresentationController* popPresenter = [questionAlert popoverPresentationController]; - popPresenter.sourceView = self.view; - - [self presentViewController:questionAlert animated:YES completion:nil]; -} - -- (IBAction) clearHistoryClicked: (id) sender -{ - DDLogVerbose(@"Deleting History"); - - UIAlertController *questionAlert =[UIAlertController alertControllerWithTitle:NSLocalizedString(@"Clear Chat History", @"") message:NSLocalizedString(@"This will clear the whole chat history of this account from this device.", @"") preferredStyle:UIAlertControllerStyleActionSheet]; - UIAlertAction *noAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"No", @"") style:UIAlertActionStyleCancel handler:^(UIAlertAction* action __unused) { - //do nothing when "no" was pressed - }]; - UIAlertAction *yesAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"Yes", @"") style:UIAlertActionStyleDefault handler:^(UIAlertAction* action __unused) { - - [self.db clearMessages:self.accountID]; - [[MLNotificationQueue currentQueue] postNotificationName:kMonalRefresh object:nil userInfo:nil]; - - MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:self.view animated:YES]; - hud.mode = MBProgressHUDModeCustomView; - hud.removeFromSuperViewOnHide=YES; - hud.label.text =NSLocalizedString(@"Success", @""); - hud.detailsLabel.text =NSLocalizedString(@"The chat history has been cleared", @""); - UIImage *image = [[UIImage imageNamed:@"success"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; - hud.customView = [[UIImageView alloc] initWithImage:image]; - [hud hideAnimated:YES afterDelay:1.0f]; - }]; - - [questionAlert addAction:noAction]; - [questionAlert addAction:yesAction]; - - UIPopoverPresentationController* popPresenter = [questionAlert popoverPresentationController]; - popPresenter.sourceView = self.view; - - [self presentViewController:questionAlert animated:YES completion:nil]; - -} - -#pragma mark table view datasource methods - --(CGFloat) tableView:(UITableView*) tableView heightForHeaderInSection:(NSInteger) section -{ - if (section == 0) - return 100; - else - return UITableViewAutomaticDimension; -} - --(CGFloat) tableView:(UITableView*) tableView heightForRowAtIndexPath:(NSIndexPath*) indexPath -{ - return 40; -} - --(UITableViewCell*) tableView:(UITableView*) tableView cellForRowAtIndexPath:(NSIndexPath*) indexPath -{ - DDLogVerbose(@"xmpp edit view section %ld, row %ld", indexPath.section, indexPath.row); - - MLSwitchCell* thecell = (MLSwitchCell*)[tableView dequeueReusableCellWithIdentifier:@"AccountCell"]; - [thecell clear]; - - // load cells from interface builder - if(indexPath.section == kSettingSectionAccount) - { - //the user - switch (indexPath.row) - { - case SettingsEnabledRow: { - [thecell initCell:NSLocalizedString(@"Enabled", @"") withToggle:self.enabled andTag:1]; - break; - } - case SettingsDisplayNameRow: { - [thecell initCell:NSLocalizedString(@"Display Name", @"") withTextField:self.rosterName andPlaceholder:@"" andTag:1]; - thecell.cellLabel.text = NSLocalizedString(@"Display Name", @""); - thecell.textInputField.keyboardType = UIKeyboardTypeAlphabet; - break; - } - case SettingsStatusMessageRow: { - [thecell initCell:NSLocalizedString(@"Status Message", @"") withTextField:self.statusMessage andPlaceholder:NSLocalizedString(@"Your status", @"") andTag:6]; - break; - } - case SettingsServerDetailsRow: { - [thecell initTapCell:NSLocalizedString(@"Protocol support of your server (XEPs)", @"")]; - thecell.accessoryType = UITableViewCellAccessoryDetailButton; - break; - } - } - } - else if(indexPath.section == kSettingSectionGeneral) - { - switch (indexPath.row) - { - case SettingsChangePasswordRow: { -#ifdef IS_QUICKSY - [thecell initTapCell:NSLocalizedString(@"Change/View Password", @"")]; -#else - [thecell initTapCell:NSLocalizedString(@"Change Password", @"")]; -#endif - thecell.cellLabel.text = NSLocalizedString(@"Change Password", @""); - break; - } - case SettingsOmemoKeysRow: { - [thecell initTapCell:NSLocalizedString(@"Encryption Keys (OMEMO)", @"")]; - break; - } - case SettingsBlockedUsersRow: { - [thecell initTapCell:NSLocalizedString(@"Blocked Users", @"")]; - break; - } - } - } - else if(indexPath.section == kSettingSectionAdvanced) - { - switch (indexPath.row) - { - case SettingsJidRow: { - if(self.editMode) - { - // don't allow jid editing - [thecell initCell:NSLocalizedString(@"XMPP ID", @"") withLabel:self.jid]; - } - else - { - // allow entering jid on account creation - [thecell initCell:NSLocalizedString(@"XMPP ID", @"") withTextField:self.jid andPlaceholder:NSLocalizedString(@"Enter your XMPP ID here", @"") andTag:2]; - thecell.textInputField.keyboardType = UIKeyboardTypeEmailAddress; - thecell.textInputField.autocorrectionType = UITextAutocorrectionTypeNo; - thecell.textInputField.autocapitalizationType = UITextAutocapitalizationTypeNone; - } - break; - } - case SettingsPasswordRow: { - [thecell initCell:NSLocalizedString(@"Password", @"") withTextField:self.password secureEntry:YES andPlaceholder:NSLocalizedString(@"Enter your password here", @"") andTag:3]; - break; - } - case SettingsServerRow: { - [thecell initCell:NSLocalizedString(@"Server", @"") withTextField:self.server andPlaceholder:NSLocalizedString(@"Optional Hardcoded Hostname", @"") andTag:4]; - break; - } - case SettingsPortRow: { - [thecell initCell:NSLocalizedString(@"Port", @"") withTextField:self.port andPlaceholder:NSLocalizedString(@"Optional Port", @"") andTag:5]; - break; - } - case SettingsDirectTLSRow: { - [thecell initCell:NSLocalizedString(@"Always use direct TLS, not STARTTLS", @"") withToggle:self.directTLS andTag:2]; - break; - } - case SettingsPlainActivatedRow: { - [thecell initCell:NSLocalizedString(@"Allow MITM-prone PLAIN authentication", @"") withToggle:self.plainActivated andTag:3]; - if(self.editMode) - [thecell.toggleSwitch setEnabled:NO]; - break; - } - case SettingsResourceRow: { - [thecell initCell:NSLocalizedString(@"Resource", @"") withLabel:self.resource]; - break; - } - } - } - else if (indexPath.section == kSettingSectionEdit && self.editMode == YES) - { - switch (indexPath.row) { - case SettingsClearHistoryRow: - { - MLButtonCell* buttonCell = (MLButtonCell*)[tableView dequeueReusableCellWithIdentifier:@"ButtonCell"]; - buttonCell.buttonText.text = NSLocalizedString(@"Clear Chat History", @""); - buttonCell.buttonText.textColor = [UIColor redColor]; - buttonCell.selectionStyle = UITableViewCellSelectionStyleNone; - buttonCell.tag = SettingsClearHistoryRow; - return buttonCell; - } - case SettingsRemoveAccountRow: - { - MLButtonCell* buttonCell = (MLButtonCell*)[tableView dequeueReusableCellWithIdentifier:@"ButtonCell"]; - buttonCell.buttonText.text = NSLocalizedString(@"Remove Account from this Device", @""); - buttonCell.buttonText.textColor = [UIColor redColor]; - buttonCell.selectionStyle = UITableViewCellSelectionStyleNone; - buttonCell.tag = SettingsRemoveAccountRow; - return buttonCell; - } - case SettingsDeleteAccountRow: - { - MLButtonCell* buttonCell = (MLButtonCell*)[tableView dequeueReusableCellWithIdentifier:@"ButtonCell"]; - buttonCell.buttonText.text = NSLocalizedString(@"Delete Account on Server", @""); - buttonCell.buttonText.textColor = [UIColor redColor]; - buttonCell.selectionStyle = UITableViewCellSelectionStyleNone; - buttonCell.tag = SettingsDeleteAccountRow; - return buttonCell; - } - } - } - thecell.textInputField.delegate = self; - if(thecell.textInputField.hidden == YES) - [thecell.toggleSwitch addTarget:self action:@selector(toggleSwitch:) forControlEvents:UIControlEventValueChanged]; - return thecell; -} - --(NSInteger) numberOfSectionsInTableView:(UITableView*) tableView -{ - return kSettingSectionCount; -} - --(UIView*) tableView:(UITableView*) tableView viewForHeaderInSection:(NSInteger) section -{ - if (section == 0) - { - UIView* avatarView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.tableView.frame.size.width, 100)]; - avatarView.backgroundColor = [UIColor clearColor]; - avatarView.userInteractionEnabled = YES; - - self.userAvatarImageView = [[UIImageView alloc] initWithFrame:CGRectMake((self.tableView.frame.size.width - 90)/2 , 25, 90, 90)]; - self.userAvatarImageView.layer.cornerRadius = self.userAvatarImageView.frame.size.height / 2; - self.userAvatarImageView.layer.borderWidth = 2.0f; - self.userAvatarImageView.layer.borderColor = ([UIColor clearColor]).CGColor; - self.userAvatarImageView.clipsToBounds = YES; - self.userAvatarImageView.userInteractionEnabled = YES; - - UITapGestureRecognizer* touchUserAvatarRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(getPhotoAction:)]; - [self.userAvatarImageView addGestureRecognizer:touchUserAvatarRecognizer]; - - if(self.editMode == YES && self.jid != nil && self.accountID.intValue >= 0) - [[MLImageManager sharedInstance] getIconForContact:[MLContact createContactFromJid:self.jid andAccountID:self.accountID] withCompletion:^(UIImage *image) { - [self.userAvatarImageView setImage:image]; - }]; - else - { - //use noicon image for account creation - [self.userAvatarImageView setImage:[MLImageManager circularImage:[UIImage imageNamed:@"noicon"]]]; - } - [avatarView addSubview:self.userAvatarImageView]; - - return avatarView; - } - else - { - NSString* sectionTitle = [self tableView:tableView titleForHeaderInSection:section]; - return [HelperTools MLCustomViewHeaderWithTitle:sectionTitle]; - } -} - --(NSString*) tableView:(UITableView*) tableView titleForHeaderInSection:(NSInteger) section -{ - return self.sectionDictionary[@(section)]; -} - --(NSInteger) tableView:(UITableView*) tableView numberOfRowsInSection:(NSInteger) section -{ - if(section == kSettingSectionAvatar) - return SettingsAvatarRowsCnt; - else if(section == kSettingSectionAccount) - return SettingsAccountRowsCnt; - else if(section == kSettingSectionGeneral && self.editMode) - return SettingsGeneralRowsCnt; - else if(section == kSettingSectionAdvanced) - return SettingsAdvancedRowsCnt; - else if(section == kSettingSectionEdit && self.editMode) - return SettingsEditRowsCnt; - else - return 0; -} - -#pragma mark - table view delegate --(void) tableView:(UITableView*) tableView didSelectRowAtIndexPath:(NSIndexPath*) newIndexPath -{ - DDLogVerbose(@"selected log section %ld , row %ld", newIndexPath.section, newIndexPath.row); - - if(newIndexPath.section == kSettingSectionAccount) - { - switch(newIndexPath.row) - { - case SettingsServerDetailsRow: { - xmpp* xmppAccount = [[MLXMPPManager sharedInstance] getEnabledAccountForID:self.accountID]; - UIViewController* serverDetailsView = [[SwiftuiInterface new] makeServerDetailsViewFor:xmppAccount]; - [self showDetailViewController:serverDetailsView sender:self]; - break; - } - } - } - else if(newIndexPath.section == kSettingSectionGeneral) - { - switch(newIndexPath.row) - { - case SettingsChangePasswordRow: - [self performSegueWithIdentifier:@"showPassChange" sender:self]; - break; - case SettingsOmemoKeysRow: { - UIViewController* ownOmemoKeysView; - xmpp* xmppAccount = [[MLXMPPManager sharedInstance] getEnabledAccountForID:self.accountID]; - if(self.jid == nil || self.accountID == nil || xmppAccount == nil) - { - ownOmemoKeysView = [[SwiftuiInterface new] makeOwnOmemoKeyView:nil]; - } else { - MLContact* ownContact = [MLContact createContactFromJid:self.jid andAccountID:self.accountID]; - ownOmemoKeysView = [[SwiftuiInterface new] makeOwnOmemoKeyView:ownContact]; - } - [self showDetailViewController:ownOmemoKeysView sender:self]; - break; - } - case SettingsBlockedUsersRow: { - xmpp* xmppAccount = [[MLXMPPManager sharedInstance] getEnabledAccountForID:self.accountID]; - if(xmppAccount != nil) - { - UIViewController* blockedUsersView = [[SwiftuiInterface new] makeBlockedUsersViewFor:xmppAccount]; - [self showDetailViewController:blockedUsersView sender:self]; - } - break; - } - } - } - else if(newIndexPath.section == kSettingSectionAdvanced) - { - // nothing to do here - } - else if(newIndexPath.section == kSettingSectionEdit) - { - switch(newIndexPath.row) - { - case SettingsClearHistoryRow: - [self clearHistoryClicked:[tableView cellForRowAtIndexPath:newIndexPath]]; - break; - case SettingsRemoveAccountRow: - [self removeAccountClicked:[tableView cellForRowAtIndexPath:newIndexPath]]; - break; - case SettingsDeleteAccountRow: - [self deleteAccountClicked:[tableView cellForRowAtIndexPath:newIndexPath]]; - break; - } - } -} - --(void) tableView:(UITableView*) tableView accessoryButtonTappedForRowWithIndexPath:(NSIndexPath*) indexPath -{ - if(indexPath.section == kSettingSectionAccount) - { - switch(indexPath.row) - { - case SettingsServerDetailsRow: { - xmpp* xmppAccount = [[MLXMPPManager sharedInstance] getEnabledAccountForID:self.accountID]; - UIViewController* serverDetailsView = [[SwiftuiInterface new] makeServerDetailsViewFor:xmppAccount]; - [self showDetailViewController:serverDetailsView sender:self]; - break; - } - } - } -} - - -#pragma mark - segeue - --(void) prepareForSegue:(UIStoryboardSegue*) segue sender:(id) sender -{ - if([segue.identifier isEqualToString:@"showPassChange"]) - { - if(self.jid && self.accountID) - { - MLPasswordChangeTableViewController* pwchange = (MLPasswordChangeTableViewController*)segue.destinationViewController; - pwchange.xmppAccount = [[MLXMPPManager sharedInstance] getEnabledAccountForID:self.accountID]; - } - } -} - -#pragma mark - text input fielddelegate - --(void) textFieldDidBeginEditing:(UITextField*) textField -{ - self.currentTextField = textField; - if(textField.tag == 1) //user input field - { - if(textField.text.length > 0) { - UITextPosition* startPos = textField.beginningOfDocument; - UITextRange* newRange = [textField textRangeFromPosition:startPos toPosition:startPos]; - - // Set new range - [textField setSelectedTextRange:newRange]; - } - } -} - --(void) textFieldDidEndEditing:(UITextField*) textField -{ - switch (textField.tag) { - case 1: { - self.rosterName = textField.text; - self.rosterNameChanged = YES; - break; - } - case 2: { - self.jid = textField.text; - self.detailsChanged = YES; - break; - } - case 3: { - self.password = textField.text; - self.detailsChanged = YES; - break; - } - case 4: { - self.server = textField.text; - self.detailsChanged = YES; - break; - } - case 5: { - self.port = textField.text; - self.detailsChanged = YES; - break; - } - case 6: { - self.statusMessage = textField.text; - self.statusMessageChanged = YES; - break; - } - default: - break; - } -} - --(BOOL) textFieldShouldReturn:(UITextField*) textField -{ - [textField resignFirstResponder]; - return true; -} - - --(void) toggleSwitch:(id) sender -{ - UISwitch* toggle = (UISwitch*) sender; - - switch (toggle.tag) { - case 1: { - self.enabled = toggle.on; - break; - } - case 2: { - self.directTLS = toggle.on; - self.detailsChanged = YES; - break; - } - case 3: { - self.plainActivated = toggle.on; - self.detailsChanged = YES; - if(self.plainActivated) - { - UIAlertController* alert = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"Warning", @"") - message:NSLocalizedString(@"If you turn this on, you will no longer be safe from man-in-the-middle attacks. Such attacks enable the adversary to manipulate your incoming and outgoing messages, add their own OMEMO keys, change your account details and even know or change your password!\n\nYou should rather switch to another server than turning this on.", @"") preferredStyle:UIAlertControllerStyleAlert]; - [alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Understood", @"") style:UIAlertActionStyleDefault handler:^(UIAlertAction* action __unused) { - [alert dismissViewControllerAnimated:YES completion:nil]; - }]]; - [self presentViewController:alert animated:YES completion:nil]; - } - break; - } - } -} - -#pragma mark - doc picker --(void) pickImgFile:(id) sender -{ - [self presentViewController:self.imagePicker animated:YES completion:nil]; - return; -} - --(void) documentPicker:(UIDocumentPickerViewController*) controller didPickDocumentsAtURLs:(NSArray*) urls -{ - NSFileCoordinator* coordinator = [NSFileCoordinator new]; - [coordinator coordinateReadingItemAtURL:urls.firstObject options:NSFileCoordinatorReadingForUploading error:nil byAccessor:^(NSURL* _Nonnull newURL) { - NSData* data =[NSData dataWithContentsOfURL:newURL]; - UIImage* pickImg = [UIImage imageWithData:data]; - [self useAvatarImage:pickImg]; - }]; -} - --(void) getPhotoAction:(UIGestureRecognizer*) recognizer -{ - xmpp* account = [[MLXMPPManager sharedInstance] getEnabledAccountForID:self.accountID]; - if (!account) - return; - UIAlertController* actionControll = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"Select Action", @"") - message:nil preferredStyle:UIAlertControllerStyleActionSheet]; - -#if TARGET_OS_MACCATALYST - [self pickImgFile:nil]; -#else - UIImagePickerController* imagePicker = [UIImagePickerController new]; - imagePicker.delegate = self; - - UIAlertAction* cameraAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"Camera", @"") style:UIAlertActionStyleDefault handler:^(UIAlertAction* action __unused) { - imagePicker.sourceType = UIImagePickerControllerSourceTypeCamera; - [self presentViewController:imagePicker animated:YES completion:nil]; - }]; - - UIAlertAction* photosAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"Photos", @"") style:UIAlertActionStyleDefault handler:^(UIAlertAction* action __unused) { - imagePicker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary; - [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) { - if(granted) - { - dispatch_async(dispatch_get_main_queue(), ^{ - [self presentViewController:imagePicker animated:YES completion:nil]; - }); - } - }]; - }]; - - // Set image - [cameraAction setValue:[[UIImage systemImageNamed:@"camera"] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal] forKey:@"image"]; - [photosAction setValue:[[UIImage systemImageNamed:@"photo"] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal] forKey:@"image"]; - [actionControll addAction:cameraAction]; - [actionControll addAction:photosAction]; -#endif - - // Set image - [actionControll addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Cancel", @"") style:UIAlertActionStyleCancel handler:^(UIAlertAction* action __unused) { - [actionControll dismissViewControllerAnimated:YES completion:nil]; - }]]; - - actionControll.popoverPresentationController.sourceView = self.userAvatarImageView; - [self presentViewController:actionControll animated:YES completion:nil]; -} - --(void) imagePickerController:(UIImagePickerController*) picker didFinishPickingMediaWithInfo:(NSDictionary*) info -{ - NSString* mediaType = info[UIImagePickerControllerMediaType]; - if([mediaType isEqualToString:UTTypeImage.identifier]) { - UIImage* selectedImage = info[UIImagePickerControllerEditedImage]; - if(!selectedImage) selectedImage = info[UIImagePickerControllerOriginalImage]; - - TOCropViewController* cropViewController = [[TOCropViewController alloc] initWithImage:selectedImage]; - cropViewController.delegate = self; - cropViewController.transitioningDelegate = nil; - //set square aspect ratio and don't let the user change that (this is a avatar which should be square for maximum compatibility with other clients) - cropViewController.aspectRatioPreset = TOCropViewControllerAspectRatioPresetSquare; - cropViewController.aspectRatioLockEnabled = YES; - cropViewController.aspectRatioPickerButtonHidden = YES; - - UINavigationController* cropRootController = [[UINavigationController alloc] initWithRootViewController:cropViewController]; - [picker dismissViewControllerAnimated:YES completion:^{ - [self presentViewController:cropRootController animated:YES completion:nil]; - }]; - } - else - [picker dismissViewControllerAnimated:YES completion:nil]; -} - --(void) imagePickerControllerDidCancel:(UIImagePickerController*) picker -{ - [picker dismissViewControllerAnimated:YES completion:nil]; -} - --(void) useAvatarImage:(UIImage*) selectedImg -{ - /* - //small sample image - UIGraphicsImageRenderer* renderer = [[UIGraphicsImageRenderer alloc] initWithSize:CGSizeMake(200, 200)]; - selectedImg = [renderer imageWithActions:^(UIGraphicsImageRendererContext * _Nonnull context) { - [[UIColor darkGrayColor] setStroke]; - [context strokeRect:renderer.format.bounds]; - [[UIColor colorWithRed:158/255.0 green:215/255.0 blue:245/255.0 alpha:1] setFill]; - [context fillRect:CGRectMake(1, 1, 140, 140)]; - }]; - */ - - //check if conversion can be done and display error if not - if(selectedImg && UIImageJPEGRepresentation(selectedImg, 1.0)) - { - self.selectedAvatarImage = selectedImg; - [self.userAvatarImageView setImage:self.selectedAvatarImage]; - self.avatarChanged = YES; - } - else - { - UIAlertController* alert = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"Error", @"") - message:NSLocalizedString(@"Can't convert the image to jpeg format.", @"") preferredStyle:UIAlertControllerStyleAlert]; - [alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Close", @"") style:UIAlertActionStyleDefault handler:^(UIAlertAction* action __unused) { - [alert dismissViewControllerAnimated:YES completion:nil]; - }]]; - [self presentViewController:alert animated:YES completion:nil]; - } -} - - -#pragma mark -- TOCropViewController delagate - --(void) cropViewController:(nonnull TOCropViewController*) cropViewController didCropToImage:(UIImage* _Nonnull) image withRect:(CGRect) cropRect angle:(NSInteger) angle -{ - [self useAvatarImage:image]; - [self dismissViewControllerAnimated:YES completion:nil]; -} - -@end diff --git a/Monal/Classes/xmpp.h b/Monal/Classes/xmpp.h index 46c3ee4941..4e88040be6 100644 --- a/Monal/Classes/xmpp.h +++ b/Monal/Classes/xmpp.h @@ -231,7 +231,7 @@ typedef void (^monal_iq_handler_t)(XMPPIQ* _Nullable); -(void) bindResource:(NSString*) resource; -(void) initSession; -(void) sendDisplayMarkerForMessages:(NSArray*) unread; --(void) publishAvatar:(UIImage*) image; +-(void) publishAvatar:(UIImage* _Nullable) image; -(void) publishStatusMessage:(NSString*) message; -(void) delayIncomingMessageStanzasForArchiveJid:(NSString*) archiveJid; diff --git a/Monal/Classes/xmpp.m b/Monal/Classes/xmpp.m index 9efc854a7d..9fa3beca46 100644 --- a/Monal/Classes/xmpp.m +++ b/Monal/Classes/xmpp.m @@ -1855,6 +1855,10 @@ -(void) processInput:(MLXMLNode*) parsedStanza withDelayedReplay:(BOOL) delayedR NSMutableDictionary* accountDetails = [[DataLayer sharedInstance] detailsForAccount:self.accountID]; accountDetails[@"statusMessage"] = [presenceNode check:@"status#"] ? [presenceNode findFirst:@"status#"] : @""; [[DataLayer sharedInstance] updateAccounWithDictionary:accountDetails]; + //TODO: post a notification so the view can be refreshed + [[MLNotificationQueue currentQueue] postNotificationName:kMonalAccountSettingsRefresh object:self userInfo:@{ + @"accountID": self.accountID + }]; } } else diff --git a/Monal/Monal.xcodeproj/project.pbxproj b/Monal/Monal.xcodeproj/project.pbxproj index 873956623a..881360e47c 100644 --- a/Monal/Monal.xcodeproj/project.pbxproj +++ b/Monal/Monal.xcodeproj/project.pbxproj @@ -22,12 +22,10 @@ 261E542523A0A1D300394F59 /* monalxmpp.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 26CC579223A0867400ABB92A /* monalxmpp.framework */; }; 261E542623A0A1D300394F59 /* monalxmpp.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 26CC579223A0867400ABB92A /* monalxmpp.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 262797AF178A577300B85D94 /* MLContactCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 262797AE178A577300B85D94 /* MLContactCell.m */; }; - 262AEFE820AE756800498F82 /* MLMAMPrefTableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 262AEFE720AE756800498F82 /* MLMAMPrefTableViewController.m */; }; 262E51921AD8CAC600788351 /* MLButtonCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 262E51901AD8CAC600788351 /* MLButtonCell.m */; }; 262E51931AD8CAC600788351 /* MLButtonCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 262E51911AD8CAC600788351 /* MLButtonCell.xib */; }; 262E51971AD8CB7200788351 /* MLTextInputCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 262E51951AD8CB7200788351 /* MLTextInputCell.m */; }; 262E51981AD8CB7200788351 /* MLTextInputCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 262E51961AD8CB7200788351 /* MLTextInputCell.xib */; }; - 2636C43F177BD58C001CA71F /* XMPPEdit.m in Sources */ = {isa = PBXBuildFile; fileRef = 2636C43E177BD58C001CA71F /* XMPPEdit.m */; }; 2638008B2374816D00144929 /* NotificationService.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 260773C0232FC4E800BFD50F /* NotificationService.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 263DFAC02183D7160038E716 /* MLSelectionController.m in Sources */ = {isa = PBXBuildFile; fileRef = 263DFABE2183A59B0038E716 /* MLSelectionController.m */; }; 263DFAC32187D0E00038E716 /* MLLinkCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 263DFAC22187D0E00038E716 /* MLLinkCell.m */; }; @@ -215,7 +213,9 @@ C1F5C7AC2777621B0001F295 /* ContactResources.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1F5C7AB2777621B0001F295 /* ContactResources.swift */; }; C1F5C7AF2777638B0001F295 /* OrderedCollections in Frameworks */ = {isa = PBXBuildFile; productRef = C1F5C7AE2777638B0001F295 /* OrderedCollections */; }; D02192F32C89BB3800202A59 /* BlockedUsers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02192F22C89BB3800202A59 /* BlockedUsers.swift */; }; + D03C87F62CC5871800F0A2CD /* LoginCredentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03C87F52CC5871800F0A2CD /* LoginCredentials.swift */; }; D09B51F62C7F30DD008D725B /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 26B2A4BA1B73061400272E63 /* Images.xcassets */; }; + D0DBFABC2C9F25D5004B7C87 /* AccountSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DBFABB2C9F25D4004B7C87 /* AccountSettings.swift */; }; D0FA79B12C7E5C7400216D2A /* ServerDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FA79B02C7E5C7400216D2A /* ServerDetails.swift */; }; D7E74AF213445E39318BC648 /* Pods_MonalUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 29250DA62DD2322383585B2B /* Pods_MonalUITests.framework */; }; E89DD32525C6626400925F62 /* MLFileTransferDataCell.m in Sources */ = {isa = PBXBuildFile; fileRef = E89DD32025C6626300925F62 /* MLFileTransferDataCell.m */; }; @@ -343,8 +343,6 @@ 261A628A176C159000059090 /* AccountListController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AccountListController.m; sourceTree = ""; }; 262797AD178A577300B85D94 /* MLContactCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MLContactCell.h; sourceTree = ""; }; 262797AE178A577300B85D94 /* MLContactCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MLContactCell.m; sourceTree = ""; }; - 262AEFE620AE756800498F82 /* MLMAMPrefTableViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MLMAMPrefTableViewController.h; sourceTree = ""; }; - 262AEFE720AE756800498F82 /* MLMAMPrefTableViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MLMAMPrefTableViewController.m; sourceTree = ""; }; 262D9EA817921E82009292B4 /* XMPPMessage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XMPPMessage.h; sourceTree = ""; }; 262D9EA917921E82009292B4 /* XMPPMessage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = XMPPMessage.m; sourceTree = ""; }; 262D9EAB17924532009292B4 /* MLConstants.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MLConstants.h; sourceTree = ""; }; @@ -358,8 +356,6 @@ 2633CB42231CB816006D0277 /* MLMessageProcessor.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MLMessageProcessor.m; sourceTree = ""; }; 26352EA6177D222600E2C8FF /* MLXMPPManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MLXMPPManager.h; sourceTree = ""; }; 26352EA7177D222600E2C8FF /* MLXMPPManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MLXMPPManager.m; sourceTree = ""; }; - 2636C43D177BD58C001CA71F /* XMPPEdit.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XMPPEdit.h; sourceTree = ""; }; - 2636C43E177BD58C001CA71F /* XMPPEdit.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = XMPPEdit.m; sourceTree = ""; }; 263AFDFA209B3B35007F9CEE /* MLSignalStore.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MLSignalStore.h; sourceTree = ""; }; 263AFDFB209B3B35007F9CEE /* MLSignalStore.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MLSignalStore.m; sourceTree = ""; }; 263DFABD2183A59B0038E716 /* MLSelectionController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MLSelectionController.h; sourceTree = ""; }; @@ -758,6 +754,8 @@ C1F5C7A82775DA000001F295 /* MLContactSoftwareVersionInfo.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MLContactSoftwareVersionInfo.m; sourceTree = ""; }; C1F5C7AB2777621B0001F295 /* ContactResources.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactResources.swift; sourceTree = ""; }; D02192F22C89BB3800202A59 /* BlockedUsers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedUsers.swift; sourceTree = ""; }; + D03C87F52CC5871800F0A2CD /* LoginCredentials.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginCredentials.swift; sourceTree = ""; }; + D0DBFABB2C9F25D4004B7C87 /* AccountSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSettings.swift; sourceTree = ""; }; D0FA79B02C7E5C7400216D2A /* ServerDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerDetails.swift; sourceTree = ""; }; D310A8387B2EB10761312F77 /* Pods-NotificaionService.appstore.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificaionService.appstore.xcconfig"; path = "Target Support Files/Pods-NotificaionService/Pods-NotificaionService.appstore.xcconfig"; sourceTree = ""; }; D8D2595B2BE453296E59F1AF /* Pods-MonalUITests.appstore.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MonalUITests.appstore.xcconfig"; path = "Target Support Files/Pods-MonalUITests/Pods-MonalUITests.appstore.xcconfig"; sourceTree = ""; }; @@ -1045,14 +1043,12 @@ children = ( 261A6289176C159000059090 /* AccountListController.h */, 261A628A176C159000059090 /* AccountListController.m */, - 2636C43D177BD58C001CA71F /* XMPPEdit.h */, - 2636C43E177BD58C001CA71F /* XMPPEdit.m */, - 262AEFE620AE756800498F82 /* MLMAMPrefTableViewController.h */, - 262AEFE720AE756800498F82 /* MLMAMPrefTableViewController.m */, 26AAAAD82295742400200433 /* MLPasswordChangeTableViewController.h */, 26AAAAD92295742400200433 /* MLPasswordChangeTableViewController.m */, D0FA79B02C7E5C7400216D2A /* ServerDetails.swift */, D02192F22C89BB3800202A59 /* BlockedUsers.swift */, + D0DBFABB2C9F25D4004B7C87 /* AccountSettings.swift */, + D03C87F52CC5871800F0A2CD /* LoginCredentials.swift */, ); name = Accounts; sourceTree = ""; @@ -2065,6 +2061,7 @@ C18967C72B81F61B0073C7C5 /* ChannelMemberList.swift in Sources */, C114D13D2B15B903000FB99F /* ContactEntry.swift in Sources */, 841B6F1A297B18720074F9B7 /* AccountPicker.swift in Sources */, + D03C87F62CC5871800F0A2CD /* LoginCredentials.swift in Sources */, 3D65B791272350F0005A30F4 /* SwiftuiHelpers.swift in Sources */, C1A80DA424D9552400B99E01 /* MLChatViewHelper.m in Sources */, C117F7E22B0863B3001F2BC6 /* ContactPicker.swift in Sources */, @@ -2097,7 +2094,6 @@ 20D3611C2C10E12500E46587 /* BoardingCards.swift in Sources */, 3D06A515281FFCC000DDAE90 /* NotificationDebugging.swift in Sources */, 845D636B2AD4AEDA0066EFFB /* MediaViewer.swift in Sources */, - 2636C43F177BD58C001CA71F /* XMPPEdit.m in Sources */, 84FC375928981A5600634E3E /* PasswordMigration.swift in Sources */, E89DD32625C6626400925F62 /* MLFileTransferVideoCell.m in Sources */, 3DC5035C2822F5220064C8A7 /* OmemoKeysView.swift in Sources */, @@ -2112,11 +2108,11 @@ 952EBC802BAF72F300183DBF /* DebugView.swift in Sources */, 3D7D352328626CB80042C5E5 /* LoadingOverlay.swift in Sources */, 26D7C05E23D6AFD800CA123C /* MLChatInputContainer.m in Sources */, - 262AEFE820AE756800498F82 /* MLMAMPrefTableViewController.m in Sources */, 26AAE285179F7B0200271345 /* MLSettingCell.m in Sources */, 2664D28523F2312400CD4085 /* MLAccountPickerViewController.m in Sources */, 843AD3AB2AA55CE20036844D /* MLOgHtmlParser.swift in Sources */, 3D27D958290B0BC80014748B /* ContactRequestsMenu.swift in Sources */, + D0DBFABC2C9F25D5004B7C87 /* AccountSettings.swift in Sources */, 3D5A91422842B4AE008CE57E /* MemberList.swift in Sources */, 849248492AD4CEC400986C1A /* ZoomableContainer.swift in Sources */, 2644D4921FF0064C00F46AB5 /* MLChatImageCell.m in Sources */,