From f1276d0d1062557537c24178b94b7495aa9492da Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Fri, 3 May 2024 01:17:39 +0200 Subject: [PATCH 001/143] Update privacy settings submenu label --- Monal/Classes/PrivacySettings.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Monal/Classes/PrivacySettings.swift b/Monal/Classes/PrivacySettings.swift index 2070e716ee..b86f41c07a 100644 --- a/Monal/Classes/PrivacySettings.swift +++ b/Monal/Classes/PrivacySettings.swift @@ -102,7 +102,7 @@ struct PrivacySettings: View { .resizable() .aspectRatio(contentMode: .fit) .frame(width: 20, height: 20) - Text("Publishing") + Text("Publishing & Appearance") } } NavigationLink(destination: PreviewsScreen()) { @@ -189,7 +189,7 @@ struct PublishingScreen: View { } } } - .navigationBarTitle("Publishing & appearance", displayMode: .inline) + .navigationBarTitle("Publishing & Appearance", displayMode: .inline) } } From e36f478dcb645ebbfcfb37c45d2a080613fa1737 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Fri, 3 May 2024 02:19:53 +0200 Subject: [PATCH 002/143] --- 901 --- 6.3.1b1 From 20838b1fdf9eabec39690f5556e1124ca4d06621 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Fri, 3 May 2024 02:19:21 +0200 Subject: [PATCH 003/143] Make sure our localization update runs on a clean checkout --- .github/workflows/beta.build-push.yml | 29 +++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/.github/workflows/beta.build-push.yml b/.github/workflows/beta.build-push.yml index 3ecabc36b4..f0c49a4117 100644 --- a/.github/workflows/beta.build-push.yml +++ b/.github/workflows/beta.build-push.yml @@ -13,7 +13,7 @@ on: jobs: # This workflow contains a single job called "build" buildAndPublishBeta: - # The type of runner that the job will run on + name: "Build and Publish Beta Release" runs-on: self-hosted env: APP_NAME: "Monal" @@ -70,11 +70,6 @@ jobs: run: ./scripts/uploadNonAlpha.sh beta - name: Publish catalyst to appstore connect run: xcrun altool --upload-app --file ./Monal/build/app/Monal.pkg --type macos --asc-provider S8D843U34Y -u "$(cat /Users/ci/apple_connect_upload_mail.txt)" -p "$(cat /Users/ci/apple_connect_upload_secret.txt)" --primary-bundle-id org.monal-im.prod.catalyst.monal - - name: Update translations - run: | - chmod +x ./scripts/updateLocalization.sh - chmod +x ./scripts/xliff_extractor.py - ./scripts/updateLocalization.sh BUILDSERVER - uses: actions/upload-artifact@v4 with: name: monal-catalyst @@ -95,3 +90,25 @@ jobs: name: monal-ios-dsym path: Monal/build/ios_Monal.xcarchive/dSYMs if-no-files-found: error + + updateTranslations: + name: Update Translations using Beta-Branch + runs-on: self-hosted + needs: [buildAndPublishBeta] + env: + APP_NAME: "Monal" + APP_DIR: "Monal.app" + BUILD_TYPE: "Beta" + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + - uses: actions/checkout@v4 + with: + clean: true + submodules: true + - name: Checkout submodules + run: git submodule update -f --init --remote + - name: Update translations + run: | + chmod +x ./scripts/updateLocalization.sh + chmod +x ./scripts/xliff_extractor.py + ./scripts/updateLocalization.sh BUILDSERVER From 9a66934430363fee7b5941176c8d97587af13f88 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Sat, 4 May 2024 04:49:36 +0200 Subject: [PATCH 004/143] Better handling of objc block typedefs in swift --- Monal/Classes/MLConstants.h | 18 +++++++++++++----- Monal/Classes/SwiftHelpers.swift | 5 +++++ 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/Monal/Classes/MLConstants.h b/Monal/Classes/MLConstants.h index 9abd0df732..f74fd3d8bc 100644 --- a/Monal/Classes/MLConstants.h +++ b/Monal/Classes/MLConstants.h @@ -55,14 +55,22 @@ static const DDLogLevel ddLogLevel = LOG_LEVEL_STDOUT; #define BGFETCH_DEFAULT_INTERVAL 3600*3 #endif +// #define defineBlockType(name, returntype, ...) \ +// typedef returntype (^name)(__VA_ARGS__); \ +// name _Nonnull castTo_##name(id _Nonnull block) { return block; } +// +// #ifndef blocktypes +// defineBlockType(monal_new_void_block_t, void, void); +// #endif + @class MLContact; //some typedefs used throughout the project -typedef void (^contactCompletion)(MLContact* _Nonnull selectedContact); -typedef void (^accountCompletion)(NSInteger accountRow); -typedef void (^monal_void_block_t)(void); -typedef void (^monal_id_block_t)(id _Nonnull); -typedef void (^monal_upload_completion_t)(NSString* _Nullable url, NSString* _Nullable mimeType, NSNumber* _Nullable size, NSError* _Nullable error); +typedef void (^contactCompletion)(MLContact* _Nonnull selectedContact) NS_SWIFT_UNAVAILABLE("To be redefined in swift."); +typedef void (^accountCompletion)(NSInteger accountRow) NS_SWIFT_UNAVAILABLE("To be redefined in swift."); +typedef void (^monal_void_block_t)(void) NS_SWIFT_UNAVAILABLE("To be redefined in swift."); +typedef void (^monal_id_block_t)(id _Nonnull) NS_SWIFT_UNAVAILABLE("To be redefined in swift."); +typedef void (^monal_upload_completion_t)(NSString* _Nullable url, NSString* _Nullable mimeType, NSNumber* _Nullable size, NSError* _Nullable error) NS_SWIFT_UNAVAILABLE("To be redefined in swift."); typedef NS_ENUM(NSUInteger, MLAudioState) { MLAudioStateNormal, diff --git a/Monal/Classes/SwiftHelpers.swift b/Monal/Classes/SwiftHelpers.swift index ffd0887d55..fe9a382492 100644 --- a/Monal/Classes/SwiftHelpers.swift +++ b/Monal/Classes/SwiftHelpers.swift @@ -28,6 +28,10 @@ let BGFETCH_DEFAULT_INTERVAL = HelperTools.getObjcDefinedValue(.BGFETCH_DEFAULT_ public typealias monal_void_block_t = @convention(block) () -> Void; public typealias monal_id_block_t = @convention(block) (AnyObject?) -> Void; +public func objcCast(_ obj: Any) -> T { + return unsafeBitCast(obj as AnyObject, to:T.self) +} + public func unreachable(_ text: String = "unreachable", _ auxData: [String:AnyObject] = [String:AnyObject](), file: String = #file, line: Int = #line, function: String = #function) -> Never { DDLogError("unreachable: \(file) \(line) \(function)") HelperTools.mlAssert(withText:text, andUserData:auxData, andFile:(file as NSString).utf8String!, andLine:Int32(line), andFunc:(function as NSString).utf8String!) @@ -226,6 +230,7 @@ public struct defaultsDB { } } + @objcMembers public class SwiftHelpers: NSObject { public static func initSwiftHelpers() { From 4456b846f841a3b9fd3c039324d02f74d91359f0 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Fri, 3 May 2024 01:17:57 +0200 Subject: [PATCH 005/143] Implement MUC destroy --- Monal/Classes/GroupDetailsEdit.swift | 84 +++++++++++++++-- Monal/Classes/MLMucProcessor.h | 1 + Monal/Classes/MLMucProcessor.m | 129 ++++++++++++++++++++++----- 3 files changed, 188 insertions(+), 26 deletions(-) diff --git a/Monal/Classes/GroupDetailsEdit.swift b/Monal/Classes/GroupDetailsEdit.swift index 36fb1b39b9..5c47713ea1 100644 --- a/Monal/Classes/GroupDetailsEdit.swift +++ b/Monal/Classes/GroupDetailsEdit.swift @@ -6,7 +6,6 @@ // Copyright © 2024 monal-im.org. All rights reserved. // -import SwiftUI import _PhotosUI_SwiftUI struct GroupDetailsEdit: View { @@ -15,10 +14,28 @@ struct GroupDetailsEdit: View { @State private var showingSheetEditSubject = false @State private var inputImage: UIImage? @State private var showingImagePicker = false + @State private var showingDestroyConfirmation = false + @State private var alertPrompt = AlertPrompt(dismissLabel: Text("Close")) + @State private var showAlert = false + @State private var success = false @StateObject private var overlay = LoadingOverlayState() + @State private var successCallback: monal_void_block_t? private let account: xmpp private let ownAffiliation: String? + private func errorAlert(title: Text, message: Text = Text("")) { + alertPrompt.title = title + alertPrompt.message = message + showAlert = true + } + + private func successAlert(title: Text, message: Text = Text("")) { + alertPrompt.title = title + alertPrompt.message = message + showAlert = true + self.success = true // < dismiss entire view on close + } + init(contact: ObservableKVOWrapper, ownAffiliation: String?) { MLAssert(contact.isGroup) @@ -37,7 +54,7 @@ struct GroupDetailsEdit: View { Image(uiImage: contact.avatar) .resizable() .scaledToFit() - .accessibilityLabel((contact.obj.mucType == "group") ? "Group Avatar" : "Channel Avatar") + .accessibilityLabel((contact.mucType == "group") ? "Group Avatar" : "Channel Avatar") .frame(width: 150, height: 150, alignment: .center) .shadow(radius: 7) .onTapGesture { @@ -50,7 +67,7 @@ struct GroupDetailsEdit: View { } } } - + Section { if ownAffiliation == "owner" { Button(action: { @@ -72,7 +89,7 @@ struct GroupDetailsEdit: View { }) { HStack { Image(systemName: "pencil") - if contact.obj.mucType == "group" { + if contact.mucType == "group" { Text("Group description") } else { Text("Channel description") @@ -84,9 +101,66 @@ struct GroupDetailsEdit: View { LazyClosureView(EditGroupSubject(contact: contact)) } } + + if ownAffiliation == "owner" { + Section { + Button(action: { + showingDestroyConfirmation = true + }) { + if contact.mucType == "group" { + Text("Destroy Group").foregroundColor(.red) + } else { + Text("Destroy Channel").foregroundColor(.red) + } + } + .actionSheet(isPresented: $showingDestroyConfirmation) { + ActionSheet( + title: contact.mucType == "group" ? Text("Destroy Group") : Text("Destroy Channel"), + message: contact.mucType == "group" ? Text("Do you really want to destroy this group? Every member will be kicked out and it will be destroyed afterwards.") : Text("Do you really want to destroy this channel? Every member will be kicked out and it will be destroyed afterwards."), + buttons: [ + .cancel(), + .destructive( + Text("Yes"), + action: { + showLoadingOverlay(overlay, headline: contact.mucType == "group" ? NSLocalizedString("Destroying group...", comment: "") : NSLocalizedString("Destroying channel...", comment: "")) + self.account.mucProcessor.destroyRoom(contact.contactJid as String) + self.account.mucProcessor.addUIHandler({_data in let data = _data as! NSDictionary + hideLoadingOverlay(overlay) + let success : Bool = data["success"] as! Bool; + if success { + if let callback = data["callback"] { + self.successCallback = objcCast(callback) as monal_void_block_t + } + DDLogError("callback: \(String(describing:self.successCallback))") + successAlert(title: Text("Success"), message: contact.mucType == "group" ? Text("Successfully destroyed group.") : Text("Successfully destroyed channel.")) + } else { + errorAlert(title: Text("Error destroying group!"), message: Text(data["errorMessage"] as! String)) + } + }, forMuc:contact.contactJid) + } + ) + ] + ) + } + } + } } .addLoadingOverlay(overlay) - .navigationTitle((contact.obj.mucType == "group") ? "Edit group" : "Edit channel") + .navigationTitle((contact.mucType == "group") ? NSLocalizedString("Edit group", comment: "") : NSLocalizedString("Edit channel", comment: "")) + .alert(isPresented: $showAlert) { + Alert(title: alertPrompt.title, message: alertPrompt.message, dismissButton:.default(Text("Close"), action: { + showAlert = false + if self.success == true { + //close muc ui and leave chat ui of this muc + if let callback = self.successCallback { + callback() + } + if let activeChats = (UIApplication.shared.delegate as! MonalAppDelegate).activeChats { + activeChats.presentChat(with:nil) + } + } + })) + } .onChange(of:inputImage) { _ in showLoadingOverlay(overlay, headline: NSLocalizedString("Uploading image...", comment: "")) self.account.mucProcessor.publishAvatar(inputImage, forMuc: contact.contactJid) diff --git a/Monal/Classes/MLMucProcessor.h b/Monal/Classes/MLMucProcessor.h index f3d82f8f65..d8fd57c84b 100644 --- a/Monal/Classes/MLMucProcessor.h +++ b/Monal/Classes/MLMucProcessor.h @@ -28,6 +28,7 @@ NS_ASSUME_NONNULL_BEGIN //muc management methods -(NSString* _Nullable) generateMucJid; -(NSString* _Nullable) createGroup:(NSString*) room; +-(void) destroyRoom:(NSString*) room; -(void) changeNameOfMuc:(NSString*) room to:(NSString*) name; -(void) changeSubjectOfMuc:(NSString*) room to:(NSString*) subject; -(void) publishAvatar:(UIImage* _Nullable) image forMuc:(NSString*) room; diff --git a/Monal/Classes/MLMucProcessor.m b/Monal/Classes/MLMucProcessor.m index 1d67680c2d..667f36b3ef 100644 --- a/Monal/Classes/MLMucProcessor.m +++ b/Monal/Classes/MLMucProcessor.m @@ -23,7 +23,7 @@ #import "MLOMEMO.h" #import "MLImageManager.h" -#define CURRENT_MUC_STATE_VERSION @7 +#define CURRENT_MUC_STATE_VERSION @8 @interface MLMucProcessor() { @@ -33,6 +33,7 @@ @interface MLMucProcessor() NSMutableDictionary* _roomFeatures; NSMutableDictionary* _creating; NSMutableDictionary* _joining; + NSMutableSet* _destroying; NSMutableSet* _firstJoin; NSDate* _lastPing; NSMutableSet* _noUpdateBookmarks; @@ -75,6 +76,7 @@ -(id) initWithAccount:(xmpp*) account _roomFeatures = [NSMutableDictionary new]; _creating = [NSMutableDictionary new]; _joining = [NSMutableDictionary new]; + _destroying = [NSMutableSet new]; _firstJoin = [NSMutableSet new]; _uiHandler = [NSMutableDictionary new]; _lastPing = [NSDate date]; @@ -107,6 +109,7 @@ -(void) setInternalState:(NSDictionary*) state _roomFeatures = [state[@"roomFeatures"] mutableCopy]; _creating = [state[@"creating"] mutableCopy]; _joining = [state[@"joining"] mutableCopy]; + _destroying = [state[@"destroying"] mutableCopy]; _firstJoin = [state[@"firstJoin"] mutableCopy]; _lastPing = state[@"lastPing"]; _noUpdateBookmarks = [state[@"noUpdateBookmarks"] mutableCopy]; @@ -122,6 +125,7 @@ -(NSDictionary*) getInternalState @"roomFeatures": [_roomFeatures copy], @"creating": [_creating copy], @"joining": [_joining copy], + @"destroying": [_destroying copy], @"firstJoin": [_firstJoin copy], @"lastPing": _lastPing, @"noUpdateBookmarks": [_noUpdateBookmarks copy], @@ -148,6 +152,7 @@ -(void) handleResourceBound:(NSNotification*) notification NSDictionary* creatingCopy = [_creating copy]; for(NSString* room in creatingCopy) [self removeRoomFromCreating:room]; + _destroying = [NSMutableSet new]; //don't clear _firstJoin and _noUpdateBookmarks to make sure half-joined mucs are still added to muc bookmarks @@ -291,25 +296,35 @@ -(void) processPresence:(XMPPPresence*) presenceNode { DDLogVerbose(@"Got muc presence from full jid: %@", presenceNode.from); - //extract info if present (use an empty dict if no info is present) - NSMutableDictionary* item = [[presenceNode findFirst:@"{http://jabber.org/protocol/muc#user}x/item@@"] mutableCopy]; - if(!item) - item = [NSMutableDictionary new]; - - //update jid to be a bare jid and add muc nick to our dict - if(item[@"jid"]) - item[@"jid"] = [HelperTools splitJid:item[@"jid"]][@"user"]; - item[@"nick"] = presenceNode.fromResource; - - //handle participant updates - if([presenceNode check:@"/"] || item[@"affiliation"] == nil) - [[DataLayer sharedInstance] removeParticipant:item fromMuc:presenceNode.fromUser forAccountId:_account.accountNo]; + //don't handle this error if we ourselves are destroying this room + BOOL isDestroying = NO; + @synchronized(_stateLockObject) { + isDestroying = [_destroying containsObject:presenceNode.fromUser]; + } + if(!isDestroying) + { + //extract info if present (use an empty dict if no info is present) + NSMutableDictionary* item = [[presenceNode findFirst:@"{http://jabber.org/protocol/muc#user}x/item@@"] mutableCopy]; + if(!item) + item = [NSMutableDictionary new]; + + //update jid to be a bare jid and add muc nick to our dict + if(item[@"jid"]) + item[@"jid"] = [HelperTools splitJid:item[@"jid"]][@"user"]; + item[@"nick"] = presenceNode.fromResource; + + //handle participant updates + if([presenceNode check:@"/"] || item[@"affiliation"] == nil) + [[DataLayer sharedInstance] removeParticipant:item fromMuc:presenceNode.fromUser forAccountId:_account.accountNo]; + else + [[DataLayer sharedInstance] addParticipant:item toMuc:presenceNode.fromUser forAccountId:_account.accountNo]; + + //handle members updates + if(item[@"jid"] != nil) + [self handleMembersListUpdate:[presenceNode find:@"{http://jabber.org/protocol/muc#user}x/item@@"] forMuc:presenceNode.fromUser]; + } else - [[DataLayer sharedInstance] addParticipant:item toMuc:presenceNode.fromUser forAccountId:_account.accountNo]; - - //handle members updates - if(item[@"jid"] != nil) - [self handleMembersListUpdate:[presenceNode find:@"{http://jabber.org/protocol/muc#user}x/item@@"] forMuc:presenceNode.fromUser]; + DDLogDebug(@"Ignoring unavailable presences of room being destroyed by us..."); //handle muc status codes in reflected presences //this MUST be done after the above code to make sure the db correctly reflects our membership/participant status @@ -712,8 +727,16 @@ -(void) handleStatusCodes:(XMPPStanza*) node //(normally these have an additional status code that was already handled in the switch statement above if([node check:@"//{http://jabber.org/protocol/muc#user}x/destroy"]) { - [self handleError:[NSString stringWithFormat:NSLocalizedString(@"Group/Channel got destroyed: %@", @""), node.fromUser] forMuc:node.fromUser withNode:node andIsSevere:YES]; - [self deleteMuc:node.fromUser withBookmarksUpdate:YES keepBuddylistEntry:YES]; + //don't handle this error if we ourselves are destroying this room + BOOL isDestroying = NO; + @synchronized(_stateLockObject) { + isDestroying = [_destroying containsObject:node.fromUser]; + } + if(!isDestroying) + { + [self handleError:[NSString stringWithFormat:NSLocalizedString(@"Group/Channel got destroyed: %@", @""), node.fromUser] forMuc:node.fromUser withNode:node andIsSevere:YES]; + [self deleteMuc:node.fromUser withBookmarksUpdate:YES keepBuddylistEntry:YES]; + } } } else @@ -909,6 +932,70 @@ -(NSString* _Nullable) createGroup:(NSString*) room return room; } +-(void) destroyRoom:(NSString*) room +{ + MLAssert([[DataLayer sharedInstance] isBuddyMuc:room forAccount:_account.accountNo], @"Cannot destroy non-muc!", (@{@"room": room})); + + @synchronized(_stateLockObject) { + [_destroying addObject:room]; + } + + XMPPIQ* iqNode = [[XMPPIQ alloc] initWithType:kiqSetType to:room]; + [iqNode addChildNode:[[MLXMLNode alloc] initWithElement:@"query" andNamespace:@"http://jabber.org/protocol/muc#owner" withAttributes:@{} andChildren:@[ + [[MLXMLNode alloc] initWithElement:@"destroy" withAttributes:@{} andChildren:@[ + [[MLXMLNode alloc] initWithElement:@"reason" andData:@"Groupchat got destroyed"] + ] andData:nil], + ] andData:nil]]; + [_account sendIq:iqNode withHandler:$newHandlerWithInvalidation(self, handleRoomDestroyResult, handleRoomDestroyResultInvalidation, $ID(room))]; +} + +$$instance_handler(handleRoomDestroyResultInvalidation, account.mucProcessor, $$ID(xmpp*, account), $$ID(NSString*, room)) + DDLogError(@"Could not destroy room '%@' on account %@: invalidation called", room, account); + @synchronized(_stateLockObject) { + [_destroying removeObject:room]; + } + [self handleError:[NSString stringWithFormat:NSLocalizedString(@"Failed to destroy group/channel '%@': timeout", @""), room] forMuc:room withNode:nil andIsSevere:YES]; +$$ + +$$instance_handler(handleRoomDestroyResult, account.mucProcessor, $$ID(xmpp*, account), $$ID(XMPPIQ*, iqNode), $$ID(NSString*, room)) + @synchronized(_stateLockObject) { + [_destroying removeObject:room]; + } + if([iqNode check:@"/"]) + { + DDLogError(@"Failed to destroy room '%@' on account %@: %@", room, account, [iqNode findFirst:@"error"]); + [self handleError:[NSString stringWithFormat:NSLocalizedString(@"Failed to destroy group/channel '%@'", @""), room] forMuc:room withNode:iqNode andIsSevere:YES]; + return; + } + + DDLogInfo(@"Successfully destroyed room '%@' on account %@", room, account); + monal_id_block_t uiHandler = [self getUIHandlerForMuc:room]; + if(uiHandler) + { + //remove handler (it will only be called once) + [self removeUIHandlerForMuc:room]; + + DDLogInfo(@"Calling UI handler for muc %@...", room); + dispatch_async(dispatch_get_main_queue(), ^{ + uiHandler(@{ + @"success": @YES, + @"muc": room, + @"account": self->_account, + @"callback": ^{ + //don't even keep our bookmark in this case + [self deleteMuc:room withBookmarksUpdate:YES keepBuddylistEntry:NO]; + }, + }); + }); + } + else + { + //don't even keep our bookmark in this case + //this will handled by the ui handler callback if the ui was used to destroy this room and must be handled here otherwise + [self deleteMuc:room withBookmarksUpdate:YES keepBuddylistEntry:NO]; + } +$$ + -(void) join:(NSString*) room { [self sendDiscoQueryFor:room withJoin:YES andBookmarksUpdate:YES]; From ccb9bc9fdae672bc97f68e587e233074bf31683f Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Sat, 4 May 2024 04:51:52 +0200 Subject: [PATCH 006/143] Simplify addUIHandler() callbacks --- Monal/Classes/AddContactMenu.swift | 6 +++--- Monal/Classes/CreateGroupMenu.swift | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Monal/Classes/AddContactMenu.swift b/Monal/Classes/AddContactMenu.swift index 313570b9d8..108b8a0368 100644 --- a/Monal/Classes/AddContactMenu.swift +++ b/Monal/Classes/AddContactMenu.swift @@ -146,14 +146,14 @@ struct AddContactMenu: View { 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" { showLoadingOverlay(overlay, headline: NSLocalizedString("Adding Group/Channel...", comment: "")) - account.mucProcessor.addUIHandler({data in - let success : Bool = (data as! NSDictionary)["success"] as! Bool; + account.mucProcessor.addUIHandler({_data in let data = _data as! NSDictionary + let success : Bool = data["success"] as! Bool; hideLoadingOverlay(overlay) if success { self.newContact = MLContact.createContact(fromJid: jid, andAccountNo: account.accountNo) successAlert(title: Text("Success!"), message: Text(String.localizedStringWithFormat("Successfully joined group/channel %@!", jid))) } else { - errorAlert(title: Text("Error entering group/channel!"), message: Text((data as! NSDictionary)["errorMessage"] as! String)) + errorAlert(title: Text("Error entering group/channel!"), message: Text(data["errorMessage"] as! String)) } }, forMuc: jid) account.joinMuc(jid) diff --git a/Monal/Classes/CreateGroupMenu.swift b/Monal/Classes/CreateGroupMenu.swift index 1c99b7474c..aa3fa5b102 100644 --- a/Monal/Classes/CreateGroupMenu.swift +++ b/Monal/Classes/CreateGroupMenu.swift @@ -86,8 +86,8 @@ struct CreateGroupMenu: View { } return } - self.selectedAccount!.mucProcessor.addUIHandler({data in - let success : Bool = (data as! NSDictionary)["success"] as! Bool; + self.selectedAccount!.mucProcessor.addUIHandler({_data in let data = _data as! NSDictionary + let success : Bool = data["success"] as! Bool; if success { self.selectedAccount!.mucProcessor.changeName(ofMuc: roomJid, to: self.groupName) for user in self.selectedContacts { @@ -102,7 +102,7 @@ struct CreateGroupMenu: View { } } else { hideLoadingOverlay(overlay) - errorAlert(title: Text("Error creating group!"), message: Text((data as! NSDictionary)["errorMessage"] as! String)) + errorAlert(title: Text("Error creating group!"), message: Text(data["errorMessage"] as! String)) } }, forMuc: roomJid) }, label: { From 4dadece88aba14bc690ae49b35308c4ef89a5d25 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Sat, 4 May 2024 06:55:26 +0200 Subject: [PATCH 007/143] Fix members list to also invite new members to muc --- Monal/Classes/MemberList.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Monal/Classes/MemberList.swift b/Monal/Classes/MemberList.swift index 472e10a80a..c0d7c4e1d4 100644 --- a/Monal/Classes/MemberList.swift +++ b/Monal/Classes/MemberList.swift @@ -109,6 +109,7 @@ struct MemberList: View { if !previousMemberList.contains(member) { // add selected group member with affiliation member affiliationChangeAction(member, affiliation: "member") + self.account.mucProcessor.inviteUser(member.contactJid, inMuc: self.group.contactJid) } } } From e16f3e345be0feb304ce37731bda23770a5db0c6 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Sat, 4 May 2024 08:36:51 +0200 Subject: [PATCH 008/143] Allow multiline group subjects when editing --- Monal/Classes/EditGroupSubject.swift | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Monal/Classes/EditGroupSubject.swift b/Monal/Classes/EditGroupSubject.swift index 7dd8e816ec..7647694151 100644 --- a/Monal/Classes/EditGroupSubject.swift +++ b/Monal/Classes/EditGroupSubject.swift @@ -12,9 +12,9 @@ struct EditGroupSubject: View { @StateObject var contact: ObservableKVOWrapper private let account: xmpp? @State private var subject: String - @State private var isEditingSubject: Bool = false @Environment(\.presentationMode) var presentationMode + //@Environment(\.dismiss) var dismiss init(contact: ObservableKVOWrapper) { MLAssert(contact.isGroup, "contact must be a muc") @@ -28,8 +28,8 @@ struct EditGroupSubject: View { NavigationView { VStack { Form { - Section(header: Text("Group Description")) { - TextField(NSLocalizedString("Group Description (optional)", comment: "placeholder when editing a group description"), text: $subject, onEditingChanged: { isEditingSubject = $0 }) + Section(header: Text("Group Description (optional)")) { + TextEditor(text: $subject) .multilineTextAlignment(.leading) .applyClosure { view in if #available(iOS 16.0, *) { @@ -38,7 +38,6 @@ struct EditGroupSubject: View { view } } - .addClearButton(isEditing: isEditingSubject, text:$subject) } } } From 64c3a5bfc4bb52aaf52dbbee78c98e7e8a426c82 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Sat, 4 May 2024 08:37:37 +0200 Subject: [PATCH 009/143] Rework whole muc edit ui to be more modern --- Monal/Classes/ContactDetails.swift | 275 +++++++++++++++++++---- Monal/Classes/ContactDetailsHeader.swift | 99 -------- Monal/Classes/EditGroupName.swift | 53 ----- Monal/Classes/GroupDetailsEdit.swift | 178 --------------- Monal/Classes/MLContact.h | 1 + Monal/Classes/MLContact.m | 32 ++- Monal/Classes/MLMessageProcessor.m | 8 +- Monal/Classes/MLMucProcessor.m | 55 ++++- Monal/Monal.xcodeproj/project.pbxproj | 12 - 9 files changed, 318 insertions(+), 395 deletions(-) delete mode 100644 Monal/Classes/ContactDetailsHeader.swift delete mode 100644 Monal/Classes/EditGroupName.swift delete mode 100644 Monal/Classes/GroupDetailsEdit.swift diff --git a/Monal/Classes/ContactDetails.swift b/Monal/Classes/ContactDetails.swift index a4fdd48f8b..74d0403f20 100644 --- a/Monal/Classes/ContactDetails.swift +++ b/Monal/Classes/ContactDetails.swift @@ -6,14 +6,11 @@ // Copyright © 2021 Monal.im. All rights reserved. // -import UIKit -import SwiftUI -import monalxmpp - struct ContactDetails: View { var delegate: SheetDismisserProtocol private var account: xmpp - private var isGroupModerator = false + private var ownRole: String + private var ownAffiliation: String @StateObject var contact: ObservableKVOWrapper @State private var showingBlockContactConfirmation = false @State private var showingCannotBlockAlert = false @@ -24,6 +21,15 @@ struct ContactDetails: View { @State private var showingCannotEncryptAlert = false @State private var showingShouldDisableEncryptionAlert = false @State private var isEditingNickname = false + @State private var inputImage: UIImage? + @State private var showingImagePicker = false + @State private var showingSheetEditSubject = false + @State private var showingDestroyConfirmation = false + @State private var alertPrompt = AlertPrompt(dismissLabel: Text("Close")) + @State private var showAlert = false + @State private var success = false + @State private var successCallback: monal_void_block_t? + @StateObject private var overlay = LoadingOverlayState() init(delegate: SheetDismisserProtocol, contact: ObservableKVOWrapper) { self.delegate = delegate @@ -31,24 +37,148 @@ struct ContactDetails: View { self.account = MLXMPPManager.sharedInstance().getConnectedAccount(forID: contact.accountId)! if contact.isGroup { - let ownRole = DataLayer.sharedInstance().getOwnRole(inGroupOrChannel: contact.obj) ?? "none" - self.isGroupModerator = (ownRole == "moderator") + self.ownRole = DataLayer.sharedInstance().getOwnRole(inGroupOrChannel: contact.obj) ?? "none" + self.ownAffiliation = DataLayer.sharedInstance().getOwnAffiliation(inGroupOrChannel:contact.obj) ?? "none" + } else { + self.ownRole = "none" + self.ownAffiliation = "none" } } + private func errorAlert(title: Text, message: Text = Text("")) { + alertPrompt.title = title + alertPrompt.message = message + showAlert = true + } + + private func successAlert(title: Text, message: Text = Text("")) { + alertPrompt.title = title + alertPrompt.message = message + showAlert = true + self.success = true // < dismiss entire view on close + } + var body: some View { Form { Section { - ContactDetailsHeader(delegate:delegate, contact:contact) + VStack(spacing: 20) { + Image(uiImage: contact.avatar) + .resizable() + .scaledToFit() + .applyClosure {view in + if contact.isGroup { + if ownAffiliation == "owner" { + view.accessibilityLabel((contact.mucType == "group") ? "Change Group Avatar" : "Change Channel Avatar") + .onTapGesture { + showingImagePicker = true + } + } else { + view.accessibilityLabel((contact.mucType == "group") ? "Group Avatar" : "Channel Avatar") + } + } else { + view.accessibilityLabel("Avatar") + } + } + .frame(width: 150, height: 150, alignment: .center) + .shadow(radius: 7) + .sheet(isPresented:$showingImagePicker) { + ImagePicker(image:$inputImage) + } + + + Button { + UIPasteboard.general.setValue(contact.contactJid as String, forPasteboardType:UTType.utf8PlainText.identifier as String) + UIAccessibility.post(notification: .announcement, argument: "JID Copied") + } label: { + HStack { + Text(contact.contactJid as String) + + Image(systemName: "doc.on.doc") + .foregroundColor(.primary) + .accessibilityHidden(true) + } + .accessibilityHint("Copies JID") + } + .buttonStyle(.borderless) + + + //only show account jid if more than one is configured + if MLXMPPManager.sharedInstance().connectedXMPP.count > 1 && !contact.isSelfChat { + Text("Account: \(account.connectionProperties.identity.jid)") + } + + if !contact.isSelfChat && !contact.isGroup { + if let lastInteractionTime = contact.lastInteractionTime as Date? { + if lastInteractionTime.timeIntervalSince1970 > 0 { + Text(String(format: NSLocalizedString("Last seen: %@", comment: ""), + DateFormatter.localizedString(from: lastInteractionTime, dateStyle: DateFormatter.Style.short, timeStyle: DateFormatter.Style.short))) + } else { + Text(String(format: NSLocalizedString("Last seen: %@", comment: ""), NSLocalizedString("now", comment: ""))) + } + } else { + Text(String(format: NSLocalizedString("Last seen: %@", comment: ""), NSLocalizedString("unknown", comment: ""))) + } + } + + if !contact.isGroup && (contact.statusMessage as String).count > 0 { + VStack { + Text("Status message:") + Text(contact.statusMessage as String) + .fixedSize(horizontal: false, vertical: true) + } + } + + if contact.isGroup && ((contact.groupSubject as String).count > 0 || ownRole == "moderator") { + VStack { + if ownRole == "moderator" { + Button { + showingSheetEditSubject.toggle() + } label: { + if contact.obj.mucType == "group" { + HStack { + Text("Group subject:") + Spacer().frame(width:8) + Image(systemName: "pencil") + .foregroundColor(.primary) + .accessibilityHidden(true) + } + .accessibilityHint("Edit Group Subject") + } else { + HStack { + Text("Channel subject:") + Spacer().frame(width:8) + Image(systemName: "pencil") + .foregroundColor(.primary) + .accessibilityHidden(true) + } + .accessibilityHint("Edit Channel Subject") + } + } + .buttonStyle(.borderless) + .sheet(isPresented: $showingSheetEditSubject) { + LazyClosureView(EditGroupSubject(contact: contact)) + } + } else { + Text("Group subject:") + } + + Text(contact.groupSubject as String) + .fixedSize(horizontal: false, vertical: true) + } + } + } + .foregroundColor(.primary) + .padding([.top, .bottom]) + .frame(maxWidth: .infinity) } // info/nondestructive buttons Section { Button { - if(contact.isGroup) { - if(!contact.isMuted && !contact.isMentionOnly) { + if contact.isGroup { + if !contact.isMuted && !contact.isMentionOnly { contact.obj.toggleMentionOnly(true) - } else if(!contact.isMuted && contact.isMentionOnly) { + } else if !contact.isMuted && contact.isMentionOnly { contact.obj.toggleMentionOnly(false) contact.obj.toggleMute(true) } else { @@ -59,14 +189,14 @@ struct ContactDetails: View { contact.obj.toggleMute(!contact.isMuted) } } label: { - if(contact.isMuted) { + if contact.isMuted { Label { contact.isGroup ? Text("Notifications disabled") : Text("Contact is muted") } icon: { Image(systemName: "bell.slash.fill") .foregroundColor(.red) } - } else if(contact.isGroup && contact.isMentionOnly) { + } else if contact.isGroup && contact.isMentionOnly { Label { Text("Notify only when mentioned") } icon: { @@ -83,9 +213,9 @@ struct ContactDetails: View { } #if !DISABLE_OMEMO - if((!contact.isGroup || (contact.isGroup && contact.mucType == "group")) && !HelperTools.isContactBlacklisted(forEncryption:contact.obj)) { + if (!contact.isGroup || (contact.isGroup && contact.mucType == "group")) && !HelperTools.isContactBlacklisted(forEncryption:contact.obj) { Button { - if(contact.isEncrypted) { + if contact.isEncrypted { showingShouldDisableEncryptionAlert = true } else { showingCannotEncryptAlert = !contact.obj.toggleEncryption(!contact.isEncrypted) @@ -132,7 +262,14 @@ struct ContactDetails: View { } #endif - if(!contact.isGroup && !contact.isSelfChat) { + if contact.isGroup && ownAffiliation == "owner" { + let label = contact.obj.mucType == "group" ? NSLocalizedString("Rename Group", comment:"") : NSLocalizedString("Rename Channel", comment:"") + TextField(label, text: $contact.fullNameView, onEditingChanged: { + isEditingNickname = $0 + }) + .accessibilityLabel(contact.obj.mucType == "group" ? "Group name" : "Channel name") + .addClearButton(isEditing: isEditingNickname, text: $contact.fullNameView) + } else if !contact.isGroup && !contact.isSelfChat { TextField(NSLocalizedString("Rename Contact", comment: "placeholder text in contact details"), text: $contact.nickNameView, onEditingChanged: { isEditingNickname = $0 }) @@ -148,22 +285,22 @@ struct ContactDetails: View { Text("Pin Chat") } - if(contact.obj.isGroup && contact.obj.mucType == "group") { + if contact.obj.isGroup && contact.obj.mucType == "group" { NavigationLink(destination: LazyClosureView(MemberList(mucContact:contact))) { Text("Group Members") } - } else if(contact.obj.isGroup && contact.obj.mucType == "channel") { + } else if contact.obj.isGroup && contact.obj.mucType == "channel" { NavigationLink(destination: LazyClosureView(ChannelMemberList(mucContact:contact))) { Text("Channel Members") } } #if !DISABLE_OMEMO - if(!HelperTools.isContactBlacklisted(forEncryption:contact.obj)) { - if(!contact.isGroup) { + if !HelperTools.isContactBlacklisted(forEncryption:contact.obj) { + if !contact.isGroup { NavigationLink(destination: LazyClosureView(OmemoKeys(contact: contact))) { contact.isSelfChat ? Text("Own Encryption Keys") : Text("Encryption Keys") } - } else if(contact.mucType == "group") { + } else if contact.mucType == "group" { NavigationLink(destination: LazyClosureView(OmemoKeys(contact: contact))) { Text("Encryption Keys") } @@ -171,7 +308,7 @@ struct ContactDetails: View { } #endif - if(!contact.isGroup && !contact.isSelfChat) { + if !contact.isGroup && !contact.isSelfChat { NavigationLink(destination: LazyClosureView(ContactResources(contact: contact))) { Text("Resources") } @@ -195,13 +332,13 @@ struct ContactDetails: View { Section { // the destructive section... if !contact.isSelfChat { Button(action: { - if(!contact.isBlocked) { + if !contact.isBlocked { showingBlockContactConfirmation = true } else { showingCannotBlockAlert = !contact.obj.toggleBlocked(!contact.isBlocked) } }) { - if(!contact.isBlocked) { + if !contact.isBlocked { Text("Block Contact") .foregroundColor(.red) } else { @@ -228,12 +365,12 @@ struct ContactDetails: View { } Group { - if(contact.isInRoster) { + if contact.isInRoster { Button(action: { showingRemoveContactConfirmation = true }) { - if(contact.isGroup) { - if(contact.mucType == "group") { + if contact.isGroup { + if contact.mucType == "group" { Text("Leave Group") .foregroundColor(.red) } else { @@ -265,8 +402,8 @@ struct ContactDetails: View { Button(action: { showingAddContactConfirmation = true }) { - if(contact.isGroup) { - if(contact.mucType == "group") { + if contact.isGroup { + if contact.mucType == "group" { Text("Join Group") } else { Text("Join Channel") @@ -294,11 +431,54 @@ struct ContactDetails: View { } } + if ownAffiliation == "owner" { + Section { + Button(action: { + showingDestroyConfirmation = true + }) { + if contact.mucType == "group" { + Text("Destroy Group").foregroundColor(.red) + } else { + Text("Destroy Channel").foregroundColor(.red) + } + } + .actionSheet(isPresented: $showingDestroyConfirmation) { + ActionSheet( + title: contact.mucType == "group" ? Text("Destroy Group") : Text("Destroy Channel"), + message: contact.mucType == "group" ? Text("Do you really want to destroy this group? Every member will be kicked out and it will be destroyed afterwards.") : Text("Do you really want to destroy this channel? Every member will be kicked out and it will be destroyed afterwards."), + buttons: [ + .cancel(), + .destructive( + Text("Yes"), + action: { + showLoadingOverlay(overlay, headline: contact.mucType == "group" ? NSLocalizedString("Destroying group...", comment: "") : NSLocalizedString("Destroying channel...", comment: "")) + self.account.mucProcessor.destroyRoom(contact.contactJid as String) + self.account.mucProcessor.addUIHandler({_data in let data = _data as! NSDictionary + hideLoadingOverlay(overlay) + let success : Bool = data["success"] as! Bool; + if success { + if let callback = data["callback"] { + self.successCallback = objcCast(callback) as monal_void_block_t + } + DDLogError("callback: \(String(describing:self.successCallback))") + successAlert(title: Text("Success"), message: contact.mucType == "group" ? Text("Successfully destroyed group.") : Text("Successfully destroyed channel.")) + } else { + errorAlert(title: Text("Error destroying group!"), message: Text(data["errorMessage"] as! String)) + } + }, forMuc:contact.contactJid) + } + ) + ] + ) + } + } + } + Button(action: { showingClearHistoryConfirmation = true }) { - if(contact.isGroup) { - if(contact.obj.mucType == "group") { + if contact.isGroup { + if contact.obj.mucType == "group" { Text("Clear chat history of this group") } else { Text("Clear chat history of this channel") @@ -329,8 +509,7 @@ struct ContactDetails: View { //omemo debug stuff, should be removed in a few months Section { // only display omemo session reset button on 1:1 and private groups - if(contact.obj.isGroup == false || (contact.isGroup && contact.mucType == "group")) - { + if contact.obj.isGroup == false || (contact.isGroup && contact.mucType == "group") { Button(action: { showingResetOmemoSessionConfirmation = true }) { @@ -357,20 +536,28 @@ struct ContactDetails: View { #endif } .frame(maxWidth: .infinity, maxHeight: .infinity) + .addLoadingOverlay(overlay) .navigationBarTitle(contact.contactDisplayName as String, displayMode:.inline) - .applyClosure { view in - if contact.isGroup && isGroupModerator && self.account.accountState.rawValue >= xmppState.stateBound.rawValue { - view.toolbar { - ToolbarItem(placement:.navigationBarTrailing) { - let ownAffiliation = DataLayer.sharedInstance().getOwnAffiliation(inGroupOrChannel:contact.obj) ?? "none" - NavigationLink(destination:LazyClosureView(GroupDetailsEdit(contact:contact, ownAffiliation:ownAffiliation))) { - Text("Edit") - } + .alert(isPresented: $showAlert) { + Alert(title: alertPrompt.title, message: alertPrompt.message, dismissButton:.default(Text("Close"), action: { + showAlert = false + if self.success == true { + //close muc ui and leave chat ui of this muc + if let callback = self.successCallback { + callback() + } + if let activeChats = (UIApplication.shared.delegate as! MonalAppDelegate).activeChats { + activeChats.presentChat(with:nil) } } - } else { - view - } + })) + } + .onChange(of:inputImage) { _ in + showLoadingOverlay(overlay, headline: NSLocalizedString("Uploading image...", comment: "")) + self.account.mucProcessor.publishAvatar(inputImage, forMuc: contact.contactJid) + } + .onChange(of:contact.avatar as UIImage) { _ in + hideLoadingOverlay(overlay) } } } diff --git a/Monal/Classes/ContactDetailsHeader.swift b/Monal/Classes/ContactDetailsHeader.swift deleted file mode 100644 index e8676bd51d..0000000000 --- a/Monal/Classes/ContactDetailsHeader.swift +++ /dev/null @@ -1,99 +0,0 @@ -// -// ContactDetailsHeader.swift -// ContactDetailsHeader -// -// Created by Friedrich Altheide on 03.09.21. -// Copyright © 2021 Monal.im. All rights reserved. -// - -import MobileCoreServices -import UniformTypeIdentifiers -import SwiftUI -import monalxmpp - -struct ContactDetailsHeader: View { - var delegate: SheetDismisserProtocol - @StateObject var contact: ObservableKVOWrapper - @State private var navigationAction: String? - - var body: some View { - VStack(spacing: 20) { - Image(uiImage: contact.avatar) - .resizable() - .scaledToFit() - .accessibilityLabel("Avatar") - .frame(width: 150, height: 150, alignment: .center) - .shadow(radius: 7) - - - Button { - UIPasteboard.general.setValue(contact.contactJid as String, forPasteboardType:UTType.utf8PlainText.identifier as String) - UIAccessibility.post(notification: .announcement, argument: "JID Copied") - } label: { - HStack { - Text(contact.contactJid as String) - - Image(systemName: "doc.on.doc") - .foregroundColor(.primary) - .accessibilityHidden(true) - } - .accessibilityHint("Copies JID") - } - .buttonStyle(.borderless) - - - //only show account jid if more than one is configured - if MLXMPPManager.sharedInstance().connectedXMPP.count > 1 && !contact.isSelfChat { - Text("Account: \(MLXMPPManager.sharedInstance().getConnectedAccount(forID:contact.accountId)!.connectionProperties.identity.jid)") - } - - if !contact.isSelfChat && !contact.isGroup { - if let lastInteractionTime = contact.lastInteractionTime as Date? { - if lastInteractionTime.timeIntervalSince1970 > 0 { - Text(String(format: NSLocalizedString("Last seen: %@", comment: ""), - DateFormatter.localizedString(from: lastInteractionTime, dateStyle: DateFormatter.Style.short, timeStyle: DateFormatter.Style.short))) - } else { - Text(String(format: NSLocalizedString("Last seen: %@", comment: ""), NSLocalizedString("now", comment: ""))) - } - } else { - Text(String(format: NSLocalizedString("Last seen: %@", comment: ""), NSLocalizedString("unknown", comment: ""))) - } - } - - if(!contact.isGroup && (contact.statusMessage as String).count > 0) { - VStack { - Text("Status message:") - Text(contact.statusMessage as String) - .fixedSize(horizontal: false, vertical: true) - } - } - - if(contact.isGroup && (contact.groupSubject as String).count > 0) { - VStack { - if(contact.obj.mucType == "group") { - Text("Group subject:") - } else { - Text("Channel subject:") - } - - Text(contact.groupSubject as String) - .fixedSize(horizontal: false, vertical: true) - } - } - } - .foregroundColor(.primary) - .padding([.top, .bottom]) - .frame(maxWidth: .infinity) - } -} - -struct ContactDetailsHeader_Previews: PreviewProvider { - static var delegate = SheetDismisserProtocol() - static var previews: some View { - ContactDetailsHeader(delegate:delegate, contact:ObservableKVOWrapper(MLContact.makeDummyContact(0))) - ContactDetailsHeader(delegate:delegate, contact:ObservableKVOWrapper(MLContact.makeDummyContact(1))) - ContactDetailsHeader(delegate:delegate, contact:ObservableKVOWrapper(MLContact.makeDummyContact(2))) - ContactDetailsHeader(delegate:delegate, contact:ObservableKVOWrapper(MLContact.makeDummyContact(3))) - ContactDetailsHeader(delegate:delegate, contact:ObservableKVOWrapper(MLContact.makeDummyContact(4))) - } -} diff --git a/Monal/Classes/EditGroupName.swift b/Monal/Classes/EditGroupName.swift deleted file mode 100644 index abadd364d9..0000000000 --- a/Monal/Classes/EditGroupName.swift +++ /dev/null @@ -1,53 +0,0 @@ -// -// EditGroupName.swift -// Monal -// -// Created by Friedrich Altheide on 24.02.24. -// Copyright © 2024 monal-im.org. All rights reserved. -// - -import SwiftUI - -struct EditGroupName: View { - @StateObject var contact: ObservableKVOWrapper - private let account: xmpp? - @State private var groupName: String - @State private var isEditingGroupName: Bool = false - - @Environment(\.presentationMode) var presentationMode - - init(contact: ObservableKVOWrapper) { - MLAssert(contact.isGroup, "contact must be a muc") - - _groupName = State(wrappedValue: contact.obj.contactDisplayName) - _contact = StateObject(wrappedValue: contact) - self.account = MLXMPPManager.sharedInstance().getConnectedAccount(forID: contact.accountId)! as xmpp - } - - var body: some View { - - NavigationView { - Form { - Section(header: Text("Group name")) { - TextField(NSLocalizedString("Group Name (optional)", comment: "placeholder when editing a group name"), text: $groupName, onEditingChanged: { isEditingGroupName = $0 }) - .autocorrectionDisabled() - .autocapitalization(.none) - .addClearButton(isEditing: isEditingGroupName, text:$groupName) - } - } - .toolbar { - ToolbarItem(placement: .cancellationAction) { - Button("Abort") { - self.presentationMode.wrappedValue.dismiss() - } - } - ToolbarItem(placement: .confirmationAction) { - Button("Done") { - self.account!.mucProcessor.changeName(ofMuc: contact.contactJid, to: self.groupName) - self.presentationMode.wrappedValue.dismiss() - } - } - } - } - } -} diff --git a/Monal/Classes/GroupDetailsEdit.swift b/Monal/Classes/GroupDetailsEdit.swift deleted file mode 100644 index 5c47713ea1..0000000000 --- a/Monal/Classes/GroupDetailsEdit.swift +++ /dev/null @@ -1,178 +0,0 @@ -// -// GroupDetailsEdit.swift -// Monal -// -// Created by Friedrich Altheide on 23.02.24. -// Copyright © 2024 monal-im.org. All rights reserved. -// - -import _PhotosUI_SwiftUI - -struct GroupDetailsEdit: View { - @StateObject var contact: ObservableKVOWrapper - @State private var showingSheetEditName = false - @State private var showingSheetEditSubject = false - @State private var inputImage: UIImage? - @State private var showingImagePicker = false - @State private var showingDestroyConfirmation = false - @State private var alertPrompt = AlertPrompt(dismissLabel: Text("Close")) - @State private var showAlert = false - @State private var success = false - @StateObject private var overlay = LoadingOverlayState() - @State private var successCallback: monal_void_block_t? - private let account: xmpp - private let ownAffiliation: String? - - private func errorAlert(title: Text, message: Text = Text("")) { - alertPrompt.title = title - alertPrompt.message = message - showAlert = true - } - - private func successAlert(title: Text, message: Text = Text("")) { - alertPrompt.title = title - alertPrompt.message = message - showAlert = true - self.success = true // < dismiss entire view on close - } - - init(contact: ObservableKVOWrapper, ownAffiliation: String?) { - MLAssert(contact.isGroup) - - _contact = StateObject(wrappedValue: contact) - _inputImage = State(initialValue: contact.avatar) - self.account = MLXMPPManager.sharedInstance().getConnectedAccount(forID: contact.accountId)! as xmpp - self.ownAffiliation = ownAffiliation - } - - var body: some View { - Form { - if ownAffiliation == "owner" { - Section { - HStack { - Spacer() - Image(uiImage: contact.avatar) - .resizable() - .scaledToFit() - .accessibilityLabel((contact.mucType == "group") ? "Group Avatar" : "Channel Avatar") - .frame(width: 150, height: 150, alignment: .center) - .shadow(radius: 7) - .onTapGesture { - showingImagePicker = true - } - Spacer() - } - .sheet(isPresented:$showingImagePicker) { - ImagePicker(image:$inputImage) - } - } - } - - Section { - if ownAffiliation == "owner" { - Button(action: { - showingSheetEditName.toggle() - }) { - HStack { - Image(systemName: "person.2") - Text(contact.contactDisplayName as String) - Spacer() - } - } - .sheet(isPresented: $showingSheetEditName) { - LazyClosureView(EditGroupName(contact: contact)) - } - } - - Button(action: { - showingSheetEditSubject.toggle() - }) { - HStack { - Image(systemName: "pencil") - if contact.mucType == "group" { - Text("Group description") - } else { - Text("Channel description") - } - Spacer() - } - } - .sheet(isPresented: $showingSheetEditSubject) { - LazyClosureView(EditGroupSubject(contact: contact)) - } - } - - if ownAffiliation == "owner" { - Section { - Button(action: { - showingDestroyConfirmation = true - }) { - if contact.mucType == "group" { - Text("Destroy Group").foregroundColor(.red) - } else { - Text("Destroy Channel").foregroundColor(.red) - } - } - .actionSheet(isPresented: $showingDestroyConfirmation) { - ActionSheet( - title: contact.mucType == "group" ? Text("Destroy Group") : Text("Destroy Channel"), - message: contact.mucType == "group" ? Text("Do you really want to destroy this group? Every member will be kicked out and it will be destroyed afterwards.") : Text("Do you really want to destroy this channel? Every member will be kicked out and it will be destroyed afterwards."), - buttons: [ - .cancel(), - .destructive( - Text("Yes"), - action: { - showLoadingOverlay(overlay, headline: contact.mucType == "group" ? NSLocalizedString("Destroying group...", comment: "") : NSLocalizedString("Destroying channel...", comment: "")) - self.account.mucProcessor.destroyRoom(contact.contactJid as String) - self.account.mucProcessor.addUIHandler({_data in let data = _data as! NSDictionary - hideLoadingOverlay(overlay) - let success : Bool = data["success"] as! Bool; - if success { - if let callback = data["callback"] { - self.successCallback = objcCast(callback) as monal_void_block_t - } - DDLogError("callback: \(String(describing:self.successCallback))") - successAlert(title: Text("Success"), message: contact.mucType == "group" ? Text("Successfully destroyed group.") : Text("Successfully destroyed channel.")) - } else { - errorAlert(title: Text("Error destroying group!"), message: Text(data["errorMessage"] as! String)) - } - }, forMuc:contact.contactJid) - } - ) - ] - ) - } - } - } - } - .addLoadingOverlay(overlay) - .navigationTitle((contact.mucType == "group") ? NSLocalizedString("Edit group", comment: "") : NSLocalizedString("Edit channel", comment: "")) - .alert(isPresented: $showAlert) { - Alert(title: alertPrompt.title, message: alertPrompt.message, dismissButton:.default(Text("Close"), action: { - showAlert = false - if self.success == true { - //close muc ui and leave chat ui of this muc - if let callback = self.successCallback { - callback() - } - if let activeChats = (UIApplication.shared.delegate as! MonalAppDelegate).activeChats { - activeChats.presentChat(with:nil) - } - } - })) - } - .onChange(of:inputImage) { _ in - showLoadingOverlay(overlay, headline: NSLocalizedString("Uploading image...", comment: "")) - self.account.mucProcessor.publishAvatar(inputImage, forMuc: contact.contactJid) - } - .onChange(of:contact.avatar as UIImage) { _ in - hideLoadingOverlay(overlay) - } - } -} - -struct GroupDetailsEdit_Previews: PreviewProvider { - static var previews: some View { - GroupDetailsEdit(contact:ObservableKVOWrapper(MLContact.makeDummyContact(0)), ownAffiliation:"owner") - } -} diff --git a/Monal/Classes/MLContact.h b/Monal/Classes/MLContact.h index 49aa660b8c..0095916050 100644 --- a/Monal/Classes/MLContact.h +++ b/Monal/Classes/MLContact.h @@ -57,6 +57,7 @@ FOUNDATION_EXPORT NSString* const kAskSubscribe; */ @property (nonatomic, readonly) NSString* nickName; @property (nonatomic, strong) NSString* nickNameView; +@property (nonatomic, strong) NSString* fullNameView; /** xmpp state text diff --git a/Monal/Classes/MLContact.m b/Monal/Classes/MLContact.m index 4999d65ae9..687c614f1f 100644 --- a/Monal/Classes/MLContact.m +++ b/Monal/Classes/MLContact.m @@ -17,6 +17,7 @@ #import "MLImageManager.h" #import "MLVoIPProcessor.h" #import "MonalAppDelegate.h" +#import "MLMucProcessor.h" @import Intents; @@ -31,6 +32,7 @@ @interface MLContact () { NSInteger _unreadCount; monal_void_block_t _cancelNickChange; + monal_void_block_t _cancelFullNameChange; UIImage* _avatar; } @property (nonatomic, assign) BOOL isSelfChat; @@ -370,6 +372,7 @@ -(NSString*) nickNameView -(void) setNickNameView:(NSString*) name { + MLAssert(!self.isGroup, @"Using nickNameView only allowed for 1:1 contacts!", (@{@"contact": self})); if([self.nickName isEqualToString:name] || name == nil) return; //no change at all self.nickName = name; @@ -377,7 +380,7 @@ -(void) setNickNameView:(NSString*) name if(_cancelNickChange) _cancelNickChange(); // delay changes because we don't want to update the roster on our server too often while typing - _cancelNickChange = createTimer(1.0, (^{ + _cancelNickChange = createTimer(2.0, (^{ xmpp* account = [[MLXMPPManager sharedInstance] getConnectedAccountForID:self.accountId]; [account updateRosterItem:self withName:self.nickName]; })); @@ -388,6 +391,33 @@ +(NSSet*) keyPathsForValuesAffectingNickNameView return [NSSet setWithObjects:@"nickName", nil]; } +-(NSString*) fullNameView +{ + return nilDefault(self.fullName, @""); +} + +-(void) setFullNameView:(NSString*) name +{ + MLAssert(self.isGroup, @"Using fullNameView only allowed for mucs!", (@{@"contact": self})); + if([self.fullName isEqualToString:name] || name == nil) + return; //no change at all + self.fullName = name; + xmpp* account = [[MLXMPPManager sharedInstance] getConnectedAccountForID:self.accountId]; + [[DataLayer sharedInstance] setFullName:self.fullName forContact:self.contactJid andAccount:account.accountNo]; + // abort old change timer and start a new one + if(_cancelFullNameChange) + _cancelFullNameChange(); + // delay changes because we don't want to update the roster on our server too often while typing + _cancelFullNameChange = createTimer(2.0, (^{ + [account.mucProcessor changeNameOfMuc:self.contactJid to:self.fullName]; + })); +} + ++(NSSet*) keyPathsForValuesAffectingFullNameView +{ + return [NSSet setWithObjects:@"fullName", nil]; +} + -(UIImage*) avatar { // return already cached image diff --git a/Monal/Classes/MLMessageProcessor.m b/Monal/Classes/MLMessageProcessor.m index 9f11a9e598..59bdc62253 100644 --- a/Monal/Classes/MLMessageProcessor.m +++ b/Monal/Classes/MLMessageProcessor.m @@ -346,16 +346,16 @@ +(MLMessage* _Nullable) processMessage:(XMPPMessage*) messageNode andOuterMessag DDLogDebug(@"This is muc, inbound is now: %@ (ownNick: %@, actualFrom: %@, participantJid: %@)", inbound ? @"YES": @"NO", ownNick, actualFrom, participantJid); } - if([messageNode check:@"//subject#"]) + if([messageNode check:@"//subject"]) { if(!isMLhistory) { - NSString* subject = [messageNode findFirst:@"//subject#"]; + NSString* subject = nilDefault([messageNode findFirst:@"//subject#"], @""); subject = [subject stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; NSString* currentSubject = [[DataLayer sharedInstance] mucSubjectforAccount:account.accountNo andRoom:messageNode.fromUser]; - DDLogInfo(@"Got MUC subject for %@: %@", messageNode.fromUser, subject); + DDLogInfo(@"Got MUC subject for %@: '%@'", messageNode.fromUser, subject); - if(subject == nil || [subject isEqualToString:currentSubject]) + if([subject isEqualToString:currentSubject]) { DDLogVerbose(@"Ignoring subject, nothing changed..."); return nil; diff --git a/Monal/Classes/MLMucProcessor.m b/Monal/Classes/MLMucProcessor.m index 667f36b3ef..4702b592a8 100644 --- a/Monal/Classes/MLMucProcessor.m +++ b/Monal/Classes/MLMucProcessor.m @@ -23,7 +23,7 @@ #import "MLOMEMO.h" #import "MLImageManager.h" -#define CURRENT_MUC_STATE_VERSION @8 +#define CURRENT_MUC_STATE_VERSION @9 @interface MLMucProcessor() { @@ -35,6 +35,7 @@ @interface MLMucProcessor() NSMutableDictionary* _joining; NSMutableSet* _destroying; NSMutableSet* _firstJoin; + NSMutableDictionary* _changingName; NSDate* _lastPing; NSMutableSet* _noUpdateBookmarks; BOOL _hasFetchedBookmarks; @@ -78,6 +79,7 @@ -(id) initWithAccount:(xmpp*) account _joining = [NSMutableDictionary new]; _destroying = [NSMutableSet new]; _firstJoin = [NSMutableSet new]; + _changingName = [NSMutableDictionary new]; _uiHandler = [NSMutableDictionary new]; _lastPing = [NSDate date]; _noUpdateBookmarks = [NSMutableSet new]; @@ -111,6 +113,7 @@ -(void) setInternalState:(NSDictionary*) state _joining = [state[@"joining"] mutableCopy]; _destroying = [state[@"destroying"] mutableCopy]; _firstJoin = [state[@"firstJoin"] mutableCopy]; + _changingName = [state[@"changingName"] mutableCopy]; _lastPing = state[@"lastPing"]; _noUpdateBookmarks = [state[@"noUpdateBookmarks"] mutableCopy]; _hasFetchedBookmarks = [state[@"hasFetchedBookmarks"] boolValue]; @@ -127,6 +130,7 @@ -(NSDictionary*) getInternalState @"joining": [_joining copy], @"destroying": [_destroying copy], @"firstJoin": [_firstJoin copy], + @"changingName": [_changingName copy], @"lastPing": _lastPing, @"noUpdateBookmarks": [_noUpdateBookmarks copy], @"hasFetchedBookmarks": @(_hasFetchedBookmarks), @@ -144,6 +148,8 @@ -(void) handleResourceBound:(NSNotification*) notification { @synchronized(_stateLockObject) { _roomFeatures = [NSMutableDictionary new]; + _destroying = [NSMutableSet new]; + _changingName = [NSMutableDictionary new]; //make sure all idle timers get invalidated properly NSDictionary* joiningCopy = [_joining copy]; @@ -152,7 +158,6 @@ -(void) handleResourceBound:(NSNotification*) notification NSDictionary* creatingCopy = [_creating copy]; for(NSString* room in creatingCopy) [self removeRoomFromCreating:room]; - _destroying = [NSMutableSet new]; //don't clear _firstJoin and _noUpdateBookmarks to make sure half-joined mucs are still added to muc bookmarks @@ -192,6 +197,32 @@ -(BOOL) isJoining:(NSString*) room } } +-(BOOL) incrementNameChange:(NSString*) room +{ + @synchronized(_stateLockObject) { + if(!_changingName[room]) + { + _changingName[room] = @1; + return YES; + } + _changingName[room] = @(((NSNumber*)_changingName[room]).integerValue + 1); + return NO; + } +} + +-(BOOL) decrementNameChange:(NSString*) room +{ + @synchronized(_stateLockObject) { + if(_changingName[room] == nil) + return YES; + NSInteger oldValue = ((NSNumber*)_changingName[room]).integerValue; + _changingName[room] = @(max(0, oldValue - 1)); + if(oldValue == 0) + return YES; + return NO; + } +} + -(void) addUIHandler:(monal_id_block_t) handler forMuc:(NSString*) room { //this will replace the old handler @@ -445,6 +476,7 @@ -(void) configureMuc:(NSString*) roomJid withMandatoryOptions:(NSDictionary*) ma } $$instance_handler(handleRoomConfigFormInvalidation, account.mucProcessor, $$ID(xmpp*, account), $$ID(NSString*, roomJid), $$ID(NSDictionary*, mandatoryOptions), $$ID(NSDictionary*, optionalOptions), $$BOOL(deleteOnError)) + [self decrementNameChange:roomJid]; if(deleteOnError) { DDLogError(@"Config form fetch failed, removing muc '%@' from _creating...", roomJid); @@ -464,6 +496,7 @@ -(void) configureMuc:(NSString*) roomJid withMandatoryOptions:(NSDictionary*) ma if([iqNode check:@"/"]) { DDLogError(@"Failed to fetch room config form for '%@': %@", roomJid, [iqNode findFirst:@"error"]); + [self decrementNameChange:roomJid]; if(deleteOnError) { [self removeRoomFromCreating:roomJid]; @@ -477,6 +510,7 @@ -(void) configureMuc:(NSString*) roomJid withMandatoryOptions:(NSDictionary*) ma if(dataForm == nil) { DDLogError(@"Got empty room config form for '%@'!", roomJid); + [self decrementNameChange:roomJid]; if(deleteOnError) { [self removeRoomFromCreating:roomJid]; @@ -491,6 +525,7 @@ -(void) configureMuc:(NSString*) roomJid withMandatoryOptions:(NSDictionary*) ma if([dataForm getField:option] == nil) { DDLogError(@"Could not configure room '%@' to be a groupchat: config option '%@' not available!", roomJid, option); + [self decrementNameChange:roomJid]; if(deleteOnError) { [self removeRoomFromCreating:roomJid]; @@ -520,6 +555,7 @@ -(void) configureMuc:(NSString*) roomJid withMandatoryOptions:(NSDictionary*) ma $$ $$instance_handler(handleRoomConfigResultInvalidation, account.mucProcessor, $$ID(xmpp*, account), $$ID(NSString*, roomJid), $$ID(NSDictionary*, mandatoryOptions), $$ID(NSDictionary*, optionalOptions), $$BOOL(deleteOnError)) + [self decrementNameChange:roomJid]; if(deleteOnError) { DDLogError(@"Config form submit failed, removing muc '%@' from _creating...", roomJid); @@ -535,6 +571,7 @@ -(void) configureMuc:(NSString*) roomJid withMandatoryOptions:(NSDictionary*) ma if([iqNode check:@"/"]) { DDLogError(@"Failed to submit room config form of '%@': %@", roomJid, [iqNode findFirst:@"error"]); + [self decrementNameChange:roomJid]; if(deleteOnError) { [self removeRoomFromCreating:roomJid]; @@ -1190,6 +1227,7 @@ -(void) setAffiliation:(NSString*) affiliation ofUser:(NSString*) jid inMuc:(NSS -(void) changeNameOfMuc:(NSString*) room to:(NSString*) name { + [self incrementNameChange:room]; [self configureMuc:room withMandatoryOptions:@{ @"muc#roomconfig_roomname": name, } andOptionalOptions:@{} deletingMucOnError:NO andJoiningMucOnSuccess:NO]; @@ -1238,6 +1276,7 @@ -(void) publishAvatar:(UIImage* _Nullable) image forMuc:(NSString*) room $$ $$instance_handler(handleDiscoResponseInvalidation, account.mucProcessor, $$ID(xmpp*, account), $$ID(NSString*, roomJid)) + [self decrementNameChange:roomJid]; DDLogInfo(@"Removing muc '%@' from _joining...", roomJid); [self removeRoomFromJoining:roomJid]; $$ @@ -1258,6 +1297,7 @@ -(void) publishAvatar:(UIImage* _Nullable) image forMuc:(NSString*) room if([iqNode check:@"//error/{urn:ietf:params:xml:ns:xmpp-stanzas}gone"]) { DDLogError(@"Querying muc info returned this muc isn't available anymore: %@", [iqNode findFirst:@"error"]); + [self decrementNameChange:iqNode.fromUser]; [self removeRoomFromJoining:iqNode.fromUser]; //delete muc from favorites table to be sure we don't try to rejoin it and update bookmarks afterwards (to make sure this muc isn't accidentally left in our boomkmarks) @@ -1272,6 +1312,7 @@ -(void) publishAvatar:(UIImage* _Nullable) image forMuc:(NSString*) room if([iqNode check:@"//error"]) { DDLogError(@"Querying muc info returned a temporary error: %@", [iqNode findFirst:@"error"]); + [self decrementNameChange:iqNode.fromUser]; [self removeRoomFromJoining:iqNode.fromUser]; //do nothing: the error is only temporary (a s2s problem etc.), a muc ping will retry the join @@ -1287,6 +1328,7 @@ -(void) publishAvatar:(UIImage* _Nullable) image forMuc:(NSString*) room else if([iqNode check:@"/"]) { DDLogError(@"Querying muc info returned a persistent error: %@", [iqNode findFirst:@"error"]); + [self decrementNameChange:iqNode.fromUser]; [self removeRoomFromJoining:iqNode.fromUser]; //delete muc from favorites table to be sure we don't try to rejoin it and update bookmarks afterwards (to make sure this muc isn't accidentally left in our boomkmarks) @@ -1305,6 +1347,7 @@ -(void) publishAvatar:(UIImage* _Nullable) image forMuc:(NSString*) room if(![features containsObject:@"http://jabber.org/protocol/muc"]) { DDLogError(@"muc disco returned that this jid is not a muc!"); + [self decrementNameChange:iqNode.fromUser]; //delete muc from favorites table to be sure we don't try to rejoin it and update bookmarks afterwards (to make sure this muc isn't accidentally left in our boomkmarks) //make sure to update remote bookmarks, even if updateBookmarks == NO @@ -1324,6 +1367,7 @@ -(void) publishAvatar:(UIImage* _Nullable) image forMuc:(NSString*) room if(join && ![self isJoining:iqNode.fromUser]) { DDLogWarn(@"Ignoring muc disco result for '%@' on account %@: not joining anymore...", iqNode.fromUser, _account); + [self decrementNameChange:iqNode.fromUser]; return; } @@ -1371,12 +1415,15 @@ -(void) publishAvatar:(UIImage* _Nullable) image forMuc:(NSString*) room [[DataLayer sharedInstance] updateMucTypeTo:mucType forRoom:iqNode.fromUser andAccount:_account.accountNo]; } - if(mucName && [mucName length]) + if(!mucName || ![mucName length]) + mucName = @""; + //only handle incoming name updates if they are not our own reflected changes + if([self decrementNameChange:iqNode.fromUser]) { MLContact* mucContact = [MLContact createContactFromJid:iqNode.fromUser andAccountNo:_account.accountNo]; if(![mucName isEqualToString:mucContact.fullName]) { - DDLogInfo(@"Configuring muc %@ to use name '%@'...", iqNode.fromUser, mucName); + DDLogInfo(@"Configuring muc %@ to use name '%@' (old value: '%@')...", iqNode.fromUser, mucName, mucContact.fullName); [[DataLayer sharedInstance] setFullName:mucName forContact:iqNode.fromUser andAccount:_account.accountNo]; } } diff --git a/Monal/Monal.xcodeproj/project.pbxproj b/Monal/Monal.xcodeproj/project.pbxproj index 93f01047d2..de395b13ad 100644 --- a/Monal/Monal.xcodeproj/project.pbxproj +++ b/Monal/Monal.xcodeproj/project.pbxproj @@ -177,10 +177,7 @@ C117F7E12B086390001F2BC6 /* CreateGroupMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D88BB76295BB6DC00FB30BA /* CreateGroupMenu.swift */; }; C117F7E22B0863B3001F2BC6 /* ContactPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D631822294BAB1D00026BE7 /* ContactPicker.swift */; }; C12436142434AB5D00B8F074 /* MLAttributedLabel.m in Sources */ = {isa = PBXBuildFile; fileRef = C12436132434AB5D00B8F074 /* MLAttributedLabel.m */; }; - C13A0BCE26E78B7B00987E29 /* ContactDetailsHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = C19C919A26E26AF000F8CC57 /* ContactDetailsHeader.swift */; }; C1414E9D24312F0100948788 /* MLChatMapsCell.m in Sources */ = {isa = PBXBuildFile; fileRef = C1414E9C24312F0100948788 /* MLChatMapsCell.m */; }; - C153825F2B89BBE600EA83EC /* GroupDetailsEdit.swift in Sources */ = {isa = PBXBuildFile; fileRef = C153825E2B89BBE600EA83EC /* GroupDetailsEdit.swift */; }; - C15382622B89C38300EA83EC /* EditGroupName.swift in Sources */ = {isa = PBXBuildFile; fileRef = C15382612B89C38300EA83EC /* EditGroupName.swift */; }; C15489B925680BBE00BBA2F0 /* MLQRCodeScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = C15489B825680BBE00BBA2F0 /* MLQRCodeScanner.swift */; }; C158D40025A0AB810005AA40 /* MLMucProcessor.h in Headers */ = {isa = PBXBuildFile; fileRef = C158D3FE25A0AB810005AA40 /* MLMucProcessor.h */; }; C158D41425A0AC630005AA40 /* MLMucProcessor.m in Sources */ = {isa = PBXBuildFile; fileRef = C158D41225A0AC630005AA40 /* MLMucProcessor.m */; }; @@ -636,8 +633,6 @@ C13E640925BD406700763D6F /* pt-PT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-PT"; path = "external/pt-PT.lproj/Localizable.strings"; sourceTree = ""; }; C1414E9B24312F0100948788 /* MLChatMapsCell.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MLChatMapsCell.h; sourceTree = ""; }; C1414E9C24312F0100948788 /* MLChatMapsCell.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MLChatMapsCell.m; sourceTree = ""; }; - C153825E2B89BBE600EA83EC /* GroupDetailsEdit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupDetailsEdit.swift; sourceTree = ""; }; - C15382612B89C38300EA83EC /* EditGroupName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditGroupName.swift; sourceTree = ""; }; C15489B825680BBE00BBA2F0 /* MLQRCodeScanner.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MLQRCodeScanner.swift; sourceTree = ""; }; C1567E3528255C64006E9637 /* Monal.macos.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Monal.macos.entitlements; sourceTree = ""; }; C1567E3628255C64006E9637 /* Monal.ios.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Monal.ios.entitlements; sourceTree = ""; }; @@ -666,7 +661,6 @@ C18E7579245E8AE900AE8FB7 /* MLPipe.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MLPipe.m; sourceTree = ""; }; C1943A4A25309A9D0036172F /* MLReloadCell.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MLReloadCell.h; sourceTree = ""; }; C1943A4B25309A9D0036172F /* MLReloadCell.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MLReloadCell.m; sourceTree = ""; }; - C19C919A26E26AF000F8CC57 /* ContactDetailsHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactDetailsHeader.swift; sourceTree = ""; }; C1A80DA224D9552400B99E01 /* MLChatViewHelper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MLChatViewHelper.h; sourceTree = ""; }; C1A80DA324D9552400B99E01 /* MLChatViewHelper.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MLChatViewHelper.m; sourceTree = ""; }; C1AAC3E224B5EF4100BB15D6 /* HelperTools.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HelperTools.h; sourceTree = ""; }; @@ -1099,11 +1093,8 @@ 3D85E586282AE523006F5B3A /* OmemoQrCodeView.swift */, 3DC5035B2822F5220064C8A7 /* OmemoKeys.swift */, 3D65B78C27234B74005A30F4 /* ContactDetails.swift */, - C19C919A26E26AF000F8CC57 /* ContactDetailsHeader.swift */, 3D5A91412842B4AE008CE57E /* MemberList.swift */, C18967C62B81F61B0073C7C5 /* ChannelMemberList.swift */, - C153825E2B89BBE600EA83EC /* GroupDetailsEdit.swift */, - C15382612B89C38300EA83EC /* EditGroupName.swift */, C1E8A7F62B8E47C300760220 /* EditGroupSubject.swift */, ); name = "Contact Details"; @@ -2047,7 +2038,6 @@ 262E51971AD8CB7200788351 /* MLTextInputCell.m in Sources */, 20ED55852BADDA5C0005783E /* PrivacySettings.swift in Sources */, 84E55E7D2964424E003E191A /* ActiveChatsViewController.m in Sources */, - C15382622B89C38300EA83EC /* EditGroupName.swift in Sources */, C1E8A7F72B8E47C300760220 /* EditGroupSubject.swift in Sources */, 263DFAC32187D0E00038E716 /* MLLinkCell.m in Sources */, 3D65B78D27234B74005A30F4 /* ContactDetails.swift in Sources */, @@ -2077,8 +2067,6 @@ 2696EED21791245A00BC54B8 /* chatViewController.m in Sources */, 26A78ED823C2B59400C7CF40 /* MLPlaceholderViewController.m in Sources */, C12436142434AB5D00B8F074 /* MLAttributedLabel.m in Sources */, - C13A0BCE26E78B7B00987E29 /* ContactDetailsHeader.swift in Sources */, - C153825F2B89BBE600EA83EC /* GroupDetailsEdit.swift in Sources */, 3D85E587282AE523006F5B3A /* OmemoQrCodeView.swift in Sources */, 849A53E4287135B2007E941A /* MLVoIPProcessor.m in Sources */, C117F7E12B086390001F2BC6 /* CreateGroupMenu.swift in Sources */, From ced8624f06138e5738bde209653b962a97d7b0ee Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Sat, 4 May 2024 18:55:45 +0200 Subject: [PATCH 010/143] Fix stanzaid handling for muc reflections, fixes message retraction --- Monal/Classes/DataLayer.h | 2 ++ Monal/Classes/DataLayer.m | 16 ++++++++-------- Monal/Classes/MLMessageProcessor.m | 21 ++++++++++++++++++--- Monal/Classes/chatViewController.m | 12 ++++++++++-- Monal/Classes/xmpp.m | 1 + 5 files changed, 39 insertions(+), 13 deletions(-) diff --git a/Monal/Classes/DataLayer.h b/Monal/Classes/DataLayer.h index b030a79a5f..dc92321f78 100644 --- a/Monal/Classes/DataLayer.h +++ b/Monal/Classes/DataLayer.h @@ -179,6 +179,8 @@ extern NSString* const kMessageTypeFiletransfer; -(NSNumber*) getSmallestHistoryId; -(NSNumber*) getBiggestHistoryId; +-(NSNumber* _Nullable) hasMessageForStanzaId:(NSString*) stanzaId orMessageID:(NSString*) messageId withInboundDir:(BOOL) inbound onAccount:(NSNumber*) accountNo; + /* adds a specified message to the database */ diff --git a/Monal/Classes/DataLayer.m b/Monal/Classes/DataLayer.m index 4de33cc7a3..c68dc59d5a 100644 --- a/Monal/Classes/DataLayer.m +++ b/Monal/Classes/DataLayer.m @@ -1236,7 +1236,7 @@ -(NSNumber*) addMessageToChatBuddy:(NSString*) buddyName withInboundDir:(BOOL) i return nil; return [self.db idWriteTransaction:^{ - if(!checkForDuplicates || ![self hasMessageForStanzaId:stanzaid orMessageID:messageid withInboundDir:inbound onAccount:accountNo]) + if(!checkForDuplicates || [self hasMessageForStanzaId:stanzaid orMessageID:messageid withInboundDir:inbound onAccount:accountNo] == nil) { //this is always from a contact NSDateFormatter* formatter = [NSDateFormatter new]; @@ -1293,21 +1293,21 @@ -(NSNumber*) addMessageToChatBuddy:(NSString*) buddyName withInboundDir:(BOOL) i }]; } --(BOOL) hasMessageForStanzaId:(NSString*) stanzaId orMessageID:(NSString*) messageId withInboundDir:(BOOL) inbound onAccount:(NSNumber*) accountNo +-(NSNumber* _Nullable) hasMessageForStanzaId:(NSString*) stanzaId orMessageID:(NSString*) messageId withInboundDir:(BOOL) inbound onAccount:(NSNumber*) accountNo { if(accountNo == nil) - return NO; + return (NSNumber*)nil; - return [self.db boolWriteTransaction:^{ + return (NSNumber*)[self.db idWriteTransaction:^{ //if the stanzaid was given, this is conclusive for dedup, we don't need to check any other ids (EXCEPTION BELOW) if(stanzaId) { DDLogVerbose(@"stanzaid provided"); - NSArray* found = [self.db executeReader:@"SELECT * FROM message_history WHERE account_id=? AND stanzaid!='' AND stanzaid=?;" andArguments:@[accountNo, stanzaId]]; + NSArray* found = [self.db executeScalarReader:@"SELECT message_history_id FROM message_history WHERE account_id=? AND stanzaid!='' AND stanzaid=?;" andArguments:@[accountNo, stanzaId]]; if([found count]) { DDLogVerbose(@"stanzaid provided and could be found: %@", found); - return YES; + return found[0]; } } @@ -1328,12 +1328,12 @@ -(BOOL) hasMessageForStanzaId:(NSString*) stanzaId orMessageID:(NSString*) messa //this entry needs an update of its stanzaid [self.db executeNonQuery:@"UPDATE message_history SET stanzaid=? WHERE message_history_id=?" andArguments:@[stanzaId, historyId]]; } - return YES; + return historyId; } } DDLogVerbose(@"nothing worked --> message not found"); - return NO; + return (NSNumber*)nil; }]; } diff --git a/Monal/Classes/MLMessageProcessor.m b/Monal/Classes/MLMessageProcessor.m index 59bdc62253..41d92409d0 100644 --- a/Monal/Classes/MLMessageProcessor.m +++ b/Monal/Classes/MLMessageProcessor.m @@ -129,7 +129,6 @@ +(MLMessage* _Nullable) processMessage:(XMPPMessage*) messageNode andOuterMessag //handle incoming jmi calls (TODO: add entry to local history, once the UI for this is implemented) //only handle incoming propose messages if not older than 60 seconds - if([messageNode check:@"{urn:xmpp:jingle-message:0}*"] && ![HelperTools shouldProvideVoip]) { DDLogWarn(@"VoIP not supported, ignoring incoming JMI message!"); @@ -459,7 +458,6 @@ +(MLMessage* _Nullable) processMessage:(XMPPMessage*) messageNode andOuterMessag DDLogInfo(@"Sending out kMonalDeletedMessageNotice notification for historyId %@", historyIdToRetract); [[MLNotificationQueue currentQueue] postNotificationName:kMonalDeletedMessageNotice object:account userInfo:@{ @"message": [[[DataLayer sharedInstance] messagesForHistoryIDs:@[historyIdToRetract]] firstObject], - @"historyId": historyIdToRetract, @"contact": possiblyUnknownContact, }]; @@ -581,6 +579,7 @@ +(MLMessage* _Nullable) processMessage:(XMPPMessage*) messageNode andOuterMessag } //handle normal messages or LMC messages that can not be found + //(this will update stanzaid in database, too, if deduplication detects a duplicate/reflection) if(historyId == nil) { historyId = [[DataLayer sharedInstance] @@ -662,7 +661,6 @@ +(MLMessage* _Nullable) processMessage:(XMPPMessage*) messageNode andOuterMessag DDLogInfo(@"Sending out kMonalNewMessageNotice notification for historyId %@", historyId); [[MLNotificationQueue currentQueue] postNotificationName:kMonalNewMessageNotice object:account userInfo:@{ @"message": message, - @"historyId": historyId, @"showAlert": @(showAlert), @"contact": possiblyUnknownContact, }]; @@ -673,6 +671,23 @@ +(MLMessage* _Nullable) processMessage:(XMPPMessage*) messageNode andOuterMessag } } } + else if(!inbound) + { + //just try to use the probably reflected message to update the stanzaid of our message in the db + //messageId is always a proper origin-id in this case, because inbound == NO and Monal uses origin-ids + NSNumber* historyId = [[DataLayer sharedInstance] hasMessageForStanzaId:stanzaid orMessageID:messageId withInboundDir:inbound onAccount:account.accountNo]; + if(historyId != nil) + { + message = [[DataLayer sharedInstance] messageForHistoryID:historyId]; + DDLogDebug(@"Managed to update stanzaid of message (or stanzaid already known): %@", message); + DDLogInfo(@"Sending out kMonalNewMessageNotice notification for historyId %@", historyId); + [[MLNotificationQueue currentQueue] postNotificationName:kMonalNewMessageNotice object:account userInfo:@{ + @"message": message, + @"showAlert": @(NO), + @"contact": possiblyUnknownContact, + }]; + } + } //handle message receipts if( diff --git a/Monal/Classes/chatViewController.m b/Monal/Classes/chatViewController.m index d2718adaa0..57c9d7f821 100644 --- a/Monal/Classes/chatViewController.m +++ b/Monal/Classes/chatViewController.m @@ -2561,15 +2561,23 @@ -(UISwipeActionsConfiguration*) tableView:(UITableView*) tableView trailingSwipe copyAction.image = [[[UIImage systemImageNamed:@"doc.on.doc.fill"] imageWithHorizontallyFlippedOrientation] imageWithTintColor:UIColor.whiteColor renderingMode:UIImageRenderingModeAutomatic]; //only allow editing for the 3 newest message && only on outgoing messages - if(!message.inbound && [[DataLayer sharedInstance] checkLMCEligible:message.messageDBId encrypted:(message.encrypted || self.contact.isEncrypted) historyBaseID:nil]) + if((!message.inbound && [[DataLayer sharedInstance] checkLMCEligible:message.messageDBId encrypted:(message.encrypted || self.contact.isEncrypted) historyBaseID:nil]) && (!message.isMuc || (message.isMuc && message.stanzaId != nil)) && !message.retracted) return [UISwipeActionsConfiguration configurationWithActions:@[ quoteAction, copyAction, LMCEditAction, retractAction, ]]; + else if(!message.inbound && [[DataLayer sharedInstance] checkLMCEligible:message.messageDBId encrypted:(message.encrypted || self.contact.isEncrypted) historyBaseID:nil] && !message.retracted) + return [UISwipeActionsConfiguration configurationWithActions:@[ + quoteAction, + copyAction, + LMCEditAction, + localDeleteAction, + ]]; //only allow retraction for outgoing messages or if we are the moderator of that muc - else if(!message.inbound || (self.contact.isGroup && [[[DataLayer sharedInstance] getOwnRoleInGroupOrChannel:self.contact] isEqualToString:@"moderator"] && [[self.xmppAccount.mucProcessor getRoomFeaturesForMuc:self.contact.contactJid] containsObject:@"urn:xmpp:message-moderate:1"])) + //but only allow retraction in mucs if we already got the reflected stanzaid (or if this is an 1:1 chat) + else if((!message.inbound || (self.contact.isGroup && [[[DataLayer sharedInstance] getOwnRoleInGroupOrChannel:self.contact] isEqualToString:@"moderator"] && [[self.xmppAccount.mucProcessor getRoomFeaturesForMuc:self.contact.contactJid] containsObject:@"urn:xmpp:message-moderate:1"])) && (!message.isMuc || (message.isMuc && message.stanzaId != nil)) && !message.retracted) return [UISwipeActionsConfiguration configurationWithActions:@[ quoteAction, copyAction, diff --git a/Monal/Classes/xmpp.m b/Monal/Classes/xmpp.m index ff776e3e21..b548940207 100644 --- a/Monal/Classes/xmpp.m +++ b/Monal/Classes/xmpp.m @@ -3326,6 +3326,7 @@ -(void) retractMessage:(MLMessage*) msg MLAssert([msg.accountId isEqual:self.accountNo], @"Can not retract message from one account on another account!", (@{@"self.accountNo": self.accountNo, @"msg": msg})); XMPPMessage* messageNode = [[XMPPMessage alloc] initWithType:kMessageChatType to:msg.buddyName]; + DDLogVerbose(@"Retracting message: %@", msg); //retraction [messageNode addChildNode:[[MLXMLNode alloc] initWithElement:@"retract" andNamespace:@"urn:xmpp:message-retract:1" withAttributes:@{ @"id": msg.isMuc ? msg.stanzaId : msg.messageId, From 4e2f710ed44e37dab92eac1bf08f9413cb45337e Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Sat, 4 May 2024 19:50:25 +0200 Subject: [PATCH 011/143] Only update stanzaid on jid equality --- Monal/Classes/DataLayer.h | 2 +- Monal/Classes/DataLayer.m | 8 ++++---- Monal/Classes/MLMessageProcessor.m | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Monal/Classes/DataLayer.h b/Monal/Classes/DataLayer.h index dc92321f78..2fe0fbec21 100644 --- a/Monal/Classes/DataLayer.h +++ b/Monal/Classes/DataLayer.h @@ -179,7 +179,7 @@ extern NSString* const kMessageTypeFiletransfer; -(NSNumber*) getSmallestHistoryId; -(NSNumber*) getBiggestHistoryId; --(NSNumber* _Nullable) hasMessageForStanzaId:(NSString*) stanzaId orMessageID:(NSString*) messageId withInboundDir:(BOOL) inbound onAccount:(NSNumber*) accountNo; +-(NSNumber* _Nullable) hasMessageForStanzaId:(NSString*) stanzaId orMessageID:(NSString*) messageId withInboundDir:(BOOL) inbound andJid:(NSString*) jid onAccount:(NSNumber*) accountNo; /* adds a specified message to the database diff --git a/Monal/Classes/DataLayer.m b/Monal/Classes/DataLayer.m index c68dc59d5a..626be21542 100644 --- a/Monal/Classes/DataLayer.m +++ b/Monal/Classes/DataLayer.m @@ -1236,7 +1236,7 @@ -(NSNumber*) addMessageToChatBuddy:(NSString*) buddyName withInboundDir:(BOOL) i return nil; return [self.db idWriteTransaction:^{ - if(!checkForDuplicates || [self hasMessageForStanzaId:stanzaid orMessageID:messageid withInboundDir:inbound onAccount:accountNo] == nil) + if(!checkForDuplicates || [self hasMessageForStanzaId:stanzaid orMessageID:messageid withInboundDir:inbound andJid:buddyName onAccount:accountNo] == nil) { //this is always from a contact NSDateFormatter* formatter = [NSDateFormatter new]; @@ -1293,7 +1293,7 @@ -(NSNumber*) addMessageToChatBuddy:(NSString*) buddyName withInboundDir:(BOOL) i }]; } --(NSNumber* _Nullable) hasMessageForStanzaId:(NSString*) stanzaId orMessageID:(NSString*) messageId withInboundDir:(BOOL) inbound onAccount:(NSNumber*) accountNo +-(NSNumber* _Nullable) hasMessageForStanzaId:(NSString*) stanzaId orMessageID:(NSString*) messageId withInboundDir:(BOOL) inbound andJid:(NSString*) jid onAccount:(NSNumber*) accountNo { if(accountNo == nil) return (NSNumber*)nil; @@ -1303,7 +1303,7 @@ -(NSNumber* _Nullable) hasMessageForStanzaId:(NSString*) stanzaId orMessageID:(N if(stanzaId) { DDLogVerbose(@"stanzaid provided"); - NSArray* found = [self.db executeScalarReader:@"SELECT message_history_id FROM message_history WHERE account_id=? AND stanzaid!='' AND stanzaid=?;" andArguments:@[accountNo, stanzaId]]; + NSArray* found = [self.db executeScalarReader:@"SELECT message_history_id FROM message_history WHERE account_id=? AND buddy_name=? AND stanzaid!='' AND stanzaid=?;" andArguments:@[accountNo, jid, stanzaId]]; if([found count]) { DDLogVerbose(@"stanzaid provided and could be found: %@", found); @@ -1318,7 +1318,7 @@ -(NSNumber* _Nullable) hasMessageForStanzaId:(NSString*) stanzaId orMessageID:(N // the check, if an origin-id was given, lives in MLMessageProcessor.m (it only triggers a dedup for messages either having a stanzaid or an origin-id) if(inbound == NO) { - NSNumber* historyId = (NSNumber*)[self.db executeScalar:@"SELECT message_history_id FROM message_history WHERE account_id=? AND inbound=0 AND messageid=?;" andArguments:@[accountNo, messageId]]; + NSNumber* historyId = (NSNumber*)[self.db executeScalar:@"SELECT message_history_id FROM message_history WHERE account_id=? AND buddy_name=? AND inbound=0 AND messageid=?;" andArguments:@[accountNo, jid, messageId]]; if(historyId != nil) { DDLogVerbose(@"found by origin-id or messageid"); diff --git a/Monal/Classes/MLMessageProcessor.m b/Monal/Classes/MLMessageProcessor.m index 41d92409d0..8ac9906a3c 100644 --- a/Monal/Classes/MLMessageProcessor.m +++ b/Monal/Classes/MLMessageProcessor.m @@ -675,7 +675,7 @@ +(MLMessage* _Nullable) processMessage:(XMPPMessage*) messageNode andOuterMessag { //just try to use the probably reflected message to update the stanzaid of our message in the db //messageId is always a proper origin-id in this case, because inbound == NO and Monal uses origin-ids - NSNumber* historyId = [[DataLayer sharedInstance] hasMessageForStanzaId:stanzaid orMessageID:messageId withInboundDir:inbound onAccount:account.accountNo]; + NSNumber* historyId = [[DataLayer sharedInstance] hasMessageForStanzaId:stanzaid orMessageID:messageId withInboundDir:inbound andJid:buddyName onAccount:account.accountNo]; if(historyId != nil) { message = [[DataLayer sharedInstance] messageForHistoryID:historyId]; From 3b767464645d51cef03ebaf96ba300133549260f Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Sat, 4 May 2024 20:56:06 +0200 Subject: [PATCH 012/143] Throw exception if sqlite bind and arg count don't match --- Monal/Classes/MLSQLite.m | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Monal/Classes/MLSQLite.m b/Monal/Classes/MLSQLite.m index 88b60fde22..1a7d320adb 100644 --- a/Monal/Classes/MLSQLite.m +++ b/Monal/Classes/MLSQLite.m @@ -153,10 +153,18 @@ -(sqlite3_stmt*) prepareQuery:(NSString*) query withArgs:(NSArray*) args if(sqlite3_prepare_v2(self->_database, [query cStringUsingEncoding:NSUTF8StringEncoding], -1, &statement, NULL) != SQLITE_OK) { - DDLogError(@"sqlite prepare '%@' failed: %s", query, sqlite3_errmsg(self->_database)); + [self throwErrorForQuery:query andArguments:args]; return NULL; } + if((int)args.count != sqlite3_bind_parameter_count(statement)) + @throw [NSException exceptionWithName:@"SQLite3Exception" reason:@"SQL parameter count not equals argument count!" userInfo:@{ + @"query": query, + @"args": args, + @"paramCount": @(sqlite3_bind_parameter_count(statement)), + @"argCount": @(args.count), + }]; + //bind args to statement sqlite3_reset(statement); [args enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop __unused) { From bb70394f39d078062996f69aadb338635c9035be Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Sun, 5 May 2024 00:27:00 +0200 Subject: [PATCH 013/143] Let ObservableKVOWrapper return description of wrapped object --- Monal/Classes/SwiftHelpers.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Monal/Classes/SwiftHelpers.swift b/Monal/Classes/SwiftHelpers.swift index fe9a382492..4df315edec 100644 --- a/Monal/Classes/SwiftHelpers.swift +++ b/Monal/Classes/SwiftHelpers.swift @@ -91,7 +91,7 @@ class KVOObserver: NSObject { } @dynamicMemberLookup -public class ObservableKVOWrapper: ObservableObject, Hashable, Equatable { +public class ObservableKVOWrapper: ObservableObject, Hashable, Equatable, CustomStringConvertible { public var obj: ObjType private var observedMembers: NSMutableSet = NSMutableSet() private var observers: [KVOObserver] = Array() @@ -158,6 +158,10 @@ public class ObservableKVOWrapper: ObservableObject, Hashable, self.setWrapper(for:member, value:newValue as AnyObject?) } } + + public var description: String { + return "ObservableKVOWrapper<\(String(describing:self.obj))>" + } @inlinable public static func ==(lhs: ObservableKVOWrapper, rhs: ObservableKVOWrapper) -> Bool { From f7ae8580e63ee3037b5b0b06fb59873851fb9108 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Sun, 5 May 2024 01:38:33 +0200 Subject: [PATCH 014/143] Make ObservableKVOWrapper conform to Identifiable --- Monal/Classes/SwiftHelpers.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Monal/Classes/SwiftHelpers.swift b/Monal/Classes/SwiftHelpers.swift index 4df315edec..82ad5e63d2 100644 --- a/Monal/Classes/SwiftHelpers.swift +++ b/Monal/Classes/SwiftHelpers.swift @@ -91,7 +91,7 @@ class KVOObserver: NSObject { } @dynamicMemberLookup -public class ObservableKVOWrapper: ObservableObject, Hashable, Equatable, CustomStringConvertible { +public class ObservableKVOWrapper: ObservableObject, Hashable, Equatable, CustomStringConvertible, Identifiable { public var obj: ObjType private var observedMembers: NSMutableSet = NSMutableSet() private var observers: [KVOObserver] = Array() From 70fa5967095dc50f6ad9867eb8ad928696865703 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Sun, 5 May 2024 00:45:32 +0200 Subject: [PATCH 015/143] Rework whole group members list ui --- Monal/Classes/ChannelMemberList.swift | 17 +- Monal/Classes/ContactDetails.swift | 23 +- Monal/Classes/CreateGroupMenu.swift | 15 +- Monal/Classes/DataLayer.m | 2 +- Monal/Classes/MemberList.swift | 295 +++++++++++--------------- Monal/Classes/SwiftuiHelpers.swift | 48 ++--- Monal/Classes/chatViewController.m | 17 +- 7 files changed, 188 insertions(+), 229 deletions(-) diff --git a/Monal/Classes/ChannelMemberList.swift b/Monal/Classes/ChannelMemberList.swift index a3cbd0444d..d25ea59078 100644 --- a/Monal/Classes/ChannelMemberList.swift +++ b/Monal/Classes/ChannelMemberList.swift @@ -11,7 +11,7 @@ import monalxmpp import OrderedCollections struct ChannelMemberList: View { - @State private var channelMembers: OrderedDictionary + @State private var channelParticipants: OrderedDictionary @StateObject var channel: ObservableKVOWrapper private let account: xmpp @@ -26,31 +26,30 @@ struct ChannelMemberList: View { nickSet.updateValue((jidDict["affiliation"] as? String) ?? "none", forKey:nick) } } - _channelMembers = State(wrappedValue: nickSet) + _channelParticipants = State(wrappedValue: nickSet) } var body: some View { List { Section(header: Text(self.channel.obj.contactDisplayName)) { - ForEach(self.channelMembers.sorted(by: <), id: \.self.key) { - member in + ForEach(self.channelParticipants.sorted(by: <), id: \.self.key) { participant in ZStack(alignment: .topLeading) { HStack(alignment: .center) { - Text(member.key) + Text(participant.key) Spacer() - if member.value == "owner" { + if participant.value == "owner" { Text(NSLocalizedString("Owner", comment: "")) - } else if member.value == "admin" { + } else if participant.value == "admin" { Text(NSLocalizedString("Admin", comment: "")) } else { - Text(NSLocalizedString("Member", comment: "")) + Text(NSLocalizedString("Participant", comment: "")) } } } } } } - .navigationBarTitle("Channel Members", displayMode: .inline) + .navigationBarTitle(NSLocalizedString("Channel Participants", comment: ""), displayMode: .inline) } } diff --git a/Monal/Classes/ContactDetails.swift b/Monal/Classes/ContactDetails.swift index 74d0403f20..4ecc37c02d 100644 --- a/Monal/Classes/ContactDetails.swift +++ b/Monal/Classes/ContactDetails.swift @@ -285,15 +285,6 @@ struct ContactDetails: View { Text("Pin Chat") } - if contact.obj.isGroup && contact.obj.mucType == "group" { - NavigationLink(destination: LazyClosureView(MemberList(mucContact:contact))) { - Text("Group Members") - } - } else if contact.obj.isGroup && contact.obj.mucType == "channel" { - NavigationLink(destination: LazyClosureView(ChannelMemberList(mucContact:contact))) { - Text("Channel Members") - } - } #if !DISABLE_OMEMO if !HelperTools.isContactBlacklisted(forEncryption:contact.obj) { if !contact.isGroup { @@ -326,6 +317,16 @@ struct ContactDetails: View { NavigationLink(destination: LazyClosureView(BackgroundSettings(contact:contact, delegate:delegate))) { Text("Change Chat Background") } + + if contact.obj.isGroup && contact.obj.mucType == "group" { + NavigationLink(destination: LazyClosureView(MemberList(mucContact:contact))) { + Text("Group Members") + } + } else if contact.obj.isGroup && contact.obj.mucType == "channel" { + NavigationLink(destination: LazyClosureView(ChannelMemberList(mucContact:contact))) { + Text("Channel Members") + } + } } .listStyle(.plain) @@ -392,6 +393,8 @@ struct ContactDetails: View { Text("Yes"), action: { contact.obj.removeFromRoster() //this will dismiss the chatview via kMonalContactRemoved notification + //this will do nothing for contact details opened through group members list (which is fine!) + //NOTE: this holds for all delegate.dismiss() calls self.delegate.dismiss() } ) @@ -542,10 +545,10 @@ struct ContactDetails: View { Alert(title: alertPrompt.title, message: alertPrompt.message, dismissButton:.default(Text("Close"), action: { showAlert = false if self.success == true { - //close muc ui and leave chat ui of this muc if let callback = self.successCallback { callback() } + //close muc ui and leave chat ui of this muc if let activeChats = (UIApplication.shared.delegate as! MonalAppDelegate).activeChats { activeChats.presentChat(with:nil) } diff --git a/Monal/Classes/CreateGroupMenu.swift b/Monal/Classes/CreateGroupMenu.swift index aa3fa5b102..b83f96ed6c 100644 --- a/Monal/Classes/CreateGroupMenu.swift +++ b/Monal/Classes/CreateGroupMenu.swift @@ -14,21 +14,16 @@ import OrderedCollections struct CreateGroupMenu: View { private var appDelegate: MonalAppDelegate - + private var delegate: SheetDismisserProtocol @State private var connectedAccounts: [xmpp] @State private var selectedAccount: xmpp? @State private var groupName: String = "" - @State private var showAlert = false // note: dismissLabel is not accessed but defined at the .alert() section @State private var alertPrompt = AlertPrompt(dismissLabel: Text("Close")) - @State private var selectedContacts : OrderedSet> = [] - + @State private var selectedContacts: OrderedSet> = [] @State private var isEditingGroupName = false - @StateObject private var overlay = LoadingOverlayState() - - private var delegate: SheetDismisserProtocol init(delegate: SheetDismisserProtocol) { self.appDelegate = UIApplication.shared.delegate as! MonalAppDelegate @@ -66,9 +61,9 @@ struct CreateGroupMenu: View { .autocapitalization(.none) .addClearButton(isEditing: isEditingGroupName, text:$groupName) - NavigationLink(destination: LazyClosureView(ContactPicker(account: self.selectedAccount!, selectedContacts: $selectedContacts)), label: { - Text("Change Group Members") - }) + NavigationLink(destination: LazyClosureView(ContactPicker(account: self.selectedAccount!, selectedContacts: $selectedContacts))) { + Text("Change Group Members") + } Button(action: { guard let generatedJid = self.selectedAccount!.mucProcessor.generateMucJid() else { errorAlert(title: Text("Error creating group!"), message: Text("Your server does not provide a MUC component.")) diff --git a/Monal/Classes/DataLayer.m b/Monal/Classes/DataLayer.m index 626be21542..1effe8c0e5 100644 --- a/Monal/Classes/DataLayer.m +++ b/Monal/Classes/DataLayer.m @@ -529,7 +529,7 @@ -(NSDictionary* _Nullable) contactDictionaryForUsername:(NSString*) username for -(NSArray*) possibleGroupMembersForAccount:(NSNumber*) accountNo { return [self.db idReadTransaction:^{ - //list all contacts and group chats + //list all contacts without groupchats and self contact NSString* query = @"SELECT B.buddy_name, B.account_id, IFNULL(IFNULL(NULLIF(B.nick_name, ''), NULLIF(B.full_name, '')), B.buddy_name) FROM buddylist as B INNER JOIN account AS A ON A.account_id=B.account_id WHERE B.account_id=? AND B.muc=0 AND B.buddy_name != (A.username || '@' || A.domain)"; NSMutableArray* toReturn = [NSMutableArray new]; for(NSDictionary* dic in [self.db executeReader:query andArguments:@[accountNo]]) diff --git a/Monal/Classes/MemberList.swift b/Monal/Classes/MemberList.swift index c0d7c4e1d4..be6e3343a2 100644 --- a/Monal/Classes/MemberList.swift +++ b/Monal/Classes/MemberList.swift @@ -6,228 +6,181 @@ // Copyright © 2022 Monal.im. All rights reserved. // -import SwiftUI -import monalxmpp import OrderedCollections +struct ActionSheetPrompt { + var title: Text = Text("") + var message: Text = Text("") + var closure: ()->Void = { } +} + struct MemberList: View { private let account: xmpp private let ownAffiliation: String; @StateObject var group: ObservableKVOWrapper @State private var memberList: OrderedSet> - @State private var affiliation: Dictionary - @State private var openAccountSelection : Bool = false + @State private var affiliations: Dictionary, String> + @State private var navigationActive: ObservableKVOWrapper? @State private var showAlert = false @State private var alertPrompt = AlertPrompt(dismissLabel: Text("Close")) - @State private var selectedMember: MLContact? + @State private var showActionSheet = false + @State private var actionSheetPrompt = ActionSheetPrompt() init(mucContact: ObservableKVOWrapper) { - self.account = MLXMPPManager.sharedInstance().getConnectedAccount(forID: mucContact.accountId)! as xmpp + account = MLXMPPManager.sharedInstance().getConnectedAccount(forID: mucContact.accountId)! as xmpp _group = StateObject(wrappedValue: mucContact) _memberList = State(wrappedValue: getContactList(viewContact: mucContact)) - self.ownAffiliation = DataLayer.sharedInstance().getOwnAffiliation(inGroupOrChannel:mucContact.obj) ?? "none" - var affiliationTmp = Dictionary() - for memberInfo in Array(DataLayer.sharedInstance().getMembersAndParticipants(ofMuc: mucContact.contactJid, forAccountId: self.account.accountNo)) { + ownAffiliation = DataLayer.sharedInstance().getOwnAffiliation(inGroupOrChannel:mucContact.obj) ?? "none" + var affiliationTmp = Dictionary, String>() + for memberInfo in Array(DataLayer.sharedInstance().getMembersAndParticipants(ofMuc: mucContact.contactJid, forAccountId: account.accountNo)) { guard let jid = memberInfo["participant_jid"] as? String ?? memberInfo["member_jid"] as? String else { continue } - affiliationTmp.updateValue((memberInfo["affiliation"] as? String) ?? "none", forKey: jid) + let contact = ObservableKVOWrapper(MLContact.createContact(fromJid:jid, andAccountNo:account.accountNo)) + affiliationTmp[contact] = memberInfo["affiliation"] as? String ?? "none" } - _affiliation = State(wrappedValue: affiliationTmp) + _affiliations = State(wrappedValue: affiliationTmp) } - func showAlert(title: String, description: String) { - self.alertPrompt.title = Text(title) - self.alertPrompt.message = Text(description) + func showAlert(title: Text, description: Text) { + self.alertPrompt.title = title + self.alertPrompt.message = description self.showAlert = true } + + func showActionSheet(title: Text, description: Text, closure: @escaping ()->Void) { + self.actionSheetPrompt.title = title + self.actionSheetPrompt.message = description + self.actionSheetPrompt.closure = closure + self.showActionSheet = true + } func ownUserHasAffiliationToRemove(contact: ObservableKVOWrapper) -> Bool { - if contact.obj.contactJid == self.account.connectionProperties.identity.jid { + if contact.contactJid == account.connectionProperties.identity.jid { return false } - if let contactAffiliation = self.affiliation[contact.contactJid] { - if self.ownAffiliation == "owner" { + if let contactAffiliation = affiliations[contact] { + if ownAffiliation == "owner" { return true - } else if self.ownAffiliation == "admin" && contactAffiliation == "member" { + } else if ownAffiliation == "admin" && (contactAffiliation != "owner" && contactAffiliation != "admin") { return true } } return false } + + func affiliationToText(_ affiliation: String?) -> some View { + if let affiliation = affiliation { + if affiliation == "owner" { + return Text("Owner") + } else if affiliation == "admin" { + return Text("Admin") + } else if affiliation == "member" { + return Text("Member") + } else if affiliation == "outcast" { + return Text("Blocked") + } else if affiliation == "profile" { + return Text("Open contact details") + } + } + return Text("") + } var body: some View { List { Section(header: Text(self.group.obj.contactDisplayName)) { - if self.ownAffiliation == "owner" || self.ownAffiliation == "admin" { - NavigationLink(destination: LazyClosureView(ContactPicker(account: self.account, selectedContacts: $memberList, existingMembers: self.memberList)), label: { - Text("Add Group Members") - }) + if ownAffiliation == "owner" || ownAffiliation == "admin" { + NavigationLink(destination: LazyClosureView(ContactPicker(account: account, selectedContacts: $memberList, existingMembers: memberList))) { + Text("Add Group Members") + } } - ForEach(self.memberList, id: \.self.obj) { - contact in - HStack(alignment: .center) { - Image(uiImage: contact.avatar) - .resizable() - .frame(width: 40, height: 40, alignment: .center) - Text(contact.contactDisplayName as String) - Spacer() - if let contactAffiliation = self.affiliation[contact.contactJid] { - if contactAffiliation == "owner" { - Text(NSLocalizedString("Owner", comment: "muc affiliation")) - } else if contactAffiliation == "admin" { - Text(NSLocalizedString("Admin", comment: "muc affiliation")) - } else if contactAffiliation == "member" { - Text(NSLocalizedString("Member", comment: "muc affiliation")) - } else if contactAffiliation == "outcast" { - Text(NSLocalizedString("Outcast", comment: "muc affiliation")) + ForEach(memberList, id:\.self) { contact in + if !contact.isSelfChat { + HStack(alignment: .center) { + Image(uiImage: contact.avatar) + .resizable() + .frame(width: 40, height: 40, alignment: .center) + Text(contact.contactDisplayName as String) + + Spacer() + + if ownAffiliation == "owner" || ownAffiliation == "admin" { + Picker(selection: Binding( + get: { affiliations[contact] ?? "none" }, + set: { newAffiliation in + if newAffiliation == "profile" { + DDLogVerbose("Activating navigation to \(String(describing:contact))") + navigationActive = contact + } else if newAffiliation == "outcast" { + showActionSheet(title: Text("Block user?"), description: Text("Do you want to block this user from entering this group/channel?")) { + DDLogVerbose("Changing affiliation of \(String(describing:contact)) to: \(String(describing:newAffiliation))...") + affiliations[contact] = newAffiliation + account.mucProcessor.setAffiliation(newAffiliation, ofUser:contact.contactJid, inMuc:self.group.contactJid) + } + } else { + DDLogVerbose("Changing affiliation of \(String(describing:contact)) to: \(String(describing:newAffiliation))...") + affiliations[contact] = newAffiliation + account.mucProcessor.setAffiliation(newAffiliation, ofUser:contact.contactJid, inMuc:self.group.contactJid) + } + } + ), label: EmptyView()) { + ForEach(["profile", "owner", "admin", "member", "outcast"], id:\.self) { affiliation in + affiliationToText(affiliation).tag(affiliation) + } + } + .pickerStyle(.menu) } else { - Text(NSLocalizedString("", comment: "muc affiliation")) + affiliationToText(affiliations[contact]) } } + .deleteDisabled( + !ownUserHasAffiliationToRemove(contact: contact) + ) + //invisible navigation link triggered programmatically + .background( + NavigationLink(destination: LazyClosureView(ContactDetails(delegate:SheetDismisserProtocol(), contact:contact)), tag:contact, selection:$navigationActive) { EmptyView() } + .opacity(0) + ) } - .onTapGesture(perform: { - if contact.obj.contactJid != self.account.connectionProperties.identity.jid { - self.selectedMember = contact.obj - } - }) - .deleteDisabled( - !ownUserHasAffiliationToRemove(contact: contact) - ) } .onDelete(perform: { memberIdx in - let member = self.memberList[memberIdx.first!] - self.account.mucProcessor.setAffiliation("none", ofUser: member.contactJid, inMuc: self.group.contactJid) - - self.showAlert(title: "Member deleted", description: self.memberList[memberIdx.first!].contactJid) - self.memberList.remove(at: memberIdx.first!) - }) - } - .onChange(of: self.memberList) { [previousMemberList = self.memberList] newMemberList in - // only handle new members (added via the contact picker) - for member in newMemberList { - if !previousMemberList.contains(member) { - // add selected group member with affiliation member - affiliationChangeAction(member, affiliation: "member") - self.account.mucProcessor.inviteUser(member.contactJid, inMuc: self.group.contactJid) + let member = memberList[memberIdx.first!] + showActionSheet(title: Text("Remove user?"), description: self.group.mucType == "group" ? Text("Do you want to remove this user from this group? The user won't be able to enter it again until added back to the group.") : Text("Do you want to remove this user from this channel? The user will be able to enter it again.")) { + account.mucProcessor.setAffiliation("none", ofUser: member.contactJid, inMuc: self.group.contactJid) + self.showAlert(title: Text("User removed"), description: Text("\(memberList[memberIdx.first!].obj.contactJid)")) + memberList.remove(at: memberIdx.first!) } - } + }) } - .alert(isPresented: $showAlert, content: { - Alert(title: alertPrompt.title, message: alertPrompt.message, dismissButton: .default(alertPrompt.dismissLabel)) - }) - .sheet(item: self.$selectedMember, content: { selectedMemberUnobserved in - let selectedMember = ObservableKVOWrapper(selectedMemberUnobserved) - VStack { - Form { - Section { - HStack { - Spacer() - Image(uiImage: selectedMember.avatar) - .resizable() - .frame(width: 150, height: 150, alignment: .center) - Spacer() - } - HStack { - Spacer() - Text(selectedMember.contactDisplayName as String) - Spacer() - } - } - Section(header: Text("Configure Membership")) { - if self.ownAffiliation == "owner" && self.affiliation[selectedMember.contactJid] == "owner" { - makeAdmin(selectedMember) - makeMember(selectedMember) - removeUserButton(selectedMember) - block(selectedMember) - } - if self.ownAffiliation == "owner" && self.affiliation[selectedMember.contactJid] == "admin" { - makeOwner(selectedMember) - makeMember(selectedMember) - removeUserButton(selectedMember) - block(selectedMember) - } - if self.ownAffiliation == "owner" && self.affiliation[selectedMember.contactJid] == "member" { - makeOwner(selectedMember) - makeAdmin(selectedMember) - removeUserButton(selectedMember) - block(selectedMember) - } - if self.ownAffiliation == "admin" && self.affiliation[selectedMember.contactJid] == "member" { - removeUserButton(selectedMember) - block(selectedMember) - } - if (self.ownAffiliation == "admin" || self.ownAffiliation == "owner") && self.affiliation[selectedMember.contactJid] == "outcast" { - makeMember(selectedMember) - } - } - } - } - }) } - .navigationBarTitle("Group Members", displayMode: .inline) - } - - func removeUserButton(_ selectedMember: ObservableKVOWrapper) -> some View { - if #available(iOS 15, *) { - return Button(role: .destructive, action: { - self.account.mucProcessor.setAffiliation("none", ofUser: selectedMember.contactJid, inMuc: self.group.contactJid) - self.showAlert(title: "Member deleted", description: selectedMember.contactJid) - if let index = self.memberList.firstIndex(of: selectedMember) { - self.memberList.remove(at: index) + .onChange(of: memberList) { [previousMemberList = memberList] newMemberList in + // only handle new members (added via the contact picker) + for member in newMemberList { + if !previousMemberList.contains(member) { + // add selected group member with affiliation member + affiliations[member] = "member" + account.mucProcessor.setAffiliation("member", ofUser:member.contactJid, inMuc:self.group.contactJid) + account.mucProcessor.inviteUser(member.contactJid, inMuc: self.group.contactJid) } - self.selectedMember = nil - }) { - Text("Remove from group") } - } else { - return AnyView(EmptyView()) - } - } - - func affiliationChangeAction(_ selectedMember: ObservableKVOWrapper, affiliation: String) { - self.account.mucProcessor.setAffiliation(affiliation, ofUser: selectedMember.contactJid, inMuc: self.group.contactJid) - self.affiliation[selectedMember.contactJid] = affiliation - } - - func affiliationButton(_ selectedMember: ObservableKVOWrapper, affiliation: String, @ViewBuilder label: () -> Label) -> some View { - return Button(action: { - affiliationChangeAction(selectedMember, affiliation: affiliation) - // dismiss sheet - self.selectedMember = nil - }) { - label() } - } - - func makeOwner(_ selectedMember: ObservableKVOWrapper) -> some View { - return affiliationButton(selectedMember, affiliation: "owner", label: { - Text("Make owner") + .alert(isPresented: $showAlert, content: { + Alert(title: alertPrompt.title, message: alertPrompt.message, dismissButton: .default(alertPrompt.dismissLabel)) }) - } - - func makeAdmin(_ selectedMember: ObservableKVOWrapper) -> some View { - return affiliationButton(selectedMember, affiliation: "admin", label: { - Text("Make admin") - }) - } - - func makeMember(_ selectedMember: ObservableKVOWrapper) -> some View { - return affiliationButton(selectedMember, affiliation: "member", label: { - Text("Make member") - }) - } - - func block(_ selectedMember: ObservableKVOWrapper) -> AnyView { - if self.group.mucType != "group" { - return AnyView( - affiliationButton(selectedMember, affiliation: "outcast", label: { - Text("Block from group") - }) + .actionSheet(isPresented: $showActionSheet) { + ActionSheet( + title: actionSheetPrompt.title, + message: actionSheetPrompt.message, + buttons: [ + .cancel(), + .destructive( + Text("Yes"), + action: actionSheetPrompt.closure + ) + ] ) - } else { - return AnyView(EmptyView()) } + .navigationBarTitle("Group Members", displayMode: .inline) } } diff --git a/Monal/Classes/SwiftuiHelpers.swift b/Monal/Classes/SwiftuiHelpers.swift index 8a58e4547d..e7ff5206ba 100644 --- a/Monal/Classes/SwiftuiHelpers.swift +++ b/Monal/Classes/SwiftuiHelpers.swift @@ -66,6 +66,28 @@ class SheetDismisserProtocol: ObservableObject { } } +func getContactList(viewContact: (ObservableKVOWrapper?)) -> OrderedSet> { + if let contact = viewContact { + if(contact.isGroup && contact.mucType == "group") { + //this uses the account the muc belongs to and treats every other account to be remote, + //even when multiple accounts of the same monal instance are in the same group + var contactList : OrderedSet> = OrderedSet() + for memberInfo in Array(DataLayer.sharedInstance().getMembersAndParticipants(ofMuc: contact.contactJid, forAccountId: contact.accountId)) { + //jid can be participant_jid (if currently joined to muc) or member_jid (if not joined but member of muc) + guard let jid = memberInfo["participant_jid"] as? String ?? memberInfo["member_jid"] as? String else { + continue + } + contactList.append(ObservableKVOWrapper(MLContact.createContact(fromJid: jid, andAccountNo: contact.accountId))) + } + return contactList + } else { + return [contact] + } + } else { + return [] + } +} + //see here for some ideas used herein: https://blog.logrocket.com/adding-gifs-ios-app-flanimatedimage-swiftui/#using-flanimatedimage-with-swift struct GIFViewer: UIViewRepresentable { typealias UIViewType = FLAnimatedImageView @@ -501,29 +523,3 @@ class SwiftuiInterface : NSObject { return host } } - -func getContactList(viewContact: (ObservableKVOWrapper?)) -> OrderedSet> { - if let contact = viewContact { - if(contact.isGroup && contact.mucType == "group") { - //this uses the account the muc belongs to and treats every other account to be remote, even when multiple accounts of the same monal instance are in the same group - let jidList = Array(DataLayer.sharedInstance().getMembersAndParticipants(ofMuc: contact.contactJid, forAccountId: contact.accountId)) - var contactList : OrderedSet> = OrderedSet() - for jidDict in jidList { - //jid can be participant_jid (if currently joined to muc) or member_jid (if not joined but member of muc) - var jid : String? = jidDict["participant_jid"] as? String - if(jid == nil) { - jid = jidDict["member_jid"] as? String - } - if(jid != nil) { - let contact = MLContact.createContact(fromJid: jid!, andAccountNo: contact.accountId) - contactList.append(ObservableKVOWrapper(contact)) - } - } - return contactList - } else { - return [contact] - } - } else { - return [] - } -} diff --git a/Monal/Classes/chatViewController.m b/Monal/Classes/chatViewController.m index 57c9d7f821..34c78eb104 100644 --- a/Monal/Classes/chatViewController.m +++ b/Monal/Classes/chatViewController.m @@ -636,8 +636,21 @@ -(void) updateUIElements if(self.contact.isGroup) { NSArray* members = [[DataLayer sharedInstance] getMembersAndParticipantsOfMuc:self.contact.contactJid forAccountId:self.xmppAccount.accountNo]; - if(members.count > 0) - jidLabelText = [NSString stringWithFormat:@"%@ (%ld)", contactDisplayName, members.count]; + NSInteger membercount = members.count; + if([self.contact.mucType isEqualToString:@"group"]) + { + NSMutableSet* memberSet = [NSMutableSet new]; + for(NSDictionary* entry in members) + { + if(entry[@"participant_jid"] != nil) + [memberSet addObject:entry[@"participant_jid"]]; + if(entry[@"member_jid"] != nil) + [memberSet addObject:entry[@"member_jid"]]; + } + membercount = memberSet.count; + } + if(membercount > 1) + jidLabelText = [NSString stringWithFormat:@"%@ (%ld)", contactDisplayName, membercount - 1]; //don't count ourselves } // change text values dispatch_async(dispatch_get_main_queue(), ^{ From e2b48ed3947628a36f6994c7d70a61165e065c83 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Sun, 5 May 2024 01:39:04 +0200 Subject: [PATCH 016/143] Fix contact picker and completely rework it --- Monal/Classes/ContactPicker.swift | 71 ++++++++++++++--------------- Monal/Classes/CreateGroupMenu.swift | 2 +- Monal/Classes/MemberList.swift | 2 +- 3 files changed, 37 insertions(+), 38 deletions(-) diff --git a/Monal/Classes/ContactPicker.swift b/Monal/Classes/ContactPicker.swift index a58279fde0..f3fec92b4b 100644 --- a/Monal/Classes/ContactPicker.swift +++ b/Monal/Classes/ContactPicker.swift @@ -41,37 +41,40 @@ struct ContactPickerEntry: View { struct ContactPicker: View { @Environment(\.presentationMode) private var presentationMode - - @State var contacts: OrderedSet> - let account: xmpp - @Binding var selectedContacts: OrderedSet> - let existingMembers: OrderedSet> + @Binding var returnedContacts: OrderedSet> + @State var allContacts: OrderedSet> + @State var selectedContacts: OrderedSet> @State var searchText = "" + @State var isEditingSearchInput = false + let allowRemoval: Bool - @State var isEditingSearchInput: Bool = false - - init(account: xmpp, selectedContacts: Binding>>) { - self.init(account: account, selectedContacts: selectedContacts, existingMembers: OrderedSet()) - } - - init(account: xmpp, selectedContacts: Binding>>, existingMembers: OrderedSet>) { - self.account = account - self._selectedContacts = selectedContacts - self.existingMembers = existingMembers - + init(_ account: xmpp, binding returnedContacts: Binding>>, allowRemoval: Bool = true) { + self.allowRemoval = allowRemoval var contactsTmp: OrderedSet> = OrderedSet() + + //build currently selected list of contacts + contactsTmp.removeAll() + for contact in returnedContacts.wrappedValue { + contactsTmp.append(contact) + } + _selectedContacts = State(wrappedValue: contactsTmp) + + //build list of all possible contacts on this account (excluding selfchat and other mucs) + contactsTmp.removeAll() for contact in DataLayer.sharedInstance().possibleGroupMembers(forAccount: account.accountNo) { contactsTmp.append(ObservableKVOWrapper(contact)) } - _contacts = State(wrappedValue: contactsTmp) + _allContacts = State(wrappedValue: contactsTmp) + + _returnedContacts = returnedContacts } private var searchResults : OrderedSet> { if searchText.isEmpty { - return self.contacts + return self.allContacts } else { var filteredContacts: OrderedSet> = OrderedSet() - for contact in self.contacts { + for contact in self.allContacts { if (contact.contactDisplayName as String).lowercased().contains(searchText.lowercased()) || (contact.contactJid as String).contains(searchText.lowercased()) { filteredContacts.append(contact) @@ -82,26 +85,24 @@ struct ContactPicker: View { } var body: some View { - if(contacts.isEmpty) { + if(allContacts.isEmpty) { Text("No contacts to show :(") .navigationTitle("Contact Lists") } else { - List { - ForEach(searchResults, id: \.self.obj) { contact in - let contactIsSelected = self.selectedContacts.contains(contact); - let contactIsAlreadyMember = self.existingMembers.contains(contact); - ContactPickerEntry(contact: contact, isPicked: contactIsSelected, isExistingMember: contactIsAlreadyMember) - .onTapGesture(perform: { + List(searchResults) { contact in + let contactIsSelected = self.selectedContacts.contains(contact); + let contactIsAlreadyMember = self.returnedContacts.contains(contact); + ContactPickerEntry(contact: contact, isPicked: contactIsSelected, isExistingMember: !(!contactIsAlreadyMember || allowRemoval)) + .onTapGesture { // only allow changes to members that are not already part of the group - if(!contactIsAlreadyMember) { + if(!contactIsAlreadyMember || allowRemoval) { if(contactIsSelected) { self.selectedContacts.remove(contact) } else { self.selectedContacts.append(contact) } } - }) - } + } } .applyClosure { view in if #available(iOS 15.0, *) { @@ -111,13 +112,11 @@ struct ContactPicker: View { } } .listStyle(.inset) - .navigationBarTitle("Contact Selection", displayMode: .inline) - .navigationBarBackButtonHidden(true) - .toolbar { - ToolbarItem(placement: .navigationBarLeading) { - Button("Back", action: { - self.presentationMode.wrappedValue.dismiss() - }) + .navigationBarTitle(NSLocalizedString("Contact Selection", comment: ""), displayMode: .inline) + .onDisappear { + returnedContacts.removeAll() + for contact in selectedContacts { + returnedContacts.append(contact) } } } diff --git a/Monal/Classes/CreateGroupMenu.swift b/Monal/Classes/CreateGroupMenu.swift index b83f96ed6c..6a58f2ae5e 100644 --- a/Monal/Classes/CreateGroupMenu.swift +++ b/Monal/Classes/CreateGroupMenu.swift @@ -61,7 +61,7 @@ struct CreateGroupMenu: View { .autocapitalization(.none) .addClearButton(isEditing: isEditingGroupName, text:$groupName) - NavigationLink(destination: LazyClosureView(ContactPicker(account: self.selectedAccount!, selectedContacts: $selectedContacts))) { + NavigationLink(destination: LazyClosureView(ContactPicker(self.selectedAccount!, binding: $selectedContacts))) { Text("Change Group Members") } Button(action: { diff --git a/Monal/Classes/MemberList.swift b/Monal/Classes/MemberList.swift index be6e3343a2..1d1c7f17a9 100644 --- a/Monal/Classes/MemberList.swift +++ b/Monal/Classes/MemberList.swift @@ -90,7 +90,7 @@ struct MemberList: View { List { Section(header: Text(self.group.obj.contactDisplayName)) { if ownAffiliation == "owner" || ownAffiliation == "admin" { - NavigationLink(destination: LazyClosureView(ContactPicker(account: account, selectedContacts: $memberList, existingMembers: memberList))) { + NavigationLink(destination: LazyClosureView(ContactPicker(account, binding: $memberList, allowRemoval: false))) { Text("Add Group Members") } } From 8124c69f5e49766f9a9b43bfc5689221ab976b68 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Sun, 5 May 2024 01:44:13 +0200 Subject: [PATCH 017/143] Slight overhaul of create group menu --- Monal/Classes/CreateGroupMenu.swift | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/Monal/Classes/CreateGroupMenu.swift b/Monal/Classes/CreateGroupMenu.swift index 6a58f2ae5e..b884a31844 100644 --- a/Monal/Classes/CreateGroupMenu.swift +++ b/Monal/Classes/CreateGroupMenu.swift @@ -61,9 +61,6 @@ struct CreateGroupMenu: View { .autocapitalization(.none) .addClearButton(isEditing: isEditingGroupName, text:$groupName) - NavigationLink(destination: LazyClosureView(ContactPicker(self.selectedAccount!, binding: $selectedContacts))) { - Text("Change Group Members") - } Button(action: { guard let generatedJid = self.selectedAccount!.mucProcessor.generateMucJid() else { errorAlert(title: Text("Error creating group!"), message: Text("Your server does not provide a MUC component.")) @@ -104,15 +101,17 @@ struct CreateGroupMenu: View { Text("Create new group") }) } - if self.selectedContacts.count > 0 { - Section(header: Text("Selected Group Members")) { - ForEach(self.selectedContacts, id: \.obj.contactJid) { contact in - ContactEntry(contact: contact) - } - .onDelete(perform: { indexSet in - self.selectedContacts.remove(at: indexSet.first!) - }) + + Section(header: Text("Selected Group Members")) { + NavigationLink(destination: LazyClosureView(ContactPicker(self.selectedAccount!, binding: $selectedContacts))) { + Text("Change Group Members") } + ForEach(self.selectedContacts, id: \.obj.contactJid) { contact in + ContactEntry(contact: contact) + } + .onDelete(perform: { indexSet in + self.selectedContacts.remove(at: indexSet.first!) + }) } } } @@ -122,7 +121,7 @@ struct CreateGroupMenu: View { })) } .addLoadingOverlay(overlay) - .navigationBarTitle("Create new group", displayMode: .inline) + .navigationBarTitle(NSLocalizedString("Create new group", comment:""), displayMode: .inline) .navigationViewStyle(.stack) } } From 73c1022eef0325bf3e5c46c928321c3e27712f20 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Sun, 5 May 2024 02:35:30 +0200 Subject: [PATCH 018/143] Clean up swiftui implementations This changes every display of a contact with ContactEntry() to not replicate the same code over and over again. --- Monal/Classes/AccountPicker.swift | 17 ++----------- Monal/Classes/BackgroundSettings.swift | 4 ---- Monal/Classes/ChannelMemberList.swift | 2 -- Monal/Classes/ChatPlaceholder.swift | 2 -- Monal/Classes/ContactEntry.swift | 18 ++++++++++---- Monal/Classes/ContactPicker.swift | 10 +------- Monal/Classes/ContactRequestsMenu.swift | 3 --- Monal/Classes/CreateGroupMenu.swift | 4 ---- Monal/Classes/EditGroupSubject.swift | 2 -- Monal/Classes/LoadingOverlay.swift | 3 --- Monal/Classes/MLContact.h | 1 + Monal/Classes/MLContact.m | 28 ++++++++++++++++++---- Monal/Classes/MLQRCodeScanner.swift | 4 ---- Monal/Classes/MemberList.swift | 5 +--- Monal/Classes/OmemoKeys.swift | 4 +--- Monal/Classes/OmemoQrCodeView.swift | 2 -- Monal/Classes/QRCodeScannerLoginView.swift | 2 -- Monal/Classes/RichAlert.swift | 1 - Monal/Classes/WelcomeLogIn.swift | 3 --- Monal/Classes/ZoomableContainer.swift | 4 ---- 20 files changed, 43 insertions(+), 76 deletions(-) diff --git a/Monal/Classes/AccountPicker.swift b/Monal/Classes/AccountPicker.swift index f2c2520e59..2b321cdc9c 100644 --- a/Monal/Classes/AccountPicker.swift +++ b/Monal/Classes/AccountPicker.swift @@ -6,9 +6,6 @@ // Copyright © 2023 monal-im.org. All rights reserved. // -import SwiftUI -import monalxmpp - struct AccountPicker: View { let delegate: SheetDismisserProtocol let contacts: [MLContact] @@ -43,25 +40,15 @@ struct AccountPicker: View { .frame(maxWidth: .infinity) .background(Color(UIColor.systemBackground)) - let appDelegate = UIApplication.shared.delegate as! MonalAppDelegate List { ForEach(contacts) { contact in if let accountEntry = DataLayer.sharedInstance().details(forAccount:contact.accountId) { let accountJid = "\(accountEntry["username"] ?? "" as NSString)@\(accountEntry["domain"] ?? "" as NSString)" - let accountDisplayName = MLContact.ownDisplayName(forAccount:MLXMPPManager.sharedInstance().getConnectedAccount(forID: contact.accountId)!) as String let accountContact = MLContact.createContact(fromJid:accountJid, andAccountNo:accountEntry["account_id"] as! NSNumber) Button { - appDelegate.activeChats!.call(contact, with:callType) + (UIApplication.shared.delegate as! MonalAppDelegate).activeChats!.call(contact, with:callType) } label: { - HStack(alignment: .center) { - Image(uiImage: MLImageManager.sharedInstance().getIconFor(accountContact)!) - .resizable() - .frame(width: 40, height: 40, alignment: .center) - VStack(alignment: .leading) { - Text(accountDisplayName) - Text(accountJid).font(.footnote).opacity(0.6) - } - } + ContactEntry(contact:ObservableKVOWrapper(accountContact), selfnotesPrefix:false) } } } diff --git a/Monal/Classes/BackgroundSettings.swift b/Monal/Classes/BackgroundSettings.swift index bbb0985c9f..12b45d6081 100644 --- a/Monal/Classes/BackgroundSettings.swift +++ b/Monal/Classes/BackgroundSettings.swift @@ -6,10 +6,6 @@ // Copyright © 2022 monal-im.org. All rights reserved. // -import SwiftUI -import UniformTypeIdentifiers -import monalxmpp - @ViewBuilder func title(contact: ObservableKVOWrapper?) -> some View { if let contact = contact { diff --git a/Monal/Classes/ChannelMemberList.swift b/Monal/Classes/ChannelMemberList.swift index d25ea59078..3258d56e22 100644 --- a/Monal/Classes/ChannelMemberList.swift +++ b/Monal/Classes/ChannelMemberList.swift @@ -6,8 +6,6 @@ // Copyright © 2024 monal-im.org. All rights reserved. // -import SwiftUI -import monalxmpp import OrderedCollections struct ChannelMemberList: View { diff --git a/Monal/Classes/ChatPlaceholder.swift b/Monal/Classes/ChatPlaceholder.swift index 3888438dbe..cd6a407995 100644 --- a/Monal/Classes/ChatPlaceholder.swift +++ b/Monal/Classes/ChatPlaceholder.swift @@ -6,8 +6,6 @@ // Copyright © 2022 monal-im.org. All rights reserved. // -import SwiftUI - struct ChatPlaceholder: View { @Environment(\.colorScheme) var colorScheme var body: some View { diff --git a/Monal/Classes/ContactEntry.swift b/Monal/Classes/ContactEntry.swift index 9ff3eac03f..4171715a22 100644 --- a/Monal/Classes/ContactEntry.swift +++ b/Monal/Classes/ContactEntry.swift @@ -6,11 +6,15 @@ // Copyright © 2023 monal-im.org. All rights reserved. // -import SwiftUI - struct ContactEntry: View { - let contact : ObservableKVOWrapper - + let contact: ObservableKVOWrapper + let selfnotesPrefix: Bool + + init(contact:ObservableKVOWrapper, selfnotesPrefix: Bool = true) { + self.contact = contact + self.selfnotesPrefix = selfnotesPrefix + } + var body:some View { ZStack(alignment: .topLeading) { HStack(alignment: .center) { @@ -18,7 +22,11 @@ struct ContactEntry: View { .resizable() .frame(width: 40, height: 40, alignment: .center) VStack(alignment: .leading) { - Text(contact.contactDisplayName as String) + if selfnotesPrefix { + Text(contact.contactDisplayName as String) + } else { + Text(contact.contactDisplayNameWithoutSelfnotesPrefix as String) + } Text(contact.contactJid as String).font(.footnote).opacity(0.6) } } diff --git a/Monal/Classes/ContactPicker.swift b/Monal/Classes/ContactPicker.swift index f3fec92b4b..0ca4f833e2 100644 --- a/Monal/Classes/ContactPicker.swift +++ b/Monal/Classes/ContactPicker.swift @@ -6,8 +6,6 @@ // Copyright © 2022 monal-im.org. All rights reserved. // -import SwiftUI -import monalxmpp import OrderedCollections struct ContactPickerEntry: View { @@ -27,13 +25,7 @@ struct ContactPickerEntry: View { } else { Image(systemName: "circle") } - Image(uiImage: contact.avatar) - .resizable() - .frame(width: 40, height: 40, alignment: .center) - VStack(alignment: .leading) { - Text(contact.contactDisplayName as String) - Text(contact.contactJid as String).font(.footnote).opacity(0.6) - } + ContactEntry(contact: contact) } } } diff --git a/Monal/Classes/ContactRequestsMenu.swift b/Monal/Classes/ContactRequestsMenu.swift index ebce742ba8..80c873ff1a 100644 --- a/Monal/Classes/ContactRequestsMenu.swift +++ b/Monal/Classes/ContactRequestsMenu.swift @@ -6,9 +6,6 @@ // Copyright © 2022 monal-im.org. All rights reserved. // -import SwiftUI -import monalxmpp - struct ContactRequestsMenuEntry: View { let contact : MLContact let doDelete: () -> () diff --git a/Monal/Classes/CreateGroupMenu.swift b/Monal/Classes/CreateGroupMenu.swift index b884a31844..f0f41f7d0e 100644 --- a/Monal/Classes/CreateGroupMenu.swift +++ b/Monal/Classes/CreateGroupMenu.swift @@ -6,10 +6,6 @@ // Copyright © 2022 monal-im.org. All rights reserved. // -import MobileCoreServices -import UniformTypeIdentifiers -import SwiftUI -import monalxmpp import OrderedCollections struct CreateGroupMenu: View { diff --git a/Monal/Classes/EditGroupSubject.swift b/Monal/Classes/EditGroupSubject.swift index 7647694151..f90e1eee7c 100644 --- a/Monal/Classes/EditGroupSubject.swift +++ b/Monal/Classes/EditGroupSubject.swift @@ -6,8 +6,6 @@ // Copyright © 2024 monal-im.org. All rights reserved. // -import SwiftUI - struct EditGroupSubject: View { @StateObject var contact: ObservableKVOWrapper private let account: xmpp? diff --git a/Monal/Classes/LoadingOverlay.swift b/Monal/Classes/LoadingOverlay.swift index 49450045a0..ac713970bc 100644 --- a/Monal/Classes/LoadingOverlay.swift +++ b/Monal/Classes/LoadingOverlay.swift @@ -6,9 +6,6 @@ // Copyright © 2022 Monal.im. All rights reserved. // -import SwiftUI -import monalxmpp - //data class for overlay state class LoadingOverlayState : ObservableObject { var enabled: Bool diff --git a/Monal/Classes/MLContact.h b/Monal/Classes/MLContact.h index 0095916050..6741dfb9f1 100644 --- a/Monal/Classes/MLContact.h +++ b/Monal/Classes/MLContact.h @@ -91,6 +91,7 @@ FOUNDATION_EXPORT NSString* const kAskSubscribe; @property (nonatomic, readonly) NSString* ask; //whether we have tried to subscribe @property (nonatomic, readonly) NSString* contactDisplayName; +@property (nonatomic, readonly) NSString* contactDisplayNameWithoutSelfnotesPrefix; -(NSString*) contactDisplayNameWithFallback:(NSString* _Nullable) fallbackName; -(void) updateWithContact:(MLContact*) contact; diff --git a/Monal/Classes/MLContact.m b/Monal/Classes/MLContact.m index 687c614f1f..499559b31c 100644 --- a/Monal/Classes/MLContact.m +++ b/Monal/Classes/MLContact.m @@ -305,6 +305,11 @@ -(void) updateUnreadCount } -(NSString*) contactDisplayNameWithFallback:(NSString* _Nullable) fallbackName; +{ + return [self contactDisplayNameWithFallback:fallbackName andSelfnotesPrefix:YES]; +} + +-(NSString*) contactDisplayNameWithFallback:(NSString* _Nullable) fallbackName andSelfnotesPrefix:(BOOL) hasSelfnotesPrefix { DDLogVerbose(@"Calculating contact display name..."); NSString* displayName; @@ -338,11 +343,16 @@ -(NSString*) contactDisplayNameWithFallback:(NSString* _Nullable) fallbackName; else { xmpp* account = [[MLXMPPManager sharedInstance] getConnectedAccountForID:self.accountId]; - //add "Note to self: " prefix for selfchats - if([[DataLayer sharedInstance] enabledAccountCnts].intValue > 1) - displayName = [NSString stringWithFormat:NSLocalizedString(@"Notes to self: %@", @""), [[self class] ownDisplayNameForAccount:account]]; + if(hasSelfnotesPrefix) + { + //add "Note to self: " prefix for selfchats + if([[DataLayer sharedInstance] enabledAccountCnts].intValue > 1) + displayName = [NSString stringWithFormat:NSLocalizedString(@"Notes to self: %@", @""), [[self class] ownDisplayNameForAccount:account]]; + else + displayName = NSLocalizedString(@"Notes to self", @""); + } else - displayName = NSLocalizedString(@"Notes to self", @""); + displayName = [[self class] ownDisplayNameForAccount:account]; } DDLogVerbose(@"Calculated contactDisplayName for '%@': %@", self.contactJid, displayName); @@ -365,6 +375,16 @@ +(NSSet*) keyPathsForValuesAffectingContactDisplayName return [NSSet setWithObjects:@"nickName", @"fullName", @"contactJid", nil]; } +-(NSString*) contactDisplayNameWithoutSelfnotesPrefix +{ + return [self contactDisplayNameWithFallback:nil andSelfnotesPrefix:NO]; +} + ++(NSSet*) keyPathsForValuesAffectingContactDisplayNameWithoutSelfnotesPrefix +{ + return [NSSet setWithObjects:@"nickName", @"fullName", @"contactJid", nil]; +} + -(NSString*) nickNameView { return nilDefault(self.nickName, @""); diff --git a/Monal/Classes/MLQRCodeScanner.swift b/Monal/Classes/MLQRCodeScanner.swift index d52f8c0a1d..db5f92eeee 100644 --- a/Monal/Classes/MLQRCodeScanner.swift +++ b/Monal/Classes/MLQRCodeScanner.swift @@ -6,10 +6,6 @@ // Copyright © 2020 Monal.im. All rights reserved. // -import CocoaLumberjack -import AVFoundation -import UIKit -import SwiftUI import SafariServices @objc protocol MLLQRCodeScannerAccountLoginDelegate : AnyObject diff --git a/Monal/Classes/MemberList.swift b/Monal/Classes/MemberList.swift index 1d1c7f17a9..bc126d5618 100644 --- a/Monal/Classes/MemberList.swift +++ b/Monal/Classes/MemberList.swift @@ -97,10 +97,7 @@ struct MemberList: View { ForEach(memberList, id:\.self) { contact in if !contact.isSelfChat { HStack(alignment: .center) { - Image(uiImage: contact.avatar) - .resizable() - .frame(width: 40, height: 40, alignment: .center) - Text(contact.contactDisplayName as String) + ContactEntry(contact:contact) Spacer() diff --git a/Monal/Classes/OmemoKeys.swift b/Monal/Classes/OmemoKeys.swift index 8621abc8dd..19aaf5bc28 100644 --- a/Monal/Classes/OmemoKeys.swift +++ b/Monal/Classes/OmemoKeys.swift @@ -5,10 +5,8 @@ // Created by Jan on 04.05.22. // Copyright © 2022 Monal.im. All rights reserved. // -import UniformTypeIdentifiers -import SwiftUI + import OrderedCollections -import monalxmpp struct OmemoKeysEntry: View { private let contactJid: String diff --git a/Monal/Classes/OmemoQrCodeView.swift b/Monal/Classes/OmemoQrCodeView.swift index fc880295aa..e3b59b09f4 100644 --- a/Monal/Classes/OmemoQrCodeView.swift +++ b/Monal/Classes/OmemoQrCodeView.swift @@ -6,9 +6,7 @@ // Copyright © 2022 Monal.im. All rights reserved. // -import SwiftUI import CoreImage.CIFilterBuiltins -import monalxmpp func createQrCode(value: String) -> UIImage { diff --git a/Monal/Classes/QRCodeScannerLoginView.swift b/Monal/Classes/QRCodeScannerLoginView.swift index fcb0b33296..913a4b283c 100644 --- a/Monal/Classes/QRCodeScannerLoginView.swift +++ b/Monal/Classes/QRCodeScannerLoginView.swift @@ -6,8 +6,6 @@ // Copyright © 2022 Monal.im. All rights reserved. // -import SwiftUI - struct QRCodeScannerLoginView: UIViewControllerRepresentable { @Binding private var account : String @Binding private var password : String diff --git a/Monal/Classes/RichAlert.swift b/Monal/Classes/RichAlert.swift index 4b2a12a329..0c0983f08c 100644 --- a/Monal/Classes/RichAlert.swift +++ b/Monal/Classes/RichAlert.swift @@ -6,7 +6,6 @@ // Copyright © 2022 monal-im.org. All rights reserved. // -import SwiftUI import ViewExtractor struct RichAlertView: ViewModifier where TitleContent: View, BodyContent: View, ButtonContent: View { diff --git a/Monal/Classes/WelcomeLogIn.swift b/Monal/Classes/WelcomeLogIn.swift index 765852a813..0379ee5d73 100644 --- a/Monal/Classes/WelcomeLogIn.swift +++ b/Monal/Classes/WelcomeLogIn.swift @@ -6,9 +6,6 @@ // Copyright © 2022 Monal.im. All rights reserved. // -import SwiftUI -import monalxmpp - struct WelcomeLogIn: View { static private let credFaultyPattern = "^.+@.+\\..{2,}$" diff --git a/Monal/Classes/ZoomableContainer.swift b/Monal/Classes/ZoomableContainer.swift index 9a1884c410..64d9e40e2f 100644 --- a/Monal/Classes/ZoomableContainer.swift +++ b/Monal/Classes/ZoomableContainer.swift @@ -6,10 +6,6 @@ // Copyright © 2023 monal-im.org. All rights reserved. // -import Foundation -import UIKit -import SwiftUI - //based upon: https://stackoverflow.com/a/76649224/3528174 struct ZoomableContainer: View { let content: Content From 162bf6fa0160d3f14a5f9df44cd105290299649a Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Tue, 7 May 2024 03:52:05 +0200 Subject: [PATCH 019/143] Fix mds bug when receiving while not in catchup --- Monal/Classes/MLPubSubProcessor.m | 2 +- Monal/Classes/xmpp.m | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/Monal/Classes/MLPubSubProcessor.m b/Monal/Classes/MLPubSubProcessor.m index d06615ecde..7dc4527be3 100644 --- a/Monal/Classes/MLPubSubProcessor.m +++ b/Monal/Classes/MLPubSubProcessor.m @@ -33,7 +33,7 @@ -(NSString*) calculateNickForMuc:(NSString*) room; @implementation MLPubSubProcessor $$class_handler(mdsHandler, $$ID(xmpp*, account), $$ID(NSString*, jid), $$ID(NSString*, type), $_ID((NSDictionary*), data)) - DDLogDebug(@"Got new mds displayed status from '%@'", jid); + DDLogDebug(@"Got new mds displayed status from '%@' (should be own jid)...", jid); if(![jid isEqualToString:account.connectionProperties.identity.jid]) { DDLogWarn(@"Ignoring mds update not coming from our own jid"); diff --git a/Monal/Classes/xmpp.m b/Monal/Classes/xmpp.m index b548940207..ae9cdc2531 100644 --- a/Monal/Classes/xmpp.m +++ b/Monal/Classes/xmpp.m @@ -5349,7 +5349,18 @@ -(void) handleFinishedCatchup -(void) updateMdsData:(NSDictionary*) mdsData { for(NSString* jid in mdsData) + { + //update cached data _mdsData[jid] = mdsData[jid]; + + //handle mds update directly, if not in catchup for this jid + //everything else will be handled once the catchup is finished + NSString* catchupJid = self.connectionProperties.identity.jid; + if([[DataLayer sharedInstance] isBuddyMuc:jid forAccount:self.accountNo]) + catchupJid = jid; + if(_inCatchup[catchupJid] == nil && _mdsData[jid] != nil) + [self handleMdsData:_mdsData[jid] forJid:jid]; + } } -(void) handleMdsData:(MLXMLNode*) data forJid:(NSString*) jid From 2b9e0f75d33a99113e0e9ec0934e7b718f10c2fc Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Tue, 30 Apr 2024 18:48:28 +0200 Subject: [PATCH 020/143] Improve WebRTC handling and add macOS CallKit experiment - Add com.apple.security.network.server entitlement to allow WebRTC to open listening sockets - Implement non-xep conforming TCP transport for ICE candidates only active in alpha builds - Bump WebRTC loglevel to verbose in alpha builds (it's info otherwise) - Activate VoIP for alpha builds on macOS - Circumvent CallKit audio session activation problems on macOS Catalyst to activate VoIP as much as possible on macOS --- Monal/Alpha.Monal.macos.entitlements | 2 + Monal/Classes/HelperTools.m | 6 ++- Monal/Classes/MLCall.m | 50 +++++++++++++++++- Monal/Classes/WebRTCClient.swift | 4 ++ Monal/Monal.ios.entitlements | 2 + .../NotificationService/NotificationService.m | 51 ++++++++++--------- rust/sdp-to-jingle/src/xep_0176.rs | 4 +- 7 files changed, 91 insertions(+), 28 deletions(-) diff --git a/Monal/Alpha.Monal.macos.entitlements b/Monal/Alpha.Monal.macos.entitlements index f8560de197..9134ea69aa 100644 --- a/Monal/Alpha.Monal.macos.entitlements +++ b/Monal/Alpha.Monal.macos.entitlements @@ -22,6 +22,8 @@ com.apple.security.network.client + com.apple.security.network.server + com.apple.security.personal-information.location keychain-access-groups diff --git a/Monal/Classes/HelperTools.m b/Monal/Classes/HelperTools.m index 0054d9e159..aee39aa79e 100644 --- a/Monal/Classes/HelperTools.m +++ b/Monal/Classes/HelperTools.m @@ -648,9 +648,11 @@ +(NSURL*) getFailoverTurnApiServer +(BOOL) shouldProvideVoip { - BOOL shouldProvideVoip; + BOOL shouldProvideVoip = NO; #if TARGET_OS_MACCATALYST - shouldProvideVoip = NO; +#ifdef IS_ALPHA + shouldProvideVoip = YES; +#endif #else shouldProvideVoip = YES; #endif diff --git a/Monal/Classes/MLCall.m b/Monal/Classes/MLCall.m index 186a65c204..9731a95a49 100644 --- a/Monal/Classes/MLCall.m +++ b/Monal/Classes/MLCall.m @@ -399,6 +399,13 @@ -(void) setIsConnected:(BOOL) isConnected if(self.isConnected && self.audioSession != nil) [self startCallDuartionTimer]; } + +#ifdef IS_ALPHA +#if TARGET_OS_MACCATALYST + //set audio session to default one + self.audioSession = [AVAudioSession sharedInstance]; +#endif +#endif } -(BOOL) isConnected { @@ -415,7 +422,13 @@ -(void) setAudioSession:(AVAudioSession*) audioSession DDLogWarn(@"Trying to activate same audio session a second time, ignoring..."); return; } - if(audioSession != nil) + BOOL assertActivated = YES; +#ifdef IS_ALPHA +#if TARGET_OS_MACCATALYST + assertActivated = NO; +#endif +#endif + if(assertActivated && audioSession != nil) MLAssert(_audioSession == nil, @"Audio session should never be activated without deactivating old audio session first!", (@{ @"oldAudioSession": nilWrapper(_audioSession), @"newAudioSession": nilWrapper(audioSession), @@ -1110,6 +1123,25 @@ -(void) webRTCClient:(WebRTCClient*) webRTCClient didDiscoverLocalCandidate:(RTC DDLogError(@"Failed to convert raw sdp candidate to jingle, ignoring this candidate: %@", candidate); return; } +#ifdef IS_ALPHA + if([contentNode check:@"{urn:xmpp:jingle:transports:ice-udp:1}transport/candidate"]) + { + //add tcptype because that attribute is apparently not supported by our mozilla sdp lib + MLXMLNode* candidateNode = [contentNode findFirst:@"{urn:xmpp:jingle:transports:ice-udp:1}transport/candidate"]; + if([candidate.sdp containsString:@"typ host tcptype active"]) + candidateNode.attributes[@"tcptype"] = @"active"; + else if([candidate.sdp containsString:@"typ host tcptype passive"]) + candidateNode.attributes[@"tcptype"] = @"passive"; + else + DDLogWarn(@"Unknown type-tcptype combination!"); + } +#else + if([contentNode check:@"{urn:xmpp:jingle:transports:ice-udp:1}transport/candidate"]) + { + DDLogError(@"Ignoring raw sdp candidate, because it's using tcp instead of udp: %@", candidate); + return; + } +#endif //see https://webrtc.googlesource.com/src/+/refs/heads/main/sdk/objc/api/peerconnection/RTCIceCandidate.h XMPPIQ* candidateIq = [[XMPPIQ alloc] initWithType:kiqSetType to:self.fullRemoteJid]; [candidateIq addChildNode:[[MLXMLNode alloc] initWithElement:@"jingle" andNamespace:@"urn:xmpp:jingle:1" withAttributes:@{ @@ -1269,6 +1301,22 @@ -(void) processRemoteICECandidate:(XMPPIQ*) iqNode { RTCIceCandidate* incomingCandidate = nil; NSString* rawSdp = [HelperTools xml2candidate:[iqNode findFirst:@"{urn:xmpp:jingle:1}jingle"] withInitiator:self.direction==MLCallDirectionIncoming]; +#ifdef IS_ALPHA + if([iqNode check:@"{urn:xmpp:jingle:1}jingle/content/{urn:xmpp:jingle:transports:ice-udp:1}transport/candidate"]) + { + NSString* type = [iqNode findFirst:@"{urn:xmpp:jingle:1}jingle/content/{urn:xmpp:jingle:transports:ice-udp:1}transport/candidate@type"]; + NSString* tcptype = [iqNode findFirst:@"{urn:xmpp:jingle:1}jingle/content/{urn:xmpp:jingle:transports:ice-udp:1}transport/candidate@tcptype"]; + DDLogDebug(@"Patching raw sdp type=%@ to contain tcptype: %@", type, tcptype); + rawSdp = [rawSdp stringByReplacingOccurrencesOfString:[NSString stringWithFormat:@"typ %@", type] withString:[NSString stringWithFormat:@"typ %@ tcptype %@", type, tcptype]]; + } +#else + if([iqNode check:@"{urn:xmpp:jingle:1}jingle/content/{urn:xmpp:jingle:transports:ice-udp:1}transport/candidate"]) + { + DDLogWarn(@"Got tcp candidate, ignoring: %@", [iqNode findFirst:@"{urn:xmpp:jingle:1}jingle/content/{urn:xmpp:jingle:transports:ice-udp:1}transport/candidate"]); + rawSdp = nil; + } +#endif + DDLogVerbose(@"Got raw remote sdp: %@", rawSdp); if(rawSdp == nil) { DDLogError(@"Failed to convert jingle candidate to raw sdp!"); diff --git a/Monal/Classes/WebRTCClient.swift b/Monal/Classes/WebRTCClient.swift index 481ed73ad7..5805a5f072 100644 --- a/Monal/Classes/WebRTCClient.swift +++ b/Monal/Classes/WebRTCClient.swift @@ -90,7 +90,11 @@ final class WebRTCClient: NSObject { @objc required init(iceServers: [RTCIceServer], audioOnly: Bool, forceRelay: Bool) { +#if IS_ALPHA + RTCSetMinDebugLogLevel(.verbose) +#else RTCSetMinDebugLogLevel(.info) +#endif var peerConnection = WebRTCClient.createPeerConnection(iceServers: iceServers, forceRelay: forceRelay) if peerConnection == nil { diff --git a/Monal/Monal.ios.entitlements b/Monal/Monal.ios.entitlements index cb5cda403c..485d26ec64 100644 --- a/Monal/Monal.ios.entitlements +++ b/Monal/Monal.ios.entitlements @@ -24,6 +24,8 @@ com.apple.security.network.client + com.apple.security.network.server + com.apple.security.personal-information.location com.apple.security.personal-information.photos-library diff --git a/Monal/NotificationService/NotificationService.m b/Monal/NotificationService/NotificationService.m index 0eaf40f866..88d21331a7 100644 --- a/Monal/NotificationService/NotificationService.m +++ b/Monal/NotificationService/NotificationService.m @@ -160,32 +160,37 @@ -(BOOL) feedNextHandler -(void) handleIncomingVoipCall:(NSNotification*) notification { DDLogInfo(@"Got incoming VOIP call"); - if(@available(iOS 14.5, macCatalyst 14.5, *)) + if([HelperTools shouldProvideVoip]) { - //disconnect while still being in the receive queue to make sure we don't process any other stanza after this jmi one - //(we don't want to handle a second jmi stanza for example: that could confuse tie-breaking and other parts of our call handling) - xmpp* account = [[MLXMPPManager sharedInstance] getConnectedAccountForID:notification.userInfo[@"accountNo"]]; - [account disconnect]; - - //now disconnect all other accounts, post the voip push and kill the appex - //do this in an extra thread to avoid deadlocks via: receive_queue -> disconnect_thread -> receive_queue - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - //directly disconnect without handling any possibly queued stanzas (they will be handled in mainapp once we wake it up) - [self disconnectAndFeedAllWaitingHandlers]; - - DDLogInfo(@"Dispatching voip call to mainapp..."); - NSString* payload = [HelperTools encodeBase64WithData:[HelperTools serializeObject:notification.userInfo]]; - [CXProvider reportNewIncomingVoIPPushPayload:@{@"base64Payload": payload} completion:^(NSError* _Nullable error) { - if(error != nil) - DDLogError(@"Got error for reportNewIncomingVoIPPushPayload: %@", error); - else - DDLogInfo(@"Successfully called reportNewIncomingVoIPPushPayload"); - [self killAppex]; - }]; - }); + if(@available(iOS 14.5, macCatalyst 14.5, *)) + { + //disconnect while still being in the receive queue to make sure we don't process any other stanza after this jmi one + //(we don't want to handle a second jmi stanza for example: that could confuse tie-breaking and other parts of our call handling) + xmpp* account = [[MLXMPPManager sharedInstance] getConnectedAccountForID:notification.userInfo[@"accountNo"]]; + [account disconnect]; + + //now disconnect all other accounts, post the voip push and kill the appex + //do this in an extra thread to avoid deadlocks via: receive_queue -> disconnect_thread -> receive_queue + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + //directly disconnect without handling any possibly queued stanzas (they will be handled in mainapp once we wake it up) + [self disconnectAndFeedAllWaitingHandlers]; + + DDLogInfo(@"Dispatching voip call to mainapp..."); + NSString* payload = [HelperTools encodeBase64WithData:[HelperTools serializeObject:notification.userInfo]]; + [CXProvider reportNewIncomingVoIPPushPayload:@{@"base64Payload": payload} completion:^(NSError* _Nullable error) { + if(error != nil) + DDLogError(@"Got error for reportNewIncomingVoIPPushPayload: %@", error); + else + DDLogInfo(@"Successfully called reportNewIncomingVoIPPushPayload"); + [self killAppex]; + }]; + }); + } + else + DDLogError(@"iOS < 14.5 detected, ignoring incoming call!"); } else - DDLogError(@"iOS < 14.5 detected, ignoring incoming call!"); + DDLogError(@"shouldProvideVoip returned NO, ignoring incoming call!"); } -(void) disconnectAndFeedAllWaitingHandlers diff --git a/rust/sdp-to-jingle/src/xep_0176.rs b/rust/sdp-to-jingle/src/xep_0176.rs index f9db742026..d868aa3d9f 100644 --- a/rust/sdp-to-jingle/src/xep_0176.rs +++ b/rust/sdp-to-jingle/src/xep_0176.rs @@ -173,7 +173,7 @@ impl JingleTransportCandidate { priority: candidate.priority, protocol: match candidate.transport { SdpAttributeCandidateTransport::Udp => "udp".to_string(), - //SdpAttributeCandidateTransport::Tcp => "tcp".to_string(), //not specced in xep-0176 + SdpAttributeCandidateTransport::Tcp => "tcp".to_string(), //not specced in xep-0176 _ => { return Err(SdpParserInternalError::Generic( "Encountered some candidate transport (like tcp) not specced in XEP-0176!" @@ -196,7 +196,7 @@ impl JingleTransportCandidate { component: self.component, transport: match self.protocol.as_str() { "udp" => Ok(SdpAttributeCandidateTransport::Udp), - //"tcp" => Ok(SdpAttributeCandidateTransport::Tcp), + "tcp" => Ok(SdpAttributeCandidateTransport::Tcp), //not specced in xep-0176 _ => Err(SdpParserInternalError::Generic( "Encountered some candidate transport (like tcp) not specced in XEP-0176!" .to_string(), From ae4764ff5defe3e23f12ad812f8cfd70b0e35b63 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Tue, 7 May 2024 07:35:46 +0200 Subject: [PATCH 021/143] Activate DNSSEC validation for all HTTP requests and TCP connections --- Monal/Classes/HelperTools.h | 2 ++ Monal/Classes/HelperTools.m | 8 ++++++++ Monal/Classes/MLFiletransfer.m | 6 ++++-- Monal/Classes/MLHTTPRequest.m | 7 ++++--- Monal/Classes/MLStream.m | 2 ++ Monal/Classes/MLVoIPProcessor.m | 16 ++++++++++------ Monal/Classes/MLWebViewController.m | 8 +++++--- Monal/Classes/RegisterAccount.swift | 5 ++++- Monal/Classes/chatViewController.m | 15 ++++++++++----- 9 files changed, 49 insertions(+), 20 deletions(-) diff --git a/Monal/Classes/HelperTools.h b/Monal/Classes/HelperTools.h index fb02f2877d..446b73f549 100644 --- a/Monal/Classes/HelperTools.h +++ b/Monal/Classes/HelperTools.h @@ -177,6 +177,8 @@ void swizzle(Class c, SEL orig, SEL new); +(BOOL) isIP:(NSString*) host; ++(NSURLSession*) createEphemeralURLSession; + @end NS_ASSUME_NONNULL_END diff --git a/Monal/Classes/HelperTools.m b/Monal/Classes/HelperTools.m index aee39aa79e..18a2928b04 100644 --- a/Monal/Classes/HelperTools.m +++ b/Monal/Classes/HelperTools.m @@ -2710,4 +2710,12 @@ +(BOOL) isIP:(NSString*) host return NO; } ++(NSURLSession*) createEphemeralURLSession +{ + NSURLSessionConfiguration* sessionConfig = [NSURLSessionConfiguration ephemeralSessionConfiguration]; + if(@available(iOS 16.1, macCatalyst 16.1, *)) + sessionConfig.requiresDNSSECValidation = YES; + return [NSURLSession sessionWithConfiguration:sessionConfig]; +} + @end diff --git a/Monal/Classes/MLFiletransfer.m b/Monal/Classes/MLFiletransfer.m index 174f6a4234..41b4427f1d 100644 --- a/Monal/Classes/MLFiletransfer.m +++ b/Monal/Classes/MLFiletransfer.m @@ -77,10 +77,12 @@ +(void) checkMimeTypeAndSizeForHistoryID:(NSNumber*) historyId dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{ DDLogInfo(@"Requesting mime-type and size for historyID %@ from http server", historyId); NSMutableURLRequest* request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:url]]; + if(@available(iOS 16.1, macCatalyst 16.1, *)) + request.requiresDNSSECValidation = YES; request.HTTPMethod = @"HEAD"; request.cachePolicy = NSURLRequestReturnCacheDataElseLoad; - NSURLSession* session = [NSURLSession sharedSession]; + NSURLSession* session = [HelperTools createEphemeralURLSession]; [[session dataTaskWithRequest:request completionHandler:^(NSData* _Nullable data __unused, NSURLResponse* _Nullable response, NSError* _Nullable error) { if(error != nil) { @@ -181,7 +183,7 @@ +(void) downloadFileForHistoryID:(NSNumber*) historyId andForceDownload:(BOOL) f return; } - NSURLSession* session = [NSURLSession sharedSession]; + NSURLSession* session = [HelperTools createEphemeralURLSession]; // set app defined description for download size checks [session setSessionDescription:url]; NSURLSessionDownloadTask* task = [session downloadTaskWithURL:[NSURL URLWithString:url] completionHandler:^(NSURL* _Nullable location, NSURLResponse* _Nullable response, NSError* _Nullable error) { diff --git a/Monal/Classes/MLHTTPRequest.m b/Monal/Classes/MLHTTPRequest.m index a12ca5805b..acce093a69 100644 --- a/Monal/Classes/MLHTTPRequest.m +++ b/Monal/Classes/MLHTTPRequest.m @@ -7,7 +7,7 @@ // #import "MLHTTPRequest.h" - +#import "HelperTools.h" @interface MLHTTPRequest () @@ -47,6 +47,8 @@ +(void) sendWithVerb:(NSString*) verb path:(NSString*) path headers:(NSDictiona NSMutableURLRequest* theRequest = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:path] cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:60.0]; + if(@available(iOS 16.1, macCatalyst 16.1, *)) + theRequest.requiresDNSSECValidation = YES; [theRequest setHTTPMethod:verb]; NSData* dataToSubmit = postedData; @@ -70,8 +72,7 @@ +(void) sendWithVerb:(NSString*) verb path:(NSString*) path headers:(NSDictiona DDLogVerbose(@"Calling: %@ %@", verb, path); - NSURLSession* session= [NSURLSession sharedSession]; - + NSURLSession* session = [HelperTools createEphemeralURLSession]; void (^completeBlock)(NSData*,NSURLResponse*,NSError*)= ^(NSData* data,NSURLResponse* response, NSError* connectionError) { diff --git a/Monal/Classes/MLStream.m b/Monal/Classes/MLStream.m index 549fceab4e..3ae299f29a 100644 --- a/Monal/Classes/MLStream.m +++ b/Monal/Classes/MLStream.m @@ -530,6 +530,8 @@ +(void) connectWithSNIDomain:(NSString*) SNIDomain connectHost:(NSString*) host } //needed to activate tcp fast open with apple's internal tls framer nw_parameters_set_fast_open_enabled(parameters, YES); + if(@available(iOS 16.0, macCatalyst 16.0, *)) + nw_parameters_set_requires_dnssec_validation(parameters, YES); //create and configure connection object nw_endpoint_t endpoint = nw_endpoint_create_host([host cStringUsingEncoding:NSUTF8StringEncoding], [[port stringValue] cStringUsingEncoding:NSUTF8StringEncoding]); diff --git a/Monal/Classes/MLVoIPProcessor.m b/Monal/Classes/MLVoIPProcessor.m index 8d66b90af8..a46a939487 100644 --- a/Monal/Classes/MLVoIPProcessor.m +++ b/Monal/Classes/MLVoIPProcessor.m @@ -403,8 +403,11 @@ -(void) initWebRTCForPendingCall:(MLCall*) call // request turn credentials NSMutableURLRequest* urlRequest = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"/api/v1/challenge/new" relativeToURL:[HelperTools getFailoverTurnApiServer]]]; + if(@available(iOS 16.1, macCatalyst 16.1, *)) + urlRequest.requiresDNSSECValidation = YES; [urlRequest setTimeoutInterval:3.0]; - NSURLSessionTask* challengeSession = [[NSURLSession sharedSession] dataTaskWithRequest:urlRequest completionHandler:^(NSData* data, NSURLResponse* response, NSError* error) { + NSURLSession* challengeSession = [HelperTools createEphemeralURLSession]; + [[challengeSession dataTaskWithRequest:urlRequest completionHandler:^(NSData* data, NSURLResponse* response, NSError* error) { if(error != nil || [(NSHTTPURLResponse*)response statusCode] != 200) { DDLogWarn(@"Could not retrieve turn challenge, only using stun: %@", error); @@ -440,6 +443,8 @@ -(void) initWebRTCForPendingCall:(MLCall*) call return; } NSMutableURLRequest* responseRequest = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"/api/v1/challenge/validate" relativeToURL:[HelperTools getFailoverTurnApiServer]]]; + if(@available(iOS 16.1, macCatalyst 16.1, *)) + responseRequest.requiresDNSSECValidation = YES; [responseRequest setHTTPMethod:@"POST"]; [responseRequest setValue:@"application/json" forHTTPHeaderField:@"Accept"]; @@ -448,7 +453,8 @@ -(void) initWebRTCForPendingCall:(MLCall*) call [responseRequest setTimeoutInterval:3.0]; [responseRequest setHTTPBody:challengeResp]; - NSURLSessionTask* responseSession = [[NSURLSession sharedSession] dataTaskWithRequest:responseRequest completionHandler:^(NSData* turnCredentialsData, NSURLResponse* response, NSError* error) { + NSURLSession* responseSession = [HelperTools createEphemeralURLSession]; + [[responseSession dataTaskWithRequest:responseRequest completionHandler:^(NSData* turnCredentialsData, NSURLResponse* response, NSError* error) { if(error != nil || [(NSHTTPURLResponse*)response statusCode] != 200) { DDLogWarn(@"Could not retrieve turn credentials, only using stun: %@", error); @@ -466,10 +472,8 @@ -(void) initWebRTCForPendingCall:(MLCall*) call [iceServers addObject:[[RTCIceServer alloc] initWithURLStrings:[turnCredentials objectForKey:@"uris"] username:[turnCredentials objectForKey:@"username"] credential:[turnCredentials objectForKey:@"password"]]]; [self createWebRTCClientForCall:call usingICEServers:iceServers]; - }]; - [responseSession resume]; - }]; - [challengeSession resume]; + }] resume]; + }] resume]; } //continue without any stun/turn servers if only p2p but no stun/turn servers could be found on local xmpp server //AND no fallback to monal servers was configured diff --git a/Monal/Classes/MLWebViewController.m b/Monal/Classes/MLWebViewController.m index acbf434df5..ea42e4c95d 100644 --- a/Monal/Classes/MLWebViewController.m +++ b/Monal/Classes/MLWebViewController.m @@ -26,10 +26,12 @@ -(void) viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; if(self.urltoLoad.fileURL) - { [self.webview loadFileURL:self.urltoLoad allowingReadAccessToURL:self.urltoLoad]; - } else { - NSURLRequest* nsrequest = [NSURLRequest requestWithURL: self.urltoLoad]; + else + { + NSMutableURLRequest* nsrequest = [NSMutableURLRequest requestWithURL: self.urltoLoad]; + if(@available(iOS 16.1, macCatalyst 16.1, *)) + nsrequest.requiresDNSSECValidation = YES; [self.webview loadRequest:nsrequest]; } self.navigationItem.largeTitleDisplayMode = UINavigationItemLargeTitleDisplayModeNever; diff --git a/Monal/Classes/RegisterAccount.swift b/Monal/Classes/RegisterAccount.swift index f3bc791d4b..35d0c86616 100644 --- a/Monal/Classes/RegisterAccount.swift +++ b/Monal/Classes/RegisterAccount.swift @@ -17,7 +17,10 @@ struct WebView: UIViewRepresentable { } func updateUIView(_ webView: WKWebView, context: Context) { - let request = URLRequest(url: url) + var request = URLRequest(url: url) + if #available(iOS 16.1, macCatalyst 16.1, *) { + request.requiresDNSSECValidation = true; + } webView.load(request) } } diff --git a/Monal/Classes/chatViewController.m b/Monal/Classes/chatViewController.m index 34c78eb104..4474859b5f 100644 --- a/Monal/Classes/chatViewController.m +++ b/Monal/Classes/chatViewController.m @@ -3003,13 +3003,15 @@ -(void) loadPreviewWithUrlForRow:(NSIndexPath *) indexPath withResultHandler:(mo { DDLogVerbose(@"Fetching HTTP HEAD for %@...", row.url); NSMutableURLRequest* headRequest = [[NSMutableURLRequest alloc] initWithURL:row.url]; + if(@available(iOS 16.1, macCatalyst 16.1, *)) + headRequest.requiresDNSSECValidation = YES; headRequest.HTTPMethod = @"HEAD"; headRequest.cachePolicy = NSURLRequestReturnCacheDataElseLoad; - NSURLSession* session = [NSURLSession sharedSession]; + NSURLSession* session = [HelperTools createEphemeralURLSession]; [[session dataTaskWithRequest:headRequest completionHandler:^(NSData* _Nullable data, NSURLResponse* _Nullable response, NSError* _Nullable error) { if(error != nil) { - DDLogWarn(@"Loding preview HEAD for %@ failed: %@", row.url, error); + DDLogWarn(@"Loading preview HEAD for %@ failed: %@", row.url, error); resultHandler(); return; } @@ -3020,7 +3022,7 @@ -(void) loadPreviewWithUrlForRow:(NSIndexPath *) indexPath withResultHandler:(mo if(mimeType.length==0) { - DDLogWarn(@"Loding preview HEAD for %@ failed: mimeType unkown", row.url); + DDLogWarn(@"Loading preview HEAD for %@ failed: mimeType unkown", row.url); resultHandler(); return; } @@ -3035,7 +3037,7 @@ -(void) loadPreviewWithUrlForRow:(NSIndexPath *) indexPath withResultHandler:(mo } if(![mimeType hasPrefix:@"text/"]) { - DDLogWarn(@"Loding HEAD preview for %@ failed: mimeType not supported: %@", row.url, mimeType); + DDLogWarn(@"Loading HEAD preview for %@ failed: mimeType not supported: %@", row.url, mimeType); resultHandler(); return; } @@ -3076,11 +3078,14 @@ -(void) downloadPreviewWithRow:(NSIndexPath*) indexPath usingByterange:(BOOL) us */ DDLogVerbose(@"Fetching HTTP GET for %@...", row.url); NSMutableURLRequest* request = [[NSMutableURLRequest alloc] initWithURL:row.url]; + if(@available(iOS 16.1, macCatalyst 16.1, *)) + request.requiresDNSSECValidation = YES; [request setValue:@"facebookexternalhit/1.1" forHTTPHeaderField:@"User-Agent"]; //required on some sites for og tags e.g. youtube if(useByterange) [request setValue:@"bytes=0-524288" forHTTPHeaderField:@"Range"]; request.timeoutInterval = 10; - [[[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData* _Nullable data, NSURLResponse* _Nullable response, NSError* _Nullable error) { + NSURLSession* session = [HelperTools createEphemeralURLSession]; + [[session dataTaskWithRequest:request completionHandler:^(NSData* _Nullable data, NSURLResponse* _Nullable response, NSError* _Nullable error) { if(error != nil) DDLogVerbose(@"preview fetching error: %@", error); else From c2c0279f083afb054e4e28e52ca49967b0f8bdea Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Fri, 10 May 2024 03:15:48 +0200 Subject: [PATCH 022/143] Don't loop on repeated pubsub precondition fails At least on older ejabberd (~22.05) this can happen because it still returns precondition-not-met errors even after successfully configuring the node. --- Monal/Classes/MLPubSub.m | 35 +++++++++-------------------------- 1 file changed, 9 insertions(+), 26 deletions(-) diff --git a/Monal/Classes/MLPubSub.m b/Monal/Classes/MLPubSub.m index 75b6120384..bb180b2742 100644 --- a/Monal/Classes/MLPubSub.m +++ b/Monal/Classes/MLPubSub.m @@ -288,7 +288,7 @@ -(void) publishItem:(MLXMLNode*) item onNode:(NSString*) node withConfigOptions: //update config options with our own defaults if not already present configOptions = [self copyDefaultNodeOptions:_defaultOptions forConfigForm:nil into:configOptions]; - [self internalPublishItem:item onNode:node withConfigOptions:configOptions andHandler:handler]; + [self internalPublishItem:item onNode:node withConfigOptions:configOptions andHandler:handler andIsRetry:NO]; } -(void) retractItemWithId:(NSString*) itemId onNode:(NSString*) node @@ -499,7 +499,7 @@ -(void) handleHeadlineMessage:(XMPPMessage*) messageNode //*** internal methods below --(void) internalPublishItem:(MLXMLNode*) item onNode:(NSString*) node withConfigOptions:(NSDictionary*) configOptions andHandler:(MLHandler* _Nullable) handler +-(void) internalPublishItem:(MLXMLNode*) item onNode:(NSString*) node withConfigOptions:(NSDictionary*) configOptions andHandler:(MLHandler* _Nullable) handler andIsRetry:(BOOL) is_retry { DDLogDebug(@"Publishing item on node '%@': %@", node, item); XMPPIQ* query = [[XMPPIQ alloc] initWithType:kiqSetType]; @@ -515,7 +515,8 @@ -(void) internalPublishItem:(MLXMLNode*) item onNode:(NSString*) node withConfig $ID(item), $ID(node), $ID(configOptions), - $HANDLER(handler) + $HANDLER(handler), + $BOOL(is_retry) )]; } @@ -880,7 +881,7 @@ -(NSDictionary*) copyDefaultNodeOptions:(NSDictionary*) defaultOptions forConfig } //try again - [self internalPublishItem:item onNode:node withConfigOptions:configOptions andHandler:handler]; + [self internalPublishItem:item onNode:node withConfigOptions:configOptions andHandler:handler andIsRetry:YES]; $$ //this is a user handler for internalPublishItem: called from handlePublishResult @@ -907,31 +908,11 @@ -(NSDictionary*) copyDefaultNodeOptions:(NSDictionary*) defaultOptions forConfig $invalidate(handler, $ID(account), $BOOL(success, NO), $ID(node), $ID(reason)); $$ -$$instance_handler(handlePublishResult, account.pubsub, $$ID(xmpp*, account), $$ID(XMPPIQ*, iqNode), $$ID(MLXMLNode*, item), $$ID(NSString*, node), $$ID(NSDictionary*, configOptions), $_HANDLER(handler)) +$$instance_handler(handlePublishResult, account.pubsub, $$ID(xmpp*, account), $$ID(XMPPIQ*, iqNode), $$ID(MLXMLNode*, item), $$ID(NSString*, node), $$ID(NSDictionary*, configOptions), $_HANDLER(handler), $$BOOL(is_retry)) if([iqNode check:@"/"]) { - //NOTE: workaround for old ejabberd versions <= 21.07 only supporting two special settings as preconditions - if([@"http://www.process-one.net/en/ejabberd/" isEqualToString:account.connectionProperties.serverIdentity] && [configOptions count] > 0 && [iqNode check:@"error/{urn:ietf:params:xml:ns:xmpp-stanzas}resource-constraint"]) - { - DDLogWarn(@"ejabberd (~21.07) workaround for old preconditions handling active for node: %@", node); - - //make sure we don't try all preconditions from configOptions again: only these two listed preconditions are safe to use with ejabberd - NSMutableDictionary* publishPreconditions = [NSMutableDictionary new]; - if(configOptions[@"pubsub#persist_items"]) - publishPreconditions[@"pubsub#persist_items"] = configOptions[@"pubsub#persist_items"]; - if(configOptions[@"pubsub#access_model"]) - publishPreconditions[@"pubsub#access_model"] = configOptions[@"pubsub#access_model"]; - - [self internalPublishItem:item onNode:node withConfigOptions:publishPreconditions andHandler:$newHandlerWithInvalidation(self, handleConfigureAfterPublish, handleConfigureAfterPublishInvalidation, - $ID(node), - $ID(configOptions), - $HANDLER(handler) - )]; - return; - } - //check if this node is already present and configured --> reconfigure it according to our access-model - if([iqNode check:@"error/{http://jabber.org/protocol/pubsub#errors}precondition-not-met"]) + if(!is_retry && [iqNode check:@"error/{http://jabber.org/protocol/pubsub#errors}precondition-not-met"]) { DDLogWarn(@"Node precondition not met, reconfiguring node: %@", node); [self configureNode:node withConfigOptions:configOptions andHandler:$newHandlerWithInvalidation(self, handlePublishAgain, handlePublishAgainInvalidation, @@ -942,6 +923,8 @@ -(NSDictionary*) copyDefaultNodeOptions:(NSDictionary*) defaultOptions forConfig )]; return; } + if(is_retry && [iqNode check:@"error/{http://jabber.org/protocol/pubsub#errors}precondition-not-met"]) + DDLogError(@"Node precondition not met even after reconfiguring node, aborting: %@", node); //all other errors are real errors --> inform user handler $call(handler, $ID(account), $BOOL(success, NO), $ID(node), $ID(errorIq, iqNode)); From c640946dddf0069f6576802b66565030a2dd3649 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Fri, 10 May 2024 03:48:32 +0200 Subject: [PATCH 023/143] Don't advertise support for urn:xmpp:idle:1 if deactivated by user This makes sure other users won't see a stale time or a constant "currently online". Fixes #1059 --- Monal/Classes/HelperTools.m | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Monal/Classes/HelperTools.m b/Monal/Classes/HelperTools.m index 18a2928b04..9f1d759ada 100644 --- a/Monal/Classes/HelperTools.m +++ b/Monal/Classes/HelperTools.m @@ -1983,7 +1983,6 @@ +(NSSet*) getOwnFeatureSet @"jabber:x:oob", @"urn:xmpp:ping", @"urn:xmpp:receipts", - @"urn:xmpp:idle:1", @"http://jabber.org/protocol/chatstates", @"urn:xmpp:chat-markers:0", @"urn:xmpp:eme:0", @@ -1992,6 +1991,8 @@ +(NSSet*) getOwnFeatureSet ] mutableCopy]; + if([[HelperTools defaultsDB] boolForKey: @"SendLastUserInteraction"]) + [featuresArray addObject:@"urn:xmpp:idle:1"]; if([[HelperTools defaultsDB] boolForKey: @"allowVersionIQ"]) [featuresArray addObject:@"jabber:iq:version"]; //voip stuff From 5417ea262d056a466caec15298a10a89d6c9dbdd Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Fri, 10 May 2024 07:45:20 +0200 Subject: [PATCH 024/143] Change advertised caps hash for privacy setting changes Changing one of the "Communication" privacy settings triggers a presence containing an updated capabilities hash. --- Monal/Classes/HelperTools.m | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Monal/Classes/HelperTools.m b/Monal/Classes/HelperTools.m index 9f1d759ada..1209e3abba 100644 --- a/Monal/Classes/HelperTools.m +++ b/Monal/Classes/HelperTools.m @@ -1982,9 +1982,6 @@ +(NSSet*) getOwnFeatureSet @"jabber:x:conference", @"jabber:x:oob", @"urn:xmpp:ping", - @"urn:xmpp:receipts", - @"http://jabber.org/protocol/chatstates", - @"urn:xmpp:chat-markers:0", @"urn:xmpp:eme:0", @"urn:xmpp:message-retract:1", @"urn:xmpp:message-correct:0", @@ -1993,6 +1990,12 @@ +(NSSet*) getOwnFeatureSet ] mutableCopy]; if([[HelperTools defaultsDB] boolForKey: @"SendLastUserInteraction"]) [featuresArray addObject:@"urn:xmpp:idle:1"]; + if([[HelperTools defaultsDB] boolForKey: @"SendLastChatState"]) + [featuresArray addObject:@"http://jabber.org/protocol/chatstates"]; + if([[HelperTools defaultsDB] boolForKey: @"SendReceivedMarkers"]) + [featuresArray addObject:@"urn:xmpp:receipts"]; + if([[HelperTools defaultsDB] boolForKey: @"SendDisplayedMarkers"]) + [featuresArray addObject:@"urn:xmpp:chat-markers:0"]; if([[HelperTools defaultsDB] boolForKey: @"allowVersionIQ"]) [featuresArray addObject:@"jabber:iq:version"]; //voip stuff From f515e71cedc3717db804edeaf6dbc5160f571128 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Fri, 10 May 2024 08:46:37 +0200 Subject: [PATCH 025/143] Allo comparison of ObservableKVOWrapper with object of wrapped type --- Monal/Classes/SwiftHelpers.swift | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/Monal/Classes/SwiftHelpers.swift b/Monal/Classes/SwiftHelpers.swift index 82ad5e63d2..4577e4879a 100644 --- a/Monal/Classes/SwiftHelpers.swift +++ b/Monal/Classes/SwiftHelpers.swift @@ -168,11 +168,31 @@ public class ObservableKVOWrapper: ObservableObject, Hashable, return lhs.obj.isEqual(rhs.obj) } + @inlinable + public static func ==(lhs: ObservableKVOWrapper, rhs: ObjType) -> Bool { + return lhs.obj.isEqual(rhs) + } + + @inlinable + public static func ==(lhs: ObjType, rhs: ObservableKVOWrapper) -> Bool { + return lhs.isEqual(rhs.obj) + } + // see https://stackoverflow.com/a/33320737 @inlinable public static func ===(lhs: ObservableKVOWrapper, rhs: ObservableKVOWrapper) -> Bool { return lhs.obj === rhs.obj } + + @inlinable + public static func ===(lhs: ObservableKVOWrapper, rhs: ObjType) -> Bool { + return lhs.obj === rhs + } + + @inlinable + public static func ===(lhs: ObjType, rhs: ObservableKVOWrapper) -> Bool { + return lhs === rhs.obj + } @inlinable public func hash(into hasher: inout Hasher) { From 7808d691a1953b5d3ed026c2872e51b106a9e113 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Fri, 10 May 2024 10:17:45 +0200 Subject: [PATCH 026/143] Make MLContact instances true singletons This also add the previously in MLXMLNode defined WeakContainer to HelperTools. --- Monal/Classes/HelperTools.h | 6 +++ Monal/Classes/HelperTools.m | 9 +++++ Monal/Classes/MLContact.m | 79 ++++++++++++++++++++++++------------- Monal/Classes/MLXMLNode.m | 15 ------- 4 files changed, 66 insertions(+), 43 deletions(-) diff --git a/Monal/Classes/HelperTools.h b/Monal/Classes/HelperTools.h index 446b73f549..dc149d6722 100644 --- a/Monal/Classes/HelperTools.h +++ b/Monal/Classes/HelperTools.h @@ -56,6 +56,12 @@ typedef NS_ENUM(NSUInteger, MLRunLoopIdentifier) { void logException(NSException* exception); void swizzle(Class c, SEL orig, SEL new); +//weak container holding an object as weak pointer (needed to not create retain circles in NSCache +@interface WeakContainer : NSObject +@property (nonatomic, weak) id obj; +-(id) initWithObj:(id) obj; +@end + @interface HelperTools : NSObject @property (class, nonatomic, strong, nullable) DDFileLogger* fileLogger; diff --git a/Monal/Classes/HelperTools.m b/Monal/Classes/HelperTools.m index 1209e3abba..f63eb3b6ef 100644 --- a/Monal/Classes/HelperTools.m +++ b/Monal/Classes/HelperTools.m @@ -278,6 +278,15 @@ void swizzle(Class c, SEL orig, SEL new) method_exchangeImplementations(origMethod, newMethod); } +@implementation WeakContainer +-(id) initWithObj:(id) obj +{ + self = [super init]; + self.obj = obj; + return self; +} +@end + @implementation HelperTools +(void) initialize diff --git a/Monal/Classes/MLContact.m b/Monal/Classes/MLContact.m index 499559b31c..a3314d02c8 100644 --- a/Monal/Classes/MLContact.m +++ b/Monal/Classes/MLContact.m @@ -28,6 +28,8 @@ NSString* const kSubRemove = @"remove"; NSString* const kAskSubscribe = @"subscribe"; +static NSMutableDictionary* _singletonCache; + @interface MLContact () { NSInteger _unreadCount; @@ -69,6 +71,11 @@ @interface MLContact () @implementation MLContact ++(void) initialize +{ + _singletonCache = [NSMutableDictionary new]; +} + +(MLContact*) makeDummyContact:(int) type { if(type == 1) @@ -191,36 +198,52 @@ +(MLContact*) createContactFromJid:(NSString*) jid andAccountNo:(NSNumber*) acco { MLAssert(jid != nil, @"jid must not be nil"); MLAssert(accountNo != nil && accountNo.intValue >= 0, @"accountNo must not be nil and > 0"); - NSDictionary* contactDict = [[DataLayer sharedInstance] contactDictionaryForUsername:jid forAccount:accountNo]; - // check if we know this contact and return a dummy one if not - if(contactDict == nil) - { - DDLogInfo(@"Returning dummy MLContact for %@ on accountNo %@", jid, accountNo); - return [self contactFromDictionary:@{ - @"buddy_name": jid.lowercaseString, - @"nick_name": @"", - @"full_name": @"", - @"subscription": kSubNone, - @"ask": @"", - @"account_id": accountNo, - //@"muc_subject": nil, - //@"muc_nick": nil, - @"Muc": @NO, - @"mentionOnly": @NO, - @"pinned": @NO, - @"blocked": @NO, - @"encrypt": @NO, - @"muted": @NO, - @"status": @"", - @"state": @"offline", - @"count": @0, - @"isActiveChat": @NO, - @"lastInteraction": nilWrapper(nil), - }]; + NSString* cacheKey = [NSString stringWithFormat:@"%@|%@", accountNo, jid]; + @synchronized(_singletonCache) { + if(_singletonCache[cacheKey] != nil) + { + if(((WeakContainer*)_singletonCache[cacheKey]).obj != nil) + return ((WeakContainer*)_singletonCache[cacheKey]).obj; + else + [_singletonCache removeObjectForKey:cacheKey]; + } + + NSDictionary* contactDict = [[DataLayer sharedInstance] contactDictionaryForUsername:jid forAccount:accountNo]; + MLContact* retval = nil; + + // check if we know this contact and return a dummy one if not + if(contactDict == nil) + { + DDLogInfo(@"Returning dummy MLContact for %@ on accountNo %@", jid, accountNo); + retval = [self contactFromDictionary:@{ + @"buddy_name": jid.lowercaseString, + @"nick_name": @"", + @"full_name": @"", + @"subscription": kSubNone, + @"ask": @"", + @"account_id": accountNo, + //@"muc_subject": nil, + //@"muc_nick": nil, + @"Muc": @NO, + @"mentionOnly": @NO, + @"pinned": @NO, + @"blocked": @NO, + @"encrypt": @NO, + @"muted": @NO, + @"status": @"", + @"state": @"offline", + @"count": @0, + @"isActiveChat": @NO, + @"lastInteraction": nilWrapper(nil), + }]; + } + else + retval = [self contactFromDictionary:contactDict]; + + _singletonCache[cacheKey] = [[WeakContainer alloc] initWithObj:retval]; + return retval; } - else - return [self contactFromDictionary:contactDict]; } -(instancetype) init diff --git a/Monal/Classes/MLXMLNode.m b/Monal/Classes/MLXMLNode.m index 3df5af0288..0c31545d97 100644 --- a/Monal/Classes/MLXMLNode.m +++ b/Monal/Classes/MLXMLNode.m @@ -23,21 +23,6 @@ //this is the required prototype from Holger's snprintf.c int rpl_vasprintf(char **, const char *, va_list *); - -//weak container holding an object as weak pointer (needed to not create retain circles in NSCache -@interface WeakContainer : NSObject -@property (nonatomic, weak) id obj; -@end -@implementation WeakContainer --(id) initWithObj:(id) obj -{ - self = [super init]; - self.obj = obj; - return self; -} -@end - - @interface MLXMLNode() { NSMutableArray* _children; From a37e0bd7bee688c4b17b0686afb0698cc41934f9 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Fri, 10 May 2024 10:42:20 +0200 Subject: [PATCH 027/143] Fix crash in contact details --- Monal/Classes/ContactDetails.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Monal/Classes/ContactDetails.swift b/Monal/Classes/ContactDetails.swift index 4ecc37c02d..d89dabe8b1 100644 --- a/Monal/Classes/ContactDetails.swift +++ b/Monal/Classes/ContactDetails.swift @@ -120,7 +120,7 @@ struct ContactDetails: View { } } - if !contact.isGroup && (contact.statusMessage as String).count > 0 { + if !contact.isGroup, let statusMessage = contact.statusMessage as String?, statusMessage.count > 0 { VStack { Text("Status message:") Text(contact.statusMessage as String) From 5f20a13c841a672c56dc69141a469a97faafb920 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Fri, 10 May 2024 10:48:12 +0200 Subject: [PATCH 028/143] Improve appearance of LoadingOverlay --- Monal/Classes/LoadingOverlay.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Monal/Classes/LoadingOverlay.swift b/Monal/Classes/LoadingOverlay.swift index ac713970bc..97387f815a 100644 --- a/Monal/Classes/LoadingOverlay.swift +++ b/Monal/Classes/LoadingOverlay.swift @@ -35,7 +35,8 @@ struct LoadingOverlay: ViewModifier { state.description.font(.footnote) ProgressView() } - .frame(width: 250, height: 100) + .padding(12) + .frame(minWidth: 250, minHeight: 100) .background(Color.secondary.colorInvert()) .cornerRadius(20) .transaction { transaction in transaction.animation = nil} From c3313b2765ff51eadb4f7a1bc25041d580ab42ff Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Fri, 10 May 2024 11:01:07 +0200 Subject: [PATCH 029/143] Make group members editor fully dynamic and handle all errors --- Monal/Classes/ContactPicker.swift | 48 ++++++-- Monal/Classes/MLConstants.h | 1 + Monal/Classes/MLMucProcessor.m | 15 ++- Monal/Classes/MemberList.swift | 189 +++++++++++++++++++++--------- 4 files changed, 183 insertions(+), 70 deletions(-) diff --git a/Monal/Classes/ContactPicker.swift b/Monal/Classes/ContactPicker.swift index 0ca4f833e2..9aff509a5f 100644 --- a/Monal/Classes/ContactPicker.swift +++ b/Monal/Classes/ContactPicker.swift @@ -32,33 +32,56 @@ struct ContactPickerEntry: View { } struct ContactPicker: View { + typealias completionType = (OrderedSet>)->Void + let account: xmpp @Environment(\.presentationMode) private var presentationMode @Binding var returnedContacts: OrderedSet> - @State var allContacts: OrderedSet> @State var selectedContacts: OrderedSet> @State var searchText = "" @State var isEditingSearchInput = false let allowRemoval: Bool + let completion: completionType? + init(_ account: xmpp, initializeFrom contacts: OrderedSet>, allowRemoval: Bool = true, completion:completionType?) { + self.account = account + self.allowRemoval = allowRemoval + self.completion = completion + _selectedContacts = State(wrappedValue:OrderedSet()) + //use a temporary storage because we don't have a binding to the outside world but use the completion handler + var storage = contacts + _returnedContacts = Binding( + get: { storage }, + set: { storage = $0 } + ) + buildPreselectedContacts(contacts) + DDLogError("self.allowRemoval = \(String(describing:self.allowRemoval))") + } + init(_ account: xmpp, binding returnedContacts: Binding>>, allowRemoval: Bool = true) { + self.account = account self.allowRemoval = allowRemoval - var contactsTmp: OrderedSet> = OrderedSet() - + self.completion = nil + _selectedContacts = State(wrappedValue:OrderedSet()) + _returnedContacts = returnedContacts + buildPreselectedContacts(returnedContacts.wrappedValue) + } + + private mutating func buildPreselectedContacts(_ source: OrderedSet>) { //build currently selected list of contacts - contactsTmp.removeAll() - for contact in returnedContacts.wrappedValue { + var contactsTmp: OrderedSet> = OrderedSet() + for contact in source { contactsTmp.append(contact) } - _selectedContacts = State(wrappedValue: contactsTmp) - + _selectedContacts = State(wrappedValue:contactsTmp) + } + + private var allContacts: OrderedSet> { //build list of all possible contacts on this account (excluding selfchat and other mucs) - contactsTmp.removeAll() + var contactsTmp: OrderedSet> = OrderedSet() for contact in DataLayer.sharedInstance().possibleGroupMembers(forAccount: account.accountNo) { contactsTmp.append(ObservableKVOWrapper(contact)) } - _allContacts = State(wrappedValue: contactsTmp) - - _returnedContacts = returnedContacts + return contactsTmp } private var searchResults : OrderedSet> { @@ -110,6 +133,9 @@ struct ContactPicker: View { for contact in selectedContacts { returnedContacts.append(contact) } + if let completion = completion { + completion(returnedContacts) + } } } } diff --git a/Monal/Classes/MLConstants.h b/Monal/Classes/MLConstants.h index f74fd3d8bc..b6c26bd941 100644 --- a/Monal/Classes/MLConstants.h +++ b/Monal/Classes/MLConstants.h @@ -173,6 +173,7 @@ static inline NSString* _Nonnull LocalizationNotNeeded(NSString* _Nonnull s) #define kMonalXmppUserSoftWareVersionRefresh @"kMonalXmppUserSoftWareVersionRefresh" #define kMonalBlockListRefresh @"kMonalBlockListRefresh" #define kMonalContactRemoved @"kMonalContactRemoved" +#define kMonalMucParticipantsAndMembersUpdated @"kMonalMucParticipantsAndMembersUpdated" // max count of char's in a single message (both: sending and receiving) #define kMonalChatMaxAllowedTextLen 2048 diff --git a/Monal/Classes/MLMucProcessor.m b/Monal/Classes/MLMucProcessor.m index 4702b592a8..7a95d311d5 100644 --- a/Monal/Classes/MLMucProcessor.m +++ b/Monal/Classes/MLMucProcessor.m @@ -350,9 +350,17 @@ -(void) processPresence:(XMPPPresence*) presenceNode else [[DataLayer sharedInstance] addParticipant:item toMuc:presenceNode.fromUser forAccountId:_account.accountNo]; - //handle members updates + //handle members updates (publishing the changes in members/participants is already handled by handleMembersListUpdate + //--> only publish if we don't call handleMembersListUpdate if(item[@"jid"] != nil) [self handleMembersListUpdate:[presenceNode find:@"{http://jabber.org/protocol/muc#user}x/item@@"] forMuc:presenceNode.fromUser]; + else + { + DDLogDebug(@"Publishing participants list update..."); + [[MLNotificationQueue currentQueue] postNotificationName:kMonalMucParticipantsAndMembersUpdated object:_account userInfo:@{ + @"contact": [MLContact createContactFromJid:presenceNode.fromUser andAccountNo:_account.accountNo] + }]; + } } else DDLogDebug(@"Ignoring unavailable presences of room being destroyed by us..."); @@ -462,6 +470,11 @@ -(void) handleMembersListUpdate:(NSArray*) items forMuc:(NSString #endif// DISABLE_OMEMO } } + + DDLogDebug(@"Publishing new memberslist..."); + [[MLNotificationQueue currentQueue] postNotificationName:kMonalMucParticipantsAndMembersUpdated object:_account userInfo:@{ + @"contact": [MLContact createContactFromJid:mucJid andAccountNo:_account.accountNo] + }]; } else DDLogWarn(@"Ignoring handleMembersListUpdate for %@, MUC not in buddylist", mucJid); diff --git a/Monal/Classes/MemberList.swift b/Monal/Classes/MemberList.swift index bc126d5618..7a5dc2ad36 100644 --- a/Monal/Classes/MemberList.swift +++ b/Monal/Classes/MemberList.swift @@ -16,7 +16,7 @@ struct ActionSheetPrompt { struct MemberList: View { private let account: xmpp - private let ownAffiliation: String; + @State private var ownAffiliation: String; @StateObject var group: ObservableKVOWrapper @State private var memberList: OrderedSet> @State private var affiliations: Dictionary, String> @@ -25,21 +25,41 @@ struct MemberList: View { @State private var alertPrompt = AlertPrompt(dismissLabel: Text("Close")) @State private var showActionSheet = false @State private var actionSheetPrompt = ActionSheetPrompt() + @StateObject private var overlay = LoadingOverlayState() init(mucContact: ObservableKVOWrapper) { account = MLXMPPManager.sharedInstance().getConnectedAccount(forID: mucContact.accountId)! as xmpp - _group = StateObject(wrappedValue: mucContact) - _memberList = State(wrappedValue: getContactList(viewContact: mucContact)) - ownAffiliation = DataLayer.sharedInstance().getOwnAffiliation(inGroupOrChannel:mucContact.obj) ?? "none" + _group = StateObject(wrappedValue:mucContact) + _ownAffiliation = State(wrappedValue:"none") + _memberList = State(wrappedValue:OrderedSet>()) + _affiliations = State(wrappedValue:[:]) + } + + func updateMemberlist() { + memberList = getContactList(viewContact:group) + ownAffiliation = DataLayer.sharedInstance().getOwnAffiliation(inGroupOrChannel:group.obj) ?? "none" var affiliationTmp = Dictionary, String>() - for memberInfo in Array(DataLayer.sharedInstance().getMembersAndParticipants(ofMuc: mucContact.contactJid, forAccountId: account.accountNo)) { + for memberInfo in Array(DataLayer.sharedInstance().getMembersAndParticipants(ofMuc:group.contactJid, forAccountId:account.accountNo)) { guard let jid = memberInfo["participant_jid"] as? String ?? memberInfo["member_jid"] as? String else { continue } let contact = ObservableKVOWrapper(MLContact.createContact(fromJid:jid, andAccountNo:account.accountNo)) affiliationTmp[contact] = memberInfo["affiliation"] as? String ?? "none" } - _affiliations = State(wrappedValue: affiliationTmp) + affiliations = affiliationTmp + } + + func performAction(_ title: Text, action: @escaping ()->Void) { + action() + self.account.mucProcessor.addUIHandler({_data in let data = _data as! NSDictionary + DispatchQueue.main.async { + hideLoadingOverlay(overlay) + let success : Bool = data["success"] as! Bool; + if !success { + showAlert(title: title, description: Text(data["errorMessage"] as! String)) + } + } + }, forMuc:group.contactJid) } func showAlert(title: Text, description: Text) { @@ -69,98 +89,139 @@ struct MemberList: View { return false } - func affiliationToText(_ affiliation: String?) -> some View { + func actionsAllowed(for contact:ObservableKVOWrapper) -> [String] { + if let contactAffiliation = affiliations[contact] { + if ownAffiliation == "owner" { + return ["profile", "owner", "admin", "member", "outcast"] + } else { //only admin left, because other affiliations don't call actionsAllowed at all + if ["member", "outcast"].contains(contactAffiliation) { + return ["profile", "member", "outcast"] + } else { + //return contact affiliation because that should be displayed as selected in picker + return ["profile", contactAffiliation] + } + } + } + return ["profile"] + } + + func affiliationToString(_ affiliation: String?) -> String { if let affiliation = affiliation { if affiliation == "owner" { - return Text("Owner") + return NSLocalizedString("Owner", comment:"muc affiliation") } else if affiliation == "admin" { - return Text("Admin") + return NSLocalizedString("Admin", comment:"muc affiliation") } else if affiliation == "member" { - return Text("Member") + return NSLocalizedString("Member", comment:"muc affiliation") } else if affiliation == "outcast" { - return Text("Blocked") + return NSLocalizedString("Blocked", comment:"muc affiliation") } else if affiliation == "profile" { - return Text("Open contact details") + return NSLocalizedString("Open contact details", comment:"") } } - return Text("") + return NSLocalizedString("", comment:"muc affiliation") } var body: some View { List { - Section(header: Text(self.group.obj.contactDisplayName)) { + Section(header: Text("\(self.group.contactDisplayName as String) (affiliation: \(affiliationToString(ownAffiliation)))")) { if ownAffiliation == "owner" || ownAffiliation == "admin" { - NavigationLink(destination: LazyClosureView(ContactPicker(account, binding: $memberList, allowRemoval: false))) { + NavigationLink(destination: LazyClosureView(ContactPicker(account, initializeFrom: memberList, allowRemoval: false) { newMemberList in + for member in newMemberList { + if !memberList.contains(member) { + showLoadingOverlay(overlay, headlineView: Text("Adding new member"), descriptionView: Text("Adding \(member.contactJid as String)...")) + performAction(Text("Error adding new member!")) { + DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 1.0) { + account.mucProcessor.setAffiliation("member", ofUser:member.contactJid, inMuc:self.group.contactJid) + account.mucProcessor.inviteUser(member.contactJid, inMuc: self.group.contactJid) + } + } + } + } + })) { Text("Add Group Members") } - } - ForEach(memberList, id:\.self) { contact in - if !contact.isSelfChat { - HStack(alignment: .center) { - ContactEntry(contact:contact) - - Spacer() - - if ownAffiliation == "owner" || ownAffiliation == "admin" { + + ForEach(memberList, id:\.self) { contact in + if !contact.isSelfChat { + HStack(alignment: .center) { + ContactEntry(contact:contact) + Spacer() Picker(selection: Binding( get: { affiliations[contact] ?? "none" }, set: { newAffiliation in + if newAffiliation == affiliations[contact] { + return + } if newAffiliation == "profile" { DDLogVerbose("Activating navigation to \(String(describing:contact))") navigationActive = contact } else if newAffiliation == "outcast" { showActionSheet(title: Text("Block user?"), description: Text("Do you want to block this user from entering this group/channel?")) { DDLogVerbose("Changing affiliation of \(String(describing:contact)) to: \(String(describing:newAffiliation))...") - affiliations[contact] = newAffiliation - account.mucProcessor.setAffiliation(newAffiliation, ofUser:contact.contactJid, inMuc:self.group.contactJid) + showLoadingOverlay(overlay, headlineView: Text("Blocking member"), descriptionView: Text("Blocking \(contact.contactJid as String)...")) + performAction(Text("Error blocking user!")) { + DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 1.0) { + account.mucProcessor.setAffiliation(newAffiliation, ofUser:contact.contactJid, inMuc:self.group.contactJid) + } + } } } else { DDLogVerbose("Changing affiliation of \(String(describing:contact)) to: \(String(describing:newAffiliation))...") - affiliations[contact] = newAffiliation - account.mucProcessor.setAffiliation(newAffiliation, ofUser:contact.contactJid, inMuc:self.group.contactJid) + showLoadingOverlay(overlay, headlineView: Text("Changing affiliation of member"), descriptionView: + Text("Changing \(contact.contactJid as String) to ") + Text(affiliationToString(newAffiliation)) + Text("...")) + performAction(Text("Error changing affiliation!")) { + DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 1.0) { + account.mucProcessor.setAffiliation(newAffiliation, ofUser:contact.contactJid, inMuc:self.group.contactJid) + } + } } } ), label: EmptyView()) { - ForEach(["profile", "owner", "admin", "member", "outcast"], id:\.self) { affiliation in - affiliationToText(affiliation).tag(affiliation) + ForEach(actionsAllowed(for:contact), id:\.self) { affiliation in + Text(affiliationToString(affiliation)).tag(affiliation) } } .pickerStyle(.menu) - } else { - affiliationToText(affiliations[contact]) + //invisible navigation link triggered programmatically + .background( + NavigationLink(destination: LazyClosureView(ContactDetails(delegate:SheetDismisserProtocol(), contact:contact)), tag:contact, selection:$navigationActive) { EmptyView() } + .opacity(0) + ) } + .deleteDisabled( + !ownUserHasAffiliationToRemove(contact: contact) + ) } - .deleteDisabled( - !ownUserHasAffiliationToRemove(contact: contact) - ) - //invisible navigation link triggered programmatically - .background( - NavigationLink(destination: LazyClosureView(ContactDetails(delegate:SheetDismisserProtocol(), contact:contact)), tag:contact, selection:$navigationActive) { EmptyView() } - .opacity(0) - ) } - } - .onDelete(perform: { memberIdx in - let member = memberList[memberIdx.first!] - showActionSheet(title: Text("Remove user?"), description: self.group.mucType == "group" ? Text("Do you want to remove this user from this group? The user won't be able to enter it again until added back to the group.") : Text("Do you want to remove this user from this channel? The user will be able to enter it again.")) { - account.mucProcessor.setAffiliation("none", ofUser: member.contactJid, inMuc: self.group.contactJid) - self.showAlert(title: Text("User removed"), description: Text("\(memberList[memberIdx.first!].obj.contactJid)")) - memberList.remove(at: memberIdx.first!) + .onDelete(perform: { memberIdx in + let member = memberList[memberIdx.first!] + showActionSheet(title: Text("Remove user?"), description: self.group.mucType == "group" ? Text("Do you want to remove this user from this group? The user won't be able to enter it again until added back to the group.") : Text("Do you want to remove this user from this channel? The user will be able to enter it again.")) { + showLoadingOverlay(overlay, headlineView: Text("Removing member"), descriptionView: Text("Removing \(member.contactJid as String)...")) + performAction(Text("Error removing user!")) { + DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 1.0) { + account.mucProcessor.setAffiliation("none", ofUser: member.contactJid, inMuc: self.group.contactJid) + } + } + } + }) + } else { + ForEach(memberList, id:\.self) { contact in + if !contact.isSelfChat { + NavigationLink(destination: LazyClosureView(ContactDetails(delegate:SheetDismisserProtocol(), contact:contact))) { + HStack(alignment: .center) { + ContactEntry(contact:contact) + Spacer() + Text(affiliationToString(affiliations[contact])) + } + } + .deleteDisabled(true) + } } - }) - } - } - .onChange(of: memberList) { [previousMemberList = memberList] newMemberList in - // only handle new members (added via the contact picker) - for member in newMemberList { - if !previousMemberList.contains(member) { - // add selected group member with affiliation member - affiliations[member] = "member" - account.mucProcessor.setAffiliation("member", ofUser:member.contactJid, inMuc:self.group.contactJid) - account.mucProcessor.inviteUser(member.contactJid, inMuc: self.group.contactJid) } } } + .addLoadingOverlay(overlay) .alert(isPresented: $showAlert, content: { Alert(title: alertPrompt.title, message: alertPrompt.message, dismissButton: .default(alertPrompt.dismissLabel)) }) @@ -178,6 +239,18 @@ struct MemberList: View { ) } .navigationBarTitle("Group Members", displayMode: .inline) + .onAppear { + updateMemberlist() + } + .onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("kMonalMucParticipantsAndMembersUpdated")).receive(on: RunLoop.main)) { notification in + if let xmppAccount = notification.object as? xmpp, let contact = notification.userInfo?["contact"] as? MLContact { + DDLogVerbose("Got muc participants/members update from account \(xmppAccount)...") + if contact == group { + updateMemberlist() + hideLoadingOverlay(overlay) + } + } + } } } From 21ed2f0477e62569150c14752126b23a829237c0 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Fri, 10 May 2024 12:48:11 +0200 Subject: [PATCH 030/143] Implement channel management This uses the slightly adapted group management gui. --- Monal/Classes/ContactDetails.swift | 11 +++++--- Monal/Classes/MemberList.swift | 42 +++++++++++++++++++++++------- Monal/Classes/SwiftuiHelpers.swift | 2 +- 3 files changed, 41 insertions(+), 14 deletions(-) diff --git a/Monal/Classes/ContactDetails.swift b/Monal/Classes/ContactDetails.swift index d89dabe8b1..90daf3fa15 100644 --- a/Monal/Classes/ContactDetails.swift +++ b/Monal/Classes/ContactDetails.swift @@ -323,8 +323,14 @@ struct ContactDetails: View { Text("Group Members") } } else if contact.obj.isGroup && contact.obj.mucType == "channel" { - NavigationLink(destination: LazyClosureView(ChannelMemberList(mucContact:contact))) { - Text("Channel Members") + if ["owner", "admin"].contains(DataLayer.sharedInstance().getOwnAffiliation(inGroupOrChannel:contact.obj) ?? "none") { + NavigationLink(destination: LazyClosureView(MemberList(mucContact:contact))) { + Text("Channel Members") + } + } else { + NavigationLink(destination: LazyClosureView(ChannelMemberList(mucContact:contact))) { + Text("Channel Members") + } } } } @@ -463,7 +469,6 @@ struct ContactDetails: View { if let callback = data["callback"] { self.successCallback = objcCast(callback) as monal_void_block_t } - DDLogError("callback: \(String(describing:self.successCallback))") successAlert(title: Text("Success"), message: contact.mucType == "group" ? Text("Successfully destroyed group.") : Text("Successfully destroyed channel.")) } else { errorAlert(title: Text("Error destroying group!"), message: Text(data["errorMessage"] as! String)) diff --git a/Monal/Classes/MemberList.swift b/Monal/Classes/MemberList.swift index 7a5dc2ad36..ac273d308c 100644 --- a/Monal/Classes/MemberList.swift +++ b/Monal/Classes/MemberList.swift @@ -76,6 +76,10 @@ struct MemberList: View { } func ownUserHasAffiliationToRemove(contact: ObservableKVOWrapper) -> Bool { + //we don't want to set affiliation=none in channels using deletion swipe (this does not delete the user) + if group.mucType == "channel" { + return false + } if contact.contactJid == account.connectionProperties.identity.jid { return false } @@ -90,19 +94,35 @@ struct MemberList: View { } func actionsAllowed(for contact:ObservableKVOWrapper) -> [String] { - if let contactAffiliation = affiliations[contact] { - if ownAffiliation == "owner" { - return ["profile", "owner", "admin", "member", "outcast"] - } else { //only admin left, because other affiliations don't call actionsAllowed at all - if ["member", "outcast"].contains(contactAffiliation) { - return ["profile", "member", "outcast"] - } else { - //return contact affiliation because that should be displayed as selected in picker - return ["profile", contactAffiliation] + if group.mucType == "group" { + if let contactAffiliation = affiliations[contact] { + if ownAffiliation == "owner" { + return ["profile", "owner", "admin", "member", "outcast"] + } else { //only admin left, because other affiliations don't call actionsAllowed at all + if ["member", "outcast"].contains(contactAffiliation) { + return ["profile", "member", "outcast"] + } else { + //return contact affiliation because that should be displayed as selected in picker + return ["profile", contactAffiliation] + } + } + } + return ["profile"] + } else { + if let contactAffiliation = affiliations[contact] { + if ownAffiliation == "owner" { + return ["profile", "owner", "admin", "member", "none", "outcast"] + } else { //only admin left, because other affiliations don't call actionsAllowed at all + if ["member", "none", "outcast"].contains(contactAffiliation) { + return ["profile", "member", "none", "outcast"] + } else { + //return contact affiliation because that should be displayed as selected in picker + return ["profile", contactAffiliation] + } } } + return ["profile", "none"] } - return ["profile"] } func affiliationToString(_ affiliation: String?) -> String { @@ -113,6 +133,8 @@ struct MemberList: View { return NSLocalizedString("Admin", comment:"muc affiliation") } else if affiliation == "member" { return NSLocalizedString("Member", comment:"muc affiliation") + } else if affiliation == "none" { + return NSLocalizedString("Participant", comment:"muc affiliation") } else if affiliation == "outcast" { return NSLocalizedString("Blocked", comment:"muc affiliation") } else if affiliation == "profile" { diff --git a/Monal/Classes/SwiftuiHelpers.swift b/Monal/Classes/SwiftuiHelpers.swift index e7ff5206ba..f4539a6ad1 100644 --- a/Monal/Classes/SwiftuiHelpers.swift +++ b/Monal/Classes/SwiftuiHelpers.swift @@ -68,7 +68,7 @@ class SheetDismisserProtocol: ObservableObject { func getContactList(viewContact: (ObservableKVOWrapper?)) -> OrderedSet> { if let contact = viewContact { - if(contact.isGroup && contact.mucType == "group") { + if(contact.isGroup) { //this uses the account the muc belongs to and treats every other account to be remote, //even when multiple accounts of the same monal instance are in the same group var contactList : OrderedSet> = OrderedSet() From 75e1502020ae4e6c15dd6201cf3f3addbb0bca4b Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Fri, 10 May 2024 12:59:40 +0200 Subject: [PATCH 031/143] Dynamically handle affiliation and role changes in contact details --- Monal/Classes/ContactDetails.swift | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/Monal/Classes/ContactDetails.swift b/Monal/Classes/ContactDetails.swift index 90daf3fa15..f44e31aae0 100644 --- a/Monal/Classes/ContactDetails.swift +++ b/Monal/Classes/ContactDetails.swift @@ -9,8 +9,8 @@ struct ContactDetails: View { var delegate: SheetDismisserProtocol private var account: xmpp - private var ownRole: String - private var ownAffiliation: String + @State private var ownRole = "participant" + @State private var ownAffiliation = "none" @StateObject var contact: ObservableKVOWrapper @State private var showingBlockContactConfirmation = false @State private var showingCannotBlockAlert = false @@ -35,16 +35,18 @@ struct ContactDetails: View { self.delegate = delegate _contact = StateObject(wrappedValue: contact) self.account = MLXMPPManager.sharedInstance().getConnectedAccount(forID: contact.accountId)! + } + private func updateRoleAndAffiliation() { if contact.isGroup { self.ownRole = DataLayer.sharedInstance().getOwnRole(inGroupOrChannel: contact.obj) ?? "none" self.ownAffiliation = DataLayer.sharedInstance().getOwnAffiliation(inGroupOrChannel:contact.obj) ?? "none" } else { - self.ownRole = "none" + self.ownRole = "participant" self.ownAffiliation = "none" } } - + private func errorAlert(title: Text, message: Text = Text("")) { alertPrompt.title = title alertPrompt.message = message @@ -323,7 +325,7 @@ struct ContactDetails: View { Text("Group Members") } } else if contact.obj.isGroup && contact.obj.mucType == "channel" { - if ["owner", "admin"].contains(DataLayer.sharedInstance().getOwnAffiliation(inGroupOrChannel:contact.obj) ?? "none") { + if ["owner", "admin"].contains(ownAffiliation) { NavigationLink(destination: LazyClosureView(MemberList(mucContact:contact))) { Text("Channel Members") } @@ -567,6 +569,17 @@ struct ContactDetails: View { .onChange(of:contact.avatar as UIImage) { _ in hideLoadingOverlay(overlay) } + .onAppear { + self.updateRoleAndAffiliation() + } + .onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("kMonalMucParticipantsAndMembersUpdated")).receive(on: RunLoop.main)) { notification in + if let xmppAccount = notification.object as? xmpp, let notificationContact = notification.userInfo?["contact"] as? MLContact { + DDLogVerbose("Got muc participants/members update from account \(xmppAccount)...") + if notificationContact == contact { + self.updateRoleAndAffiliation() + } + } + } } } From 5d17bdede5470e5851bbb4a222d496fe08ac2f8c Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Fri, 10 May 2024 15:08:30 +0200 Subject: [PATCH 032/143] Fix group avatar image picker on macos --- Monal/Classes/BackgroundSettings.swift | 4 ++-- Monal/Classes/ContactDetails.swift | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/Monal/Classes/BackgroundSettings.swift b/Monal/Classes/BackgroundSettings.swift index 12b45d6081..890d3a2af0 100644 --- a/Monal/Classes/BackgroundSettings.swift +++ b/Monal/Classes/BackgroundSettings.swift @@ -2,7 +2,7 @@ // BackgroundSettings.swift // Monal // -// Created by admin on 14.11.22. +// Created by Thilo Molitor on 14.11.22. // Copyright © 2022 monal-im.org. All rights reserved. // @@ -83,7 +83,7 @@ struct BackgroundSettings: View { ImagePicker(image:$inputImage) } - //>= ios16 + //>= ios 16 /* PhotosPicker(selection:$selectedItem, matching:.images, photoLibrary:.shared()) { if let inputImage = inputImage { diff --git a/Monal/Classes/ContactDetails.swift b/Monal/Classes/ContactDetails.swift index f44e31aae0..e548fd9f88 100644 --- a/Monal/Classes/ContactDetails.swift +++ b/Monal/Classes/ContactDetails.swift @@ -72,7 +72,24 @@ struct ContactDetails: View { if ownAffiliation == "owner" { view.accessibilityLabel((contact.mucType == "group") ? "Change Group Avatar" : "Change Channel Avatar") .onTapGesture { +#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 } } else { view.accessibilityLabel((contact.mucType == "group") ? "Group Avatar" : "Channel Avatar") From 32217fb9550deb0c807db02a061d0362200dea6d Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Sun, 19 May 2024 20:58:50 +0200 Subject: [PATCH 033/143] Make channel members list dynamic, too --- Monal/Classes/ChannelMemberList.swift | 56 +++++++++++++++++---------- 1 file changed, 36 insertions(+), 20 deletions(-) diff --git a/Monal/Classes/ChannelMemberList.swift b/Monal/Classes/ChannelMemberList.swift index 3258d56e22..7a5252c7de 100644 --- a/Monal/Classes/ChannelMemberList.swift +++ b/Monal/Classes/ChannelMemberList.swift @@ -9,45 +9,61 @@ import OrderedCollections struct ChannelMemberList: View { - @State private var channelParticipants: OrderedDictionary - @StateObject var channel: ObservableKVOWrapper private let account: xmpp + @State private var ownAffiliation: String; + @StateObject var channel: ObservableKVOWrapper + @State private var participants: OrderedDictionary init(mucContact: ObservableKVOWrapper) { - self.account = MLXMPPManager.sharedInstance().getConnectedAccount(forID: mucContact.accountId)! as xmpp - _channel = StateObject(wrappedValue: mucContact) - - let jidList = Array(DataLayer.sharedInstance().getMembersAndParticipants(ofMuc: mucContact.contactJid, forAccountId: mucContact.accountId)) - var nickSet : OrderedDictionary = OrderedDictionary() - for jidDict in jidList { - if let nick = jidDict["room_nick"] as? String { - nickSet.updateValue((jidDict["affiliation"] as? String) ?? "none", forKey:nick) + account = MLXMPPManager.sharedInstance().getConnectedAccount(forID: mucContact.accountId)! as xmpp + _channel = StateObject(wrappedValue:mucContact) + _ownAffiliation = State(wrappedValue:"none") + _participants = State(wrappedValue:OrderedDictionary()) + } + + func updateParticipantList() { + ownAffiliation = DataLayer.sharedInstance().getOwnAffiliation(inGroupOrChannel:channel.obj) ?? "none" + participants.removeAll(keepingCapacity:true) + for memberInfo in Array(DataLayer.sharedInstance().getMembersAndParticipants(ofMuc:channel.contactJid, forAccountId:account.accountNo)) { + //ignore ourselves + if let jid = memberInfo["participant_jid"] as? String ?? memberInfo["member_jid"] as? String { + if jid == account.connectionProperties.identity.jid { + continue + } + } + if let nick = memberInfo["room_nick"] as? String { + participants[nick] = memberInfo["affiliation"] as? String ?? "none" } } - _channelParticipants = State(wrappedValue: nickSet) } + var body: some View { List { - Section(header: Text(self.channel.obj.contactDisplayName)) { - ForEach(self.channelParticipants.sorted(by: <), id: \.self.key) { participant in + Section(header: Text("\(self.channel.contactDisplayName as String) (affiliation: \(mucAffiliationToString(ownAffiliation)))")) { + ForEach(participants.sorted(by: <), id: \.self.key) { participant in ZStack(alignment: .topLeading) { HStack(alignment: .center) { Text(participant.key) Spacer() - if participant.value == "owner" { - Text(NSLocalizedString("Owner", comment: "")) - } else if participant.value == "admin" { - Text(NSLocalizedString("Admin", comment: "")) - } else { - Text(NSLocalizedString("Participant", comment: "")) - } + Text(mucAffiliationToString(participant.value)) } } } } } .navigationBarTitle(NSLocalizedString("Channel Participants", comment: ""), displayMode: .inline) + .onAppear { + updateParticipantList() + } + .onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("kMonalMucParticipantsAndMembersUpdated")).receive(on: RunLoop.main)) { notification in + if let xmppAccount = notification.object as? xmpp, let contact = notification.userInfo?["contact"] as? MLContact { + DDLogVerbose("Got muc participants/members update from account \(xmppAccount)...") + if contact == channel { + updateParticipantList() + } + } + } } } From e06c32784ae9dca69e68605e451690778921b990 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Sun, 19 May 2024 20:59:27 +0200 Subject: [PATCH 034/143] Make sure to update muc contact on delete/remove/ban etc. --- Monal/Classes/MLMucProcessor.m | 41 ++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/Monal/Classes/MLMucProcessor.m b/Monal/Classes/MLMucProcessor.m index 7a95d311d5..4d57520872 100644 --- a/Monal/Classes/MLMucProcessor.m +++ b/Monal/Classes/MLMucProcessor.m @@ -346,9 +346,15 @@ -(void) processPresence:(XMPPPresence*) presenceNode //handle participant updates if([presenceNode check:@"/"] || item[@"affiliation"] == nil) + { + DDLogVerbose(@"Removing participant from muc(%@): %@", presenceNode.fromUser, item); [[DataLayer sharedInstance] removeParticipant:item fromMuc:presenceNode.fromUser forAccountId:_account.accountNo]; + } else + { + DDLogVerbose(@"Adding participant from muc(%@): %@", presenceNode.fromUser, item); [[DataLayer sharedInstance] addParticipant:item toMuc:presenceNode.fromUser forAccountId:_account.accountNo]; + } //handle members updates (publishing the changes in members/participants is already handled by handleMembersListUpdate //--> only publish if we don't call handleMembersListUpdate @@ -629,6 +635,11 @@ -(void) handleStatusCodes:(XMPPStanza*) node //update nick in database DDLogInfo(@"Updating muc %@ nick in database to nick provided by server: '%@'...", node.fromUser, node.fromResource); [[DataLayer sharedInstance] updateOwnNickName:node.fromResource forMuc:node.fromUser forAccount:_account.accountNo]; + + DDLogDebug(@"Updating muc contact..."); + [[MLNotificationQueue currentQueue] postNotificationName:kMonalContactRefresh object:_account userInfo:@{ + @"contact": [MLContact createContactFromJid:node.fromUser andAccountNo:_account.accountNo] + }]; } break; } @@ -643,6 +654,11 @@ -(void) handleStatusCodes:(XMPPStanza*) node [self deleteMuc:node.fromUser withBookmarksUpdate:YES keepBuddylistEntry:NO]; selfPrecenceHandled = YES; [self handleError:[NSString stringWithFormat:NSLocalizedString(@"You got banned from group/channel: %@", @""), node.fromUser] forMuc:node.fromUser withNode:node andIsSevere:YES]; + + DDLogDebug(@"Updating muc contact..."); + [[MLNotificationQueue currentQueue] postNotificationName:kMonalContactRefresh object:_account userInfo:@{ + @"contact": [MLContact createContactFromJid:node.fromUser andAccountNo:_account.accountNo] + }]; } break; } @@ -663,6 +679,11 @@ -(void) handleStatusCodes:(XMPPStanza*) node [self deleteMuc:node.fromUser withBookmarksUpdate:YES keepBuddylistEntry:NO]; selfPrecenceHandled = YES; [self handleError:[NSString stringWithFormat:NSLocalizedString(@"You got kicked from group/channel: %@", @""), node.fromUser] forMuc:node.fromUser withNode:node andIsSevere:YES]; + + DDLogDebug(@"Updating muc contact..."); + [[MLNotificationQueue currentQueue] postNotificationName:kMonalContactRefresh object:_account userInfo:@{ + @"contact": [MLContact createContactFromJid:node.fromUser andAccountNo:_account.accountNo] + }]; } } else @@ -687,6 +708,11 @@ -(void) handleStatusCodes:(XMPPStanza*) node [self deleteMuc:node.fromUser withBookmarksUpdate:YES keepBuddylistEntry:YES]; selfPrecenceHandled = YES; [self handleError:[NSString stringWithFormat:NSLocalizedString(@"You got removed from group/channel: %@", @""), node.fromUser] forMuc:node.fromUser withNode:node andIsSevere:YES]; + + DDLogDebug(@"Updating muc contact..."); + [[MLNotificationQueue currentQueue] postNotificationName:kMonalContactRefresh object:_account userInfo:@{ + @"contact": [MLContact createContactFromJid:node.fromUser andAccountNo:_account.accountNo] + }]; } } } @@ -703,6 +729,11 @@ -(void) handleStatusCodes:(XMPPStanza*) node [self handleError:[NSString stringWithFormat:NSLocalizedString(@"Kicked, because group/channel is now members-only: %@", @""), node.fromUser] forMuc:node.fromUser withNode:node andIsSevere:YES]; [self deleteMuc:node.fromUser withBookmarksUpdate:YES keepBuddylistEntry:YES]; selfPrecenceHandled = YES; + + DDLogDebug(@"Updating muc contact..."); + [[MLNotificationQueue currentQueue] postNotificationName:kMonalContactRefresh object:_account userInfo:@{ + @"contact": [MLContact createContactFromJid:node.fromUser andAccountNo:_account.accountNo] + }]; } break; } @@ -716,6 +747,11 @@ -(void) handleStatusCodes:(XMPPStanza*) node [self removeRoomFromJoining:node.fromUser]; selfPrecenceHandled = YES; [self handleError:[NSString stringWithFormat:NSLocalizedString(@"Kicked from group/channel, because of system shutdown: %@", @""), node.fromUser] forMuc:node.fromUser withNode:node andIsSevere:YES]; + + DDLogDebug(@"Updating muc contact..."); + [[MLNotificationQueue currentQueue] postNotificationName:kMonalContactRefresh object:_account userInfo:@{ + @"contact": [MLContact createContactFromJid:node.fromUser andAccountNo:_account.accountNo] + }]; } break; } @@ -786,6 +822,11 @@ -(void) handleStatusCodes:(XMPPStanza*) node { [self handleError:[NSString stringWithFormat:NSLocalizedString(@"Group/Channel got destroyed: %@", @""), node.fromUser] forMuc:node.fromUser withNode:node andIsSevere:YES]; [self deleteMuc:node.fromUser withBookmarksUpdate:YES keepBuddylistEntry:YES]; + + DDLogDebug(@"Updating muc contact..."); + [[MLNotificationQueue currentQueue] postNotificationName:kMonalContactRefresh object:_account userInfo:@{ + @"contact": [MLContact createContactFromJid:node.fromUser andAccountNo:_account.accountNo] + }]; } } } From 27e7e16be40ec040c05eb96e08aa3e22d90cc214 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Sat, 11 May 2024 01:57:01 +0200 Subject: [PATCH 035/143] Improve group/channel admin mode even further --- Monal/Classes/ContactDetails.swift | 4 +- Monal/Classes/MemberList.swift | 148 +++++++++++++++++------------ Monal/Classes/SwiftuiHelpers.swift | 21 ++++ 3 files changed, 108 insertions(+), 65 deletions(-) diff --git a/Monal/Classes/ContactDetails.swift b/Monal/Classes/ContactDetails.swift index e548fd9f88..96f7194437 100644 --- a/Monal/Classes/ContactDetails.swift +++ b/Monal/Classes/ContactDetails.swift @@ -344,11 +344,11 @@ struct ContactDetails: View { } else if contact.obj.isGroup && contact.obj.mucType == "channel" { if ["owner", "admin"].contains(ownAffiliation) { NavigationLink(destination: LazyClosureView(MemberList(mucContact:contact))) { - Text("Channel Members") + Text("Channel Participants") } } else { NavigationLink(destination: LazyClosureView(ChannelMemberList(mucContact:contact))) { - Text("Channel Members") + Text("Channel Participants") } } } diff --git a/Monal/Classes/MemberList.swift b/Monal/Classes/MemberList.swift index ac273d308c..28ec53c2f3 100644 --- a/Monal/Classes/MemberList.swift +++ b/Monal/Classes/MemberList.swift @@ -17,9 +17,10 @@ struct ActionSheetPrompt { struct MemberList: View { private let account: xmpp @State private var ownAffiliation: String; - @StateObject var group: ObservableKVOWrapper + @StateObject var muc: ObservableKVOWrapper @State private var memberList: OrderedSet> @State private var affiliations: Dictionary, String> + @State private var online: Dictionary, Bool> @State private var navigationActive: ObservableKVOWrapper? @State private var showAlert = false @State private var alertPrompt = AlertPrompt(dismissLabel: Text("Close")) @@ -29,37 +30,44 @@ struct MemberList: View { init(mucContact: ObservableKVOWrapper) { account = MLXMPPManager.sharedInstance().getConnectedAccount(forID: mucContact.accountId)! as xmpp - _group = StateObject(wrappedValue:mucContact) + _muc = StateObject(wrappedValue:mucContact) _ownAffiliation = State(wrappedValue:"none") _memberList = State(wrappedValue:OrderedSet>()) _affiliations = State(wrappedValue:[:]) + _online = State(wrappedValue:[:]) } func updateMemberlist() { - memberList = getContactList(viewContact:group) - ownAffiliation = DataLayer.sharedInstance().getOwnAffiliation(inGroupOrChannel:group.obj) ?? "none" - var affiliationTmp = Dictionary, String>() - for memberInfo in Array(DataLayer.sharedInstance().getMembersAndParticipants(ofMuc:group.contactJid, forAccountId:account.accountNo)) { + memberList = getContactList(viewContact:self.muc) + ownAffiliation = DataLayer.sharedInstance().getOwnAffiliation(inGroupOrChannel:self.muc.obj) ?? "none" + affiliations.removeAll(keepingCapacity:true) + online.removeAll(keepingCapacity:true) + for memberInfo in Array(DataLayer.sharedInstance().getMembersAndParticipants(ofMuc:self.muc.contactJid, forAccountId:account.accountNo)) { + DDLogVerbose("Got member/participant entry: \(String(describing:memberInfo))") guard let jid = memberInfo["participant_jid"] as? String ?? memberInfo["member_jid"] as? String else { continue } let contact = ObservableKVOWrapper(MLContact.createContact(fromJid:jid, andAccountNo:account.accountNo)) - affiliationTmp[contact] = memberInfo["affiliation"] as? String ?? "none" + affiliations[contact] = memberInfo["affiliation"] as? String ?? "none" + if let num = memberInfo["online"] as? NSNumber { + online[contact] = num.boolValue + } else { + online[contact] = false + } } - affiliations = affiliationTmp } func performAction(_ title: Text, action: @escaping ()->Void) { - action() self.account.mucProcessor.addUIHandler({_data in let data = _data as! NSDictionary DispatchQueue.main.async { hideLoadingOverlay(overlay) let success : Bool = data["success"] as! Bool; if !success { - showAlert(title: title, description: Text(data["errorMessage"] as! String)) + showAlert(title: title, description: Text(data["errorMessage"] as? String ?? "Unknown error!")) } } - }, forMuc:group.contactJid) + }, forMuc:self.muc.contactJid) + action() } func showAlert(title: Text, description: Text) { @@ -77,7 +85,7 @@ struct MemberList: View { func ownUserHasAffiliationToRemove(contact: ObservableKVOWrapper) -> Bool { //we don't want to set affiliation=none in channels using deletion swipe (this does not delete the user) - if group.mucType == "channel" { + if self.muc.mucType == "channel" { return false } if contact.contactJid == account.connectionProperties.identity.jid { @@ -94,74 +102,77 @@ struct MemberList: View { } func actionsAllowed(for contact:ObservableKVOWrapper) -> [String] { - if group.mucType == "group" { - if let contactAffiliation = affiliations[contact] { + if let contactAffiliation = affiliations[contact], let contactOnline = online[contact] { + var reinviteEntry: [String] = [] + if !contactOnline { + reinviteEntry = ["reinvite"] + } + if self.muc.mucType == "group" { if ownAffiliation == "owner" { - return ["profile", "owner", "admin", "member", "outcast"] + return ["profile"] + reinviteEntry + ["owner", "admin", "member", "outcast"] } else { //only admin left, because other affiliations don't call actionsAllowed at all if ["member", "outcast"].contains(contactAffiliation) { - return ["profile", "member", "outcast"] + return ["profile"] + reinviteEntry + ["member", "outcast"] } else { + //if this contact is a co-admin or owner, we aren't allowed to do much to their affiliation //return contact affiliation because that should be displayed as selected in picker - return ["profile", contactAffiliation] + return ["profile"] + reinviteEntry + [contactAffiliation] } } - } - return ["profile"] - } else { - if let contactAffiliation = affiliations[contact] { + } else { if ownAffiliation == "owner" { - return ["profile", "owner", "admin", "member", "none", "outcast"] + return ["profile"] + reinviteEntry + ["owner", "admin", "member", "none", "outcast"] } else { //only admin left, because other affiliations don't call actionsAllowed at all if ["member", "none", "outcast"].contains(contactAffiliation) { - return ["profile", "member", "none", "outcast"] + return ["profile"] + reinviteEntry + ["member", "none", "outcast"] } else { + //if this contact is a co-admin or owner, we aren't allowed to do much to their affiliation //return contact affiliation because that should be displayed as selected in picker - return ["profile", contactAffiliation] + return ["profile"] + reinviteEntry + [contactAffiliation] } } } - return ["profile", "none"] } - } - - func affiliationToString(_ affiliation: String?) -> String { - if let affiliation = affiliation { - if affiliation == "owner" { - return NSLocalizedString("Owner", comment:"muc affiliation") - } else if affiliation == "admin" { - return NSLocalizedString("Admin", comment:"muc affiliation") - } else if affiliation == "member" { - return NSLocalizedString("Member", comment:"muc affiliation") - } else if affiliation == "none" { - return NSLocalizedString("Participant", comment:"muc affiliation") - } else if affiliation == "outcast" { - return NSLocalizedString("Blocked", comment:"muc affiliation") - } else if affiliation == "profile" { - return NSLocalizedString("Open contact details", comment:"") - } + //fallback (should hopefully never be needed) + DDLogWarn("Fallback for group/channel \(String(describing:self.muc.contactJid as String)): affiliation=\(String(describing:affiliations[contact])), online=\(String(describing:online[contact]))") + if self.muc.mucType == "group" { + return ["profile"] + } else { + return ["profile", "reinvite", "none"] } - return NSLocalizedString("", comment:"muc affiliation") } var body: some View { List { - Section(header: Text("\(self.group.contactDisplayName as String) (affiliation: \(affiliationToString(ownAffiliation)))")) { + Section(header: Text("\(self.muc.contactDisplayName as String) (affiliation: \(mucAffiliationToString(ownAffiliation)))")) { if ownAffiliation == "owner" || ownAffiliation == "admin" { NavigationLink(destination: LazyClosureView(ContactPicker(account, initializeFrom: memberList, allowRemoval: false) { newMemberList in for member in newMemberList { if !memberList.contains(member) { - showLoadingOverlay(overlay, headlineView: Text("Adding new member"), descriptionView: Text("Adding \(member.contactJid as String)...")) - performAction(Text("Error adding new member!")) { - DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 1.0) { - account.mucProcessor.setAffiliation("member", ofUser:member.contactJid, inMuc:self.group.contactJid) - account.mucProcessor.inviteUser(member.contactJid, inMuc: self.group.contactJid) + if self.muc.mucType == "group" { + showLoadingOverlay(overlay, headlineView: Text("Adding new member"), descriptionView: Text("Adding \(member.contactJid as String)...")) + performAction(Text("Error adding new member!")) { + DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 1.0) { + account.mucProcessor.setAffiliation("member", ofUser:member.contactJid, inMuc:self.muc.contactJid) + account.mucProcessor.inviteUser(member.contactJid, inMuc: self.muc.contactJid) + } + } + } else { + showLoadingOverlay(overlay, headlineView: Text("Inviting new participant"), descriptionView: Text("Adding \(member.contactJid as String)...")) + performAction(Text("Error adding new participant!")) { + DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 1.0) { + account.mucProcessor.inviteUser(member.contactJid, inMuc: self.muc.contactJid) + } } } } } })) { - Text("Add Group Members") + if self.muc.mucType == "group" { + Text("Add members to group") + } else { + Text("Invite participants to channel") + } } ForEach(memberList, id:\.self) { contact in @@ -178,30 +189,41 @@ struct MemberList: View { if newAffiliation == "profile" { DDLogVerbose("Activating navigation to \(String(describing:contact))") navigationActive = contact + } else if newAffiliation == "reinvite" { + showLoadingOverlay(overlay, headlineView: Text("Inviting user"), descriptionView: Text("Inviting user to this group/channel: \(contact.contactJid as String)")) + performAction(Text("Error inviting user!")) { + DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 1.0) { + //first remove potential ban, then reinvite + if affiliations[contact] == "outcast" { + account.mucProcessor.setAffiliation(self.muc.mucType == "group" ? "member" : "none", ofUser:contact.contactJid, inMuc:self.muc.contactJid) + } + account.mucProcessor.inviteUser(contact.contactJid, inMuc: self.muc.contactJid) + } + } } else if newAffiliation == "outcast" { showActionSheet(title: Text("Block user?"), description: Text("Do you want to block this user from entering this group/channel?")) { DDLogVerbose("Changing affiliation of \(String(describing:contact)) to: \(String(describing:newAffiliation))...") - showLoadingOverlay(overlay, headlineView: Text("Blocking member"), descriptionView: Text("Blocking \(contact.contactJid as String)...")) + showLoadingOverlay(overlay, headlineView: Text("Blocking member"), descriptionView: Text("Blocking \(contact.contactJid as String)")) performAction(Text("Error blocking user!")) { DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 1.0) { - account.mucProcessor.setAffiliation(newAffiliation, ofUser:contact.contactJid, inMuc:self.group.contactJid) + account.mucProcessor.setAffiliation(newAffiliation, ofUser:contact.contactJid, inMuc:self.muc.contactJid) } } } } else { DDLogVerbose("Changing affiliation of \(String(describing:contact)) to: \(String(describing:newAffiliation))...") showLoadingOverlay(overlay, headlineView: Text("Changing affiliation of member"), descriptionView: - Text("Changing \(contact.contactJid as String) to ") + Text(affiliationToString(newAffiliation)) + Text("...")) + Text("Changing \(contact.contactJid as String) to ") + Text(mucAffiliationToString(newAffiliation))) performAction(Text("Error changing affiliation!")) { DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 1.0) { - account.mucProcessor.setAffiliation(newAffiliation, ofUser:contact.contactJid, inMuc:self.group.contactJid) + account.mucProcessor.setAffiliation(newAffiliation, ofUser:contact.contactJid, inMuc:self.muc.contactJid) } } } } ), label: EmptyView()) { ForEach(actionsAllowed(for:contact), id:\.self) { affiliation in - Text(affiliationToString(affiliation)).tag(affiliation) + Text(mucAffiliationToString(affiliation)).tag(affiliation) } } .pickerStyle(.menu) @@ -218,11 +240,11 @@ struct MemberList: View { } .onDelete(perform: { memberIdx in let member = memberList[memberIdx.first!] - showActionSheet(title: Text("Remove user?"), description: self.group.mucType == "group" ? Text("Do you want to remove this user from this group? The user won't be able to enter it again until added back to the group.") : Text("Do you want to remove this user from this channel? The user will be able to enter it again.")) { + showActionSheet(title: Text("Remove user?"), description: self.muc.mucType == "group" ? Text("Do you want to remove this user from this group? The user won't be able to enter it again until added back to the group.") : Text("Do you want to remove this user from this channel? The user will be able to enter it again.")) { showLoadingOverlay(overlay, headlineView: Text("Removing member"), descriptionView: Text("Removing \(member.contactJid as String)...")) performAction(Text("Error removing user!")) { DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 1.0) { - account.mucProcessor.setAffiliation("none", ofUser: member.contactJid, inMuc: self.group.contactJid) + account.mucProcessor.setAffiliation("none", ofUser: member.contactJid, inMuc: self.muc.contactJid) } } } @@ -234,7 +256,7 @@ struct MemberList: View { HStack(alignment: .center) { ContactEntry(contact:contact) Spacer() - Text(affiliationToString(affiliations[contact])) + Text(mucAffiliationToString(affiliations[contact])) } } .deleteDisabled(true) @@ -243,10 +265,6 @@ struct MemberList: View { } } } - .addLoadingOverlay(overlay) - .alert(isPresented: $showAlert, content: { - Alert(title: alertPrompt.title, message: alertPrompt.message, dismissButton: .default(alertPrompt.dismissLabel)) - }) .actionSheet(isPresented: $showActionSheet) { ActionSheet( title: actionSheetPrompt.title, @@ -260,6 +278,10 @@ struct MemberList: View { ] ) } + .alert(isPresented: $showAlert, content: { + Alert(title: alertPrompt.title, message: alertPrompt.message, dismissButton: .default(alertPrompt.dismissLabel)) + }) + .addLoadingOverlay(overlay) .navigationBarTitle("Group Members", displayMode: .inline) .onAppear { updateMemberlist() @@ -267,7 +289,7 @@ struct MemberList: View { .onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("kMonalMucParticipantsAndMembersUpdated")).receive(on: RunLoop.main)) { notification in if let xmppAccount = notification.object as? xmpp, let contact = notification.userInfo?["contact"] as? MLContact { DDLogVerbose("Got muc participants/members update from account \(xmppAccount)...") - if contact == group { + if contact == self.muc { updateMemberlist() hideLoadingOverlay(overlay) } diff --git a/Monal/Classes/SwiftuiHelpers.swift b/Monal/Classes/SwiftuiHelpers.swift index f4539a6ad1..4596d2f35a 100644 --- a/Monal/Classes/SwiftuiHelpers.swift +++ b/Monal/Classes/SwiftuiHelpers.swift @@ -88,6 +88,27 @@ func getContactList(viewContact: (ObservableKVOWrapper?)) -> OrderedS } } +func mucAffiliationToString(_ affiliation: String?) -> String { + if let affiliation = affiliation { + if affiliation == "owner" { + return NSLocalizedString("Owner", comment:"muc affiliation") + } else if affiliation == "admin" { + return NSLocalizedString("Admin", comment:"muc affiliation") + } else if affiliation == "member" { + return NSLocalizedString("Member", comment:"muc affiliation") + } else if affiliation == "none" { + return NSLocalizedString("Participant", comment:"muc affiliation") + } else if affiliation == "outcast" { + return NSLocalizedString("Blocked", comment:"muc affiliation") + } else if affiliation == "profile" { + return NSLocalizedString("Open contact details", comment:"muc members list") + } else if affiliation == "reinvite" { + return NSLocalizedString("Invite again", comment:"muc invite") + } + } + return NSLocalizedString("", comment:"muc affiliation") +} + //see here for some ideas used herein: https://blog.logrocket.com/adding-gifs-ios-app-flanimatedimage-swiftui/#using-flanimatedimage-with-swift struct GIFViewer: UIViewRepresentable { typealias UIViewType = FLAnimatedImageView From 2d229b819ee2fc23e7115cb7d310aa7644ced3f1 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Sun, 19 May 2024 23:16:49 +0200 Subject: [PATCH 036/143] Bring back upload hud when sharing through sharesheet --- Monal/Classes/MonalAppDelegate.m | 126 ++++++++++++++++--------------- 1 file changed, 64 insertions(+), 62 deletions(-) diff --git a/Monal/Classes/MonalAppDelegate.m b/Monal/Classes/MonalAppDelegate.m index e07a8c237f..f640f138fd 100644 --- a/Monal/Classes/MonalAppDelegate.m +++ b/Monal/Classes/MonalAppDelegate.m @@ -1949,7 +1949,6 @@ -(void) sendAllOutboxes } //open the destination chat only once - BOOL alreadyOpen = NO; for(NSDictionary* payload in [[DataLayer sharedInstance] getShareSheetPayload]) { DDLogInfo(@"Sending outbox entry: %@", payload); @@ -1965,16 +1964,6 @@ -(void) sendAllOutboxes } MLContact* contact = [MLContact createContactFromJid:payload[@"recipient"] andAccountNo:account.accountNo]; - DDLogVerbose(@"Trying to open chat of outbox receiver: %@", contact); - [[DataLayer sharedInstance] addActiveBuddies:contact.contactJid forAccount:contact.accountId]; - //don't use [self openChatOfContact:withCompletion:] because it's asynchronous and can only handle one contact at a time (e.g. until the asynchronous execution finished) - //we can invoke the activeChats interface directly instead, because we already did the necessary preparations ourselves - if(!alreadyOpen) - { - [(ActiveChatsViewController*)self.activeChats presentChatWithContact:contact]; - alreadyOpen = YES; - } - monal_id_block_t cleanup = ^(NSDictionary* payload) { [[DataLayer sharedInstance] deleteShareSheetPayloadWithId:payload[@"id"]]; [[MLNotificationQueue currentQueue] postNotificationName:kMonalRefresh object:nil userInfo:nil]; @@ -1983,59 +1972,72 @@ -(void) sendAllOutboxes [self.activeChats.currentChatViewController scrollToBottom]; [self.activeChats.currentChatViewController hideUploadHUD]; } + //send next item (if there is one left) + [self sendAllOutboxes]; }; - BOOL encrypted = [[DataLayer sharedInstance] shouldEncryptForJid:contact.contactJid andAccountNo:contact.accountId]; - if([payload[@"type"] isEqualToString:@"text"]) - { - [[MLXMPPManager sharedInstance] sendMessageAndAddToHistory:payload[@"data"] havingType:kMessageTypeText toContact:contact isEncrypted:encrypted uploadInfo:nil withCompletionHandler:^(BOOL successSendObject, NSString* messageIdSentObject) { - DDLogInfo(@"SHARESHEET_SEND_DATA success=%@, account=%@, messageIdSentObject=%@", bool2str(successSendObject), account.accountNo, messageIdSentObject); - cleanup(payload); - }]; - } - else if([payload[@"type"] isEqualToString:@"url"]) - { - [[MLXMPPManager sharedInstance] sendMessageAndAddToHistory:payload[@"data"] havingType:kMessageTypeUrl toContact:contact isEncrypted:encrypted uploadInfo:nil withCompletionHandler:^(BOOL successSendObject, NSString* messageIdSentObject) { - DDLogInfo(@"SHARESHEET_SEND_DATA success=%@, account=%@, messageIdSentObject=%@", bool2str(successSendObject), account.accountNo, messageIdSentObject); - cleanup(payload); - }]; - } - else if([payload[@"type"] isEqualToString:@"geo"]) - { - [[MLXMPPManager sharedInstance] sendMessageAndAddToHistory:payload[@"data"] havingType:kMessageTypeGeo toContact:contact isEncrypted:encrypted uploadInfo:nil withCompletionHandler:^(BOOL successSendObject, NSString* messageIdSentObject) { - DDLogInfo(@"SHARESHEET_SEND_DATA success=%@, account=%@, messageIdSentObject=%@", bool2str(successSendObject), account.accountNo, messageIdSentObject); - cleanup(payload); - }]; - } - else if([payload[@"type"] isEqualToString:@"image"] || [payload[@"type"] isEqualToString:@"file"] || [payload[@"type"] isEqualToString:@"contact"] || [payload[@"type"] isEqualToString:@"audiovisual"]) - { - DDLogInfo(@"Got %@ upload: %@", payload[@"type"], payload[@"data"]); - [self.activeChats.currentChatViewController showUploadHUD]; - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - $call(payload[@"data"], $ID(account), $BOOL(encrypted), $ID(completion, (^(NSString* url, NSString* mimeType, NSNumber* size, NSError* error) { - dispatch_async(dispatch_get_main_queue(), ^{ - if(error != nil) - { - DDLogError(@"Failed to upload outbox file: %@", error); - NSMutableDictionary* payloadCopy = [NSMutableDictionary dictionaryWithDictionary:payload]; - cleanup(payloadCopy); - - UIAlertController* messageAlert = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"Failed to share file", @"") message:[NSString stringWithFormat:NSLocalizedString(@"Error: %@", @""), error] preferredStyle:UIAlertControllerStyleAlert]; - [messageAlert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Close", @"") style:UIAlertActionStyleCancel handler:^(UIAlertAction* action __unused) { - }]]; - [self.activeChats presentViewController:messageAlert animated:YES completion:nil]; - } - else - [[MLXMPPManager sharedInstance] sendMessageAndAddToHistory:url havingType:kMessageTypeFiletransfer toContact:contact isEncrypted:encrypted uploadInfo:@{@"mimeType": mimeType, @"size": size} withCompletionHandler:^(BOOL successSendObject, NSString* messageIdSentObject) { - DDLogInfo(@"SHARESHEET_SEND_DATA success=%@, account=%@, messageIdSentObject=%@", bool2str(successSendObject), account.accountNo, messageIdSentObject); - cleanup(payload); - }]; - }); - }))); - }); - } - else - unreachable(@"Outbox payload type unknown", payload); + monal_id_block_t sendItem = ^(id dummy __unused){ + BOOL encrypted = [[DataLayer sharedInstance] shouldEncryptForJid:contact.contactJid andAccountNo:contact.accountId]; + if([payload[@"type"] isEqualToString:@"text"]) + { + [[MLXMPPManager sharedInstance] sendMessageAndAddToHistory:payload[@"data"] havingType:kMessageTypeText toContact:contact isEncrypted:encrypted uploadInfo:nil withCompletionHandler:^(BOOL successSendObject, NSString* messageIdSentObject) { + DDLogInfo(@"SHARESHEET_SEND_DATA success=%@, account=%@, messageIdSentObject=%@", bool2str(successSendObject), account.accountNo, messageIdSentObject); + cleanup(payload); + }]; + } + else if([payload[@"type"] isEqualToString:@"url"]) + { + [[MLXMPPManager sharedInstance] sendMessageAndAddToHistory:payload[@"data"] havingType:kMessageTypeUrl toContact:contact isEncrypted:encrypted uploadInfo:nil withCompletionHandler:^(BOOL successSendObject, NSString* messageIdSentObject) { + DDLogInfo(@"SHARESHEET_SEND_DATA success=%@, account=%@, messageIdSentObject=%@", bool2str(successSendObject), account.accountNo, messageIdSentObject); + cleanup(payload); + }]; + } + else if([payload[@"type"] isEqualToString:@"geo"]) + { + [[MLXMPPManager sharedInstance] sendMessageAndAddToHistory:payload[@"data"] havingType:kMessageTypeGeo toContact:contact isEncrypted:encrypted uploadInfo:nil withCompletionHandler:^(BOOL successSendObject, NSString* messageIdSentObject) { + DDLogInfo(@"SHARESHEET_SEND_DATA success=%@, account=%@, messageIdSentObject=%@", bool2str(successSendObject), account.accountNo, messageIdSentObject); + cleanup(payload); + }]; + } + else if([payload[@"type"] isEqualToString:@"image"] || [payload[@"type"] isEqualToString:@"file"] || [payload[@"type"] isEqualToString:@"contact"] || [payload[@"type"] isEqualToString:@"audiovisual"]) + { + DDLogInfo(@"Got %@ upload: %@", payload[@"type"], payload[@"data"]); + [self.activeChats.currentChatViewController showUploadHUD]; + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + $call(payload[@"data"], $ID(account), $BOOL(encrypted), $ID(completion, (^(NSString* url, NSString* mimeType, NSNumber* size, NSError* error) { + dispatch_async(dispatch_get_main_queue(), ^{ + if(error != nil) + { + DDLogError(@"Failed to upload outbox file: %@", error); + NSMutableDictionary* payloadCopy = [NSMutableDictionary dictionaryWithDictionary:payload]; + cleanup(payloadCopy); + + UIAlertController* messageAlert = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"Failed to share file", @"") message:[NSString stringWithFormat:NSLocalizedString(@"Error: %@", @""), error] preferredStyle:UIAlertControllerStyleAlert]; + [messageAlert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Close", @"") style:UIAlertActionStyleCancel handler:^(UIAlertAction* action __unused) { + }]]; + [self.activeChats presentViewController:messageAlert animated:YES completion:nil]; + } + else + [[MLXMPPManager sharedInstance] sendMessageAndAddToHistory:url havingType:kMessageTypeFiletransfer toContact:contact isEncrypted:encrypted uploadInfo:@{@"mimeType": mimeType, @"size": size} withCompletionHandler:^(BOOL successSendObject, NSString* messageIdSentObject) { + DDLogInfo(@"SHARESHEET_SEND_DATA success=%@, account=%@, messageIdSentObject=%@", bool2str(successSendObject), account.accountNo, messageIdSentObject); + cleanup(payload); + }]; + }); + }))); + }); + } + else + unreachable(@"Outbox payload type unknown", payload); + }; + + DDLogVerbose(@"Trying to open chat of outbox receiver: %@", contact); + [[DataLayer sharedInstance] addActiveBuddies:contact.contactJid forAccount:contact.accountId]; + //don't use [self openChatOfContact:withCompletion:] because it's asynchronous and can only handle one contact at a time (e.g. until the asynchronous execution finished) + //we can invoke the activeChats interface directly instead, because we already did the necessary preparations ourselves + [(ActiveChatsViewController*)self.activeChats presentChatWithContact:contact andCompletion:sendItem]; + + //only send one item at a time (this method will be invoked again when sending completed) + break; } } From 212dcbc511a8c01e76178dcadd20e6fd086991fd Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Mon, 20 May 2024 03:33:41 +0200 Subject: [PATCH 037/143] Fix all types of scrolling (initial, scroll down, keyboard open etc.) --- Monal/Classes/MLChatInputContainer.m | 6 ++- Monal/Classes/MonalAppDelegate.m | 2 +- Monal/Classes/chatViewController.h | 2 +- Monal/Classes/chatViewController.m | 78 +++++++++++++++------------- 4 files changed, 48 insertions(+), 40 deletions(-) diff --git a/Monal/Classes/MLChatInputContainer.m b/Monal/Classes/MLChatInputContainer.m index e28c329576..cb57367083 100644 --- a/Monal/Classes/MLChatInputContainer.m +++ b/Monal/Classes/MLChatInputContainer.m @@ -37,7 +37,11 @@ -(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event NSArray *subViews = self.subviews; for(UIView *subView in subViews) { if (CGRectContainsPoint(subView.frame, point) && subView.frame.origin.y < 0) { - [self.chatInputActionDelegate doScrollDownAction]; + DDLogDebug(@"ScrollDown button tapped..."); + //without async dispatch this would do nothing + dispatch_async(dispatch_get_main_queue(), ^{ + [self.chatInputActionDelegate doScrollDownAction]; + }); } } return [super pointInside:point withEvent:event]; diff --git a/Monal/Classes/MonalAppDelegate.m b/Monal/Classes/MonalAppDelegate.m index f640f138fd..2b6300eac1 100644 --- a/Monal/Classes/MonalAppDelegate.m +++ b/Monal/Classes/MonalAppDelegate.m @@ -1969,7 +1969,7 @@ -(void) sendAllOutboxes [[MLNotificationQueue currentQueue] postNotificationName:kMonalRefresh object:nil userInfo:nil]; if(self.activeChats.currentChatViewController != nil) { - [self.activeChats.currentChatViewController scrollToBottom]; + [self.activeChats.currentChatViewController scrollToBottomAnimated:NO]; [self.activeChats.currentChatViewController hideUploadHUD]; } //send next item (if there is one left) diff --git a/Monal/Classes/chatViewController.h b/Monal/Classes/chatViewController.h index 1fcf7b497e..f38ec1b1a6 100644 --- a/Monal/Classes/chatViewController.h +++ b/Monal/Classes/chatViewController.h @@ -84,6 +84,6 @@ -(void) showUploadHUD; -(void) hideUploadHUD; --(void) scrollToBottom; +-(void) scrollToBottomAnimated:(BOOL) animated; @end diff --git a/Monal/Classes/chatViewController.m b/Monal/Classes/chatViewController.m index 4474859b5f..fbde09d694 100644 --- a/Monal/Classes/chatViewController.m +++ b/Monal/Classes/chatViewController.m @@ -54,6 +54,7 @@ @interface chatViewController()* _localMLContactCache; BOOL _isRecording; + BOOL _isAtBottom; } @property (nonatomic, strong) NSDateFormatter* destinationDateFormat; @@ -83,9 +84,7 @@ @interface chatViewController()_localMLContactCache) { [self->_localMLContactCache removeAllObjects]; } [self refreshData]; [self reloadTable]; - }); + }]; } -(void) openCallScreen:(id) sender @@ -814,11 +813,12 @@ -(void) viewWillAppear:(BOOL)animated // Set correct chatInput height constraints [self setChatInputHeightConstraints:self.hardwareKeyboardPresent]; - [self scrollToBottom]; [self tempfreezeAutoloading]; [self.contact addObserver:self forKeyPath:@"isEncrypted" options:NSKeyValueObservingOptionNew context:nil]; + + [self scrollToBottomAnimated:NO]; } @@ -1697,7 +1697,7 @@ -(MLMessage* _Nullable) addMessageto:(NSString *)to withMessage:(nonnull NSStrin withRowAnimation:UITableViewRowAnimationNone]; } } completion:^(BOOL finished) { - [self scrollToBottom]; + [self scrollToBottomIfNeeded]; }]; }); @@ -1766,7 +1766,7 @@ -(void) handleNewMessage:(NSNotification *)notification } [self->_messageTable endUpdates]; - [self scrollToBottom]; + [self scrollToBottomIfNeeded]; [CATransaction commit]; if (self.searchController.isActive) @@ -1913,20 +1913,25 @@ -(void) handleFiletransferMessageUpdate:(NSNotification*) notification } } --(void) scrollToBottom +-(void) scrollToBottomIfNeeded { - if(self.messageList.count == 0) return; - dispatch_async(dispatch_get_main_queue(), ^{ + if(_isAtBottom) + [self scrollToBottomAnimated:YES]; +} + +-(void) scrollToBottomAnimated:(BOOL) animated +{ + if(self.messageList.count == 0) + return; + [HelperTools dispatchAsync:YES reentrantOnQueue:dispatch_get_main_queue() withBlock:^{ NSInteger bottom = [self.messageTable numberOfRowsInSection:messagesSection]; if(bottom > 0) { NSIndexPath* path1 = [NSIndexPath indexPathForRow:bottom-1 inSection:messagesSection]; - // if(![self.messageTable.indexPathsForVisibleRows containsObject:path1]) - { - [self.messageTable scrollToRowAtIndexPath:path1 atScrollPosition:UITableViewScrollPositionTop animated:YES]; - } + [self.messageTable scrollToRowAtIndexPath:path1 atScrollPosition:UITableViewScrollPositionTop animated:animated]; + self->_isAtBottom = YES; } - }); + }]; } #pragma mark - date time @@ -2686,23 +2691,18 @@ -(void) scrollViewDidScroll:(UIScrollView *)scrollView // Only load old msgs if the view appeared if(!self.viewDidAppear) return; - + // get current scroll position (y-axis) CGFloat curOffset = scrollView.contentOffset.y; - - if (self.lastOffset > curOffset) - { - [self.lastMsgButton setHidden:NO]; - } - CGFloat bottomLength = scrollView.frame.size.height + curOffset; - - if (scrollView.contentSize.height <= bottomLength) - { + _isAtBottom = scrollView.contentSize.height <= bottomLength; + + if(_isAtBottom) [self.lastMsgButton setHidden:YES]; - } - - self.lastOffset = curOffset; + else + [self.lastMsgButton setHidden:NO]; + + } -(void) loadOldMsgHistory @@ -2923,7 +2923,7 @@ -(void) commandFPressed:(UIKeyCommand*)keyCommand - (void)textViewDidBeginEditing:(UITextView *)textView { - [self scrollToBottom]; + //[self scrollToBottomAnimated:YES]; } - (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text @@ -3129,7 +3129,8 @@ - (void)keyboardWillDisappear:(NSNotification*) aNotification - (void)keyboardDidShow:(NSNotification*)aNotification { - //TODO grab animation info + static BOOL firstTime = YES; + //TODO grab animation info NSDictionary* info = [aNotification userInfo]; CGSize kbSize = [[info objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue].size; if(kbSize.height > 100) { //my inputbar +any other @@ -3139,10 +3140,12 @@ - (void)keyboardDidShow:(NSNotification*)aNotification self.messageTable.contentInset = contentInsets; self.messageTable.scrollIndicatorInsets = contentInsets; - // Only scroll to bottom of the message table if a chat is opened - // don't scroll down on other events like closing a image preview - if(self.viewDidAppear == NO) - [self scrollToBottom]; + //this will be automatically called once the whole chat view is loaded (even if not showing a keyboard) + //--> filter that first call to not scroll a few pixels on view open + //(the few pixels come from some margin/padding only applied after viewDidAppear was called) + if(!firstTime) + [self scrollToBottomIfNeeded]; + firstTime = NO; } - (void)keyboardDidHide:(NSNotification*)aNotification @@ -3157,6 +3160,7 @@ - (void)keyboardDidHide:(NSNotification*)aNotification - (void)keyboardWillShow:(NSNotification*)aNotification { + [self setChatInputHeightConstraints:NO]; //TODO grab animation info // UIEdgeInsets contentInsets = UIEdgeInsetsZero; From cc052b5529b73556e5d9c730a8a2c768e2eaa046 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Mon, 20 May 2024 16:50:48 +0200 Subject: [PATCH 038/143] Set thread name in getExtraRunloopWithIdentifier, too Before we only set the runloop name --- Monal/Classes/HelperTools.m | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Monal/Classes/HelperTools.m b/Monal/Classes/HelperTools.m index f63eb3b6ef..7ba6b4df0d 100644 --- a/Monal/Classes/HelperTools.m +++ b/Monal/Classes/HelperTools.m @@ -587,6 +587,8 @@ +(NSRunLoop*) getExtraRunloopWithIdentifier:(MLRunLoopIdentifier) identifier NSCondition* condition = [NSCondition new]; [condition lock]; dispatch_async(dispatch_queue_create_with_target(name, DISPATCH_QUEUE_SERIAL, dispatch_get_global_queue(priority, 0)), ^{ + //set thread name, too (not only runloop name) + [NSThread.currentThread setName:[NSString stringWithFormat:@"%s", name]]; //we don't need an @synchronized block around this because the @synchronized block of the outer thread //waits until we signal our condition (e.g. no other thread can race with us) NSRunLoop* localLoop = runloops[@(identifier)] = [NSRunLoop currentRunLoop]; From fe2d28d84db1abffc8f82f81f9dca76e44881bd4 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Mon, 20 May 2024 21:41:28 +0200 Subject: [PATCH 039/143] Fix scrolling for catalyst, too --- Monal/Classes/chatViewController.m | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Monal/Classes/chatViewController.m b/Monal/Classes/chatViewController.m index fbde09d694..8a2d94a618 100644 --- a/Monal/Classes/chatViewController.m +++ b/Monal/Classes/chatViewController.m @@ -1916,7 +1916,11 @@ -(void) handleFiletransferMessageUpdate:(NSNotification*) notification -(void) scrollToBottomIfNeeded { if(_isAtBottom) - [self scrollToBottomAnimated:YES]; + { + dispatch_async(dispatch_get_main_queue(), ^{ + [self scrollToBottomAnimated:YES]; + }); + } } -(void) scrollToBottomAnimated:(BOOL) animated @@ -3143,8 +3147,12 @@ - (void)keyboardDidShow:(NSNotification*)aNotification //this will be automatically called once the whole chat view is loaded (even if not showing a keyboard) //--> filter that first call to not scroll a few pixels on view open //(the few pixels come from some margin/padding only applied after viewDidAppear was called) +#if TARGET_OS_MACCATALYST + [self scrollToBottomIfNeeded]; +#else if(!firstTime) [self scrollToBottomIfNeeded]; +#endif firstTime = NO; } From 7ecf7a57bae81baa7345412922410d352552f2d5 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Mon, 20 May 2024 21:51:01 +0200 Subject: [PATCH 040/143] Make DNSSEC validation configurable (default: off) In alpha builds it will be default: on --- Monal/Classes/HelperTools.m | 3 ++- Monal/Classes/MLFiletransfer.m | 3 ++- Monal/Classes/MLHTTPRequest.m | 3 ++- Monal/Classes/MLStream.m | 4 +++- Monal/Classes/MLVoIPProcessor.m | 6 ++++-- Monal/Classes/MLWebViewController.m | 4 +++- Monal/Classes/MLXMPPManager.m | 6 ++++++ Monal/Classes/PrivacySettings.swift | 9 +++++++++ Monal/Classes/RegisterAccount.swift | 2 +- Monal/Classes/chatViewController.m | 6 ++++-- 10 files changed, 36 insertions(+), 10 deletions(-) diff --git a/Monal/Classes/HelperTools.m b/Monal/Classes/HelperTools.m index 7ba6b4df0d..abfa82d971 100644 --- a/Monal/Classes/HelperTools.m +++ b/Monal/Classes/HelperTools.m @@ -2729,7 +2729,8 @@ +(NSURLSession*) createEphemeralURLSession { NSURLSessionConfiguration* sessionConfig = [NSURLSessionConfiguration ephemeralSessionConfiguration]; if(@available(iOS 16.1, macCatalyst 16.1, *)) - sessionConfig.requiresDNSSECValidation = YES; + if([[HelperTools defaultsDB] boolForKey: @"useDnssecForAllConnections"]) + sessionConfig.requiresDNSSECValidation = YES; return [NSURLSession sessionWithConfiguration:sessionConfig]; } diff --git a/Monal/Classes/MLFiletransfer.m b/Monal/Classes/MLFiletransfer.m index 41b4427f1d..804d565f94 100644 --- a/Monal/Classes/MLFiletransfer.m +++ b/Monal/Classes/MLFiletransfer.m @@ -78,7 +78,8 @@ +(void) checkMimeTypeAndSizeForHistoryID:(NSNumber*) historyId DDLogInfo(@"Requesting mime-type and size for historyID %@ from http server", historyId); NSMutableURLRequest* request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:url]]; if(@available(iOS 16.1, macCatalyst 16.1, *)) - request.requiresDNSSECValidation = YES; + if([[HelperTools defaultsDB] boolForKey: @"useDnssecForAllConnections"]) + request.requiresDNSSECValidation = YES; request.HTTPMethod = @"HEAD"; request.cachePolicy = NSURLRequestReturnCacheDataElseLoad; diff --git a/Monal/Classes/MLHTTPRequest.m b/Monal/Classes/MLHTTPRequest.m index acce093a69..de50109ffb 100644 --- a/Monal/Classes/MLHTTPRequest.m +++ b/Monal/Classes/MLHTTPRequest.m @@ -48,7 +48,8 @@ +(void) sendWithVerb:(NSString*) verb path:(NSString*) path headers:(NSDictiona cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:60.0]; if(@available(iOS 16.1, macCatalyst 16.1, *)) - theRequest.requiresDNSSECValidation = YES; + if([[HelperTools defaultsDB] boolForKey: @"useDnssecForAllConnections"]) + theRequest.requiresDNSSECValidation = YES; [theRequest setHTTPMethod:verb]; NSData* dataToSubmit = postedData; diff --git a/Monal/Classes/MLStream.m b/Monal/Classes/MLStream.m index 3ae299f29a..4a2c6562b1 100644 --- a/Monal/Classes/MLStream.m +++ b/Monal/Classes/MLStream.m @@ -530,8 +530,10 @@ +(void) connectWithSNIDomain:(NSString*) SNIDomain connectHost:(NSString*) host } //needed to activate tcp fast open with apple's internal tls framer nw_parameters_set_fast_open_enabled(parameters, YES); + //use dnssec if configured if(@available(iOS 16.0, macCatalyst 16.0, *)) - nw_parameters_set_requires_dnssec_validation(parameters, YES); + if([[HelperTools defaultsDB] boolForKey: @"useDnssecForAllConnections"]) + nw_parameters_set_requires_dnssec_validation(parameters, YES); //create and configure connection object nw_endpoint_t endpoint = nw_endpoint_create_host([host cStringUsingEncoding:NSUTF8StringEncoding], [[port stringValue] cStringUsingEncoding:NSUTF8StringEncoding]); diff --git a/Monal/Classes/MLVoIPProcessor.m b/Monal/Classes/MLVoIPProcessor.m index a46a939487..36021c6131 100644 --- a/Monal/Classes/MLVoIPProcessor.m +++ b/Monal/Classes/MLVoIPProcessor.m @@ -404,7 +404,8 @@ -(void) initWebRTCForPendingCall:(MLCall*) call // request turn credentials NSMutableURLRequest* urlRequest = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"/api/v1/challenge/new" relativeToURL:[HelperTools getFailoverTurnApiServer]]]; if(@available(iOS 16.1, macCatalyst 16.1, *)) - urlRequest.requiresDNSSECValidation = YES; + if([[HelperTools defaultsDB] boolForKey: @"useDnssecForAllConnections"]) + urlRequest.requiresDNSSECValidation = YES; [urlRequest setTimeoutInterval:3.0]; NSURLSession* challengeSession = [HelperTools createEphemeralURLSession]; [[challengeSession dataTaskWithRequest:urlRequest completionHandler:^(NSData* data, NSURLResponse* response, NSError* error) { @@ -444,7 +445,8 @@ -(void) initWebRTCForPendingCall:(MLCall*) call } NSMutableURLRequest* responseRequest = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"/api/v1/challenge/validate" relativeToURL:[HelperTools getFailoverTurnApiServer]]]; if(@available(iOS 16.1, macCatalyst 16.1, *)) - responseRequest.requiresDNSSECValidation = YES; + if([[HelperTools defaultsDB] boolForKey: @"useDnssecForAllConnections"]) + responseRequest.requiresDNSSECValidation = YES; [responseRequest setHTTPMethod:@"POST"]; [responseRequest setValue:@"application/json" forHTTPHeaderField:@"Accept"]; diff --git a/Monal/Classes/MLWebViewController.m b/Monal/Classes/MLWebViewController.m index ea42e4c95d..33ae3a260f 100644 --- a/Monal/Classes/MLWebViewController.m +++ b/Monal/Classes/MLWebViewController.m @@ -7,6 +7,7 @@ // #import "MLWebViewController.h" +#import "HelperTools.h" @interface MLWebViewController () @property (weak, nonatomic) IBOutlet WKWebView* webview; @@ -31,7 +32,8 @@ -(void) viewWillAppear:(BOOL)animated { NSMutableURLRequest* nsrequest = [NSMutableURLRequest requestWithURL: self.urltoLoad]; if(@available(iOS 16.1, macCatalyst 16.1, *)) - nsrequest.requiresDNSSECValidation = YES; + if([[HelperTools defaultsDB] boolForKey: @"useDnssecForAllConnections"]) + nsrequest.requiresDNSSECValidation = YES; [self.webview loadRequest:nsrequest]; } self.navigationItem.largeTitleDisplayMode = UINavigationItemLargeTitleDisplayModeNever; diff --git a/Monal/Classes/MLXMPPManager.m b/Monal/Classes/MLXMPPManager.m index 01247a8ead..35857c5bac 100644 --- a/Monal/Classes/MLXMPPManager.m +++ b/Monal/Classes/MLXMPPManager.m @@ -149,6 +149,12 @@ -(void) defaultSettings #else [self upgradeBoolUserSettingsIfUnset:@"showKeyboardOnChatOpen" toDefault:NO]; #endif + +#ifdef IS_ALPHA + [self upgradeBoolUserSettingsIfUnset:@"useDnssecForAllConnections" toDefault:YES]; +#else + [self upgradeBoolUserSettingsIfUnset:@"useDnssecForAllConnections" toDefault:NO]; +#endif } -(void) upgradeFloatUserSettingsToInteger:(NSString*) settingsName diff --git a/Monal/Classes/PrivacySettings.swift b/Monal/Classes/PrivacySettings.swift index b86f41c07a..6e087a3ab9 100644 --- a/Monal/Classes/PrivacySettings.swift +++ b/Monal/Classes/PrivacySettings.swift @@ -78,6 +78,9 @@ class PrivacyDefaultDB: ObservableObject { @defaultsDB("showKeyboardOnChatOpen") var showKeyboardOnChatOpen: Bool + + @defaultsDB("useDnssecForAllConnections") + var useDnssecForAllConnections: Bool } @@ -159,6 +162,12 @@ struct PrivacyScreen: View { Toggle(isOn: $privacyDefaultDB.autodeleteAllMessagesAfter3Days) { Text("Autodelete all messages after 3 days") } + if #available(iOS 16.0, macCatalyst 16.0, *) { + Text("Use DNSSEC to validate all DNS query responses before connecting to the IP designated in the DNS response. While being more secure, this can lead to connection problems in certain networks like Hotel WiFi etc.") + Toggle(isOn: $privacyDefaultDB.useDnssecForAllConnections) { + Text("Use DNSSEC validation for all connections") + } + } } .navigationBarTitle("Privacy & security", displayMode: .inline) } diff --git a/Monal/Classes/RegisterAccount.swift b/Monal/Classes/RegisterAccount.swift index 35d0c86616..7df568471e 100644 --- a/Monal/Classes/RegisterAccount.swift +++ b/Monal/Classes/RegisterAccount.swift @@ -18,7 +18,7 @@ struct WebView: UIViewRepresentable { func updateUIView(_ webView: WKWebView, context: Context) { var request = URLRequest(url: url) - if #available(iOS 16.1, macCatalyst 16.1, *) { + if #available(iOS 16.1, macCatalyst 16.1, *), HelperTools.defaultsDB().bool(forKey:"useDnssecForAllConnections") { request.requiresDNSSECValidation = true; } webView.load(request) diff --git a/Monal/Classes/chatViewController.m b/Monal/Classes/chatViewController.m index 8a2d94a618..b5b18f830a 100644 --- a/Monal/Classes/chatViewController.m +++ b/Monal/Classes/chatViewController.m @@ -3008,7 +3008,8 @@ -(void) loadPreviewWithUrlForRow:(NSIndexPath *) indexPath withResultHandler:(mo DDLogVerbose(@"Fetching HTTP HEAD for %@...", row.url); NSMutableURLRequest* headRequest = [[NSMutableURLRequest alloc] initWithURL:row.url]; if(@available(iOS 16.1, macCatalyst 16.1, *)) - headRequest.requiresDNSSECValidation = YES; + if([[HelperTools defaultsDB] boolForKey: @"useDnssecForAllConnections"]) + headRequest.requiresDNSSECValidation = YES; headRequest.HTTPMethod = @"HEAD"; headRequest.cachePolicy = NSURLRequestReturnCacheDataElseLoad; NSURLSession* session = [HelperTools createEphemeralURLSession]; @@ -3083,7 +3084,8 @@ -(void) downloadPreviewWithRow:(NSIndexPath*) indexPath usingByterange:(BOOL) us DDLogVerbose(@"Fetching HTTP GET for %@...", row.url); NSMutableURLRequest* request = [[NSMutableURLRequest alloc] initWithURL:row.url]; if(@available(iOS 16.1, macCatalyst 16.1, *)) - request.requiresDNSSECValidation = YES; + if([[HelperTools defaultsDB] boolForKey: @"useDnssecForAllConnections"]) + request.requiresDNSSECValidation = YES; [request setValue:@"facebookexternalhit/1.1" forHTTPHeaderField:@"User-Agent"]; //required on some sites for og tags e.g. youtube if(useByterange) [request setValue:@"bytes=0-524288" forHTTPHeaderField:@"Range"]; From 7991a3dd5945046f7dfa691c546a0e8fc847fb33 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Mon, 20 May 2024 22:58:17 +0200 Subject: [PATCH 041/143] Fix swiftui warning in privacy settings --- Monal/Classes/PrivacySettings.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Monal/Classes/PrivacySettings.swift b/Monal/Classes/PrivacySettings.swift index 6e087a3ab9..4d601c1d5b 100644 --- a/Monal/Classes/PrivacySettings.swift +++ b/Monal/Classes/PrivacySettings.swift @@ -155,7 +155,7 @@ struct PrivacyScreen: View { Text(getNotificationPrivacyOption(option)).tag(option.rawValue) } } - .frame(width: .infinity, height: 56, alignment: .trailing) + .frame(height: 56, alignment: .trailing) Toggle(isOn: $privacyDefaultDB.omemoDefaultOn) { Text("Enable encryption by default for new chats") } From acece90a3d3bb3da3fa5a3dfce675b19bbb58e91 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Mon, 20 May 2024 22:58:32 +0200 Subject: [PATCH 042/143] Rework scroll to bottom again --- Monal/Classes/chatViewController.m | 48 ++++++++++++++++++------------ 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/Monal/Classes/chatViewController.m b/Monal/Classes/chatViewController.m index b5b18f830a..0a45fdb52c 100644 --- a/Monal/Classes/chatViewController.m +++ b/Monal/Classes/chatViewController.m @@ -55,6 +55,7 @@ @interface chatViewController()* _localMLContactCache; BOOL _isRecording; BOOL _isAtBottom; + monal_void_block_t _scrollToBottomTimer; } @property (nonatomic, strong) NSDateFormatter* destinationDateFormat; @@ -1685,6 +1686,7 @@ -(MLMessage* _Nullable) addMessageto:(NSString *)to withMessage:(nonnull NSStrin //update message list in ui dispatch_async(dispatch_get_main_queue(), ^{ + BOOL wasAtBottom = self->_isAtBottom; [self.messageTable performBatchUpdates:^{ if(!self.messageList) self.messageList = [NSMutableArray new]; @@ -1697,7 +1699,8 @@ -(MLMessage* _Nullable) addMessageto:(NSString *)to withMessage:(nonnull NSStrin withRowAnimation:UITableViewRowAnimationNone]; } } completion:^(BOOL finished) { - [self scrollToBottomIfNeeded]; + if(wasAtBottom) + [self scrollToBottomAnimated:NO]; }]; }); @@ -1730,6 +1733,8 @@ -(void) handleNewMessage:(NSNotification *)notification if([message isEqualToContact:self.contact]) { dispatch_async(dispatch_get_main_queue(), ^{ + BOOL wasAtBottom = self->_isAtBottom; + if(!self.messageList) self.messageList = [NSMutableArray new]; @@ -1766,7 +1771,7 @@ -(void) handleNewMessage:(NSNotification *)notification } [self->_messageTable endUpdates]; - [self scrollToBottomIfNeeded]; + [CATransaction commit]; if (self.searchController.isActive) @@ -1777,6 +1782,9 @@ -(void) handleNewMessage:(NSNotification *)notification } [self refreshCounter]; + + if(wasAtBottom) + [self scrollToBottomAnimated:YES]; }); } } @@ -1917,9 +1925,8 @@ -(void) scrollToBottomIfNeeded { if(_isAtBottom) { - dispatch_async(dispatch_get_main_queue(), ^{ - [self scrollToBottomAnimated:YES]; - }); + //DDLogVerbose(@"Scrolling to bottom because needed: %@", [NSThread callStackSymbols]); + [self scrollToBottomAnimated:NO]; } } @@ -1927,15 +1934,27 @@ -(void) scrollToBottomAnimated:(BOOL) animated { if(self.messageList.count == 0) return; - [HelperTools dispatchAsync:YES reentrantOnQueue:dispatch_get_main_queue() withBlock:^{ + monal_void_block_t scrollBlock = ^{ NSInteger bottom = [self.messageTable numberOfRowsInSection:messagesSection]; if(bottom > 0) { + DDLogVerbose(@"Scrolling to bottom(%@): %@", bool2str(animated), [NSThread callStackSymbols]); NSIndexPath* path1 = [NSIndexPath indexPathForRow:bottom-1 inSection:messagesSection]; - [self.messageTable scrollToRowAtIndexPath:path1 atScrollPosition:UITableViewScrollPositionTop animated:animated]; + [self.messageTable scrollToRowAtIndexPath:path1 atScrollPosition:UITableViewScrollPositionBottom animated:animated]; self->_isAtBottom = YES; } - }]; + }; + if(animated) + { + DDLogVerbose(@"Registering timer for scrolling to bottom(%@): %@", bool2str(animated), [NSThread callStackSymbols]); + if(_scrollToBottomTimer) + _scrollToBottomTimer(); + _scrollToBottomTimer = createQueuedTimer(0.1, dispatch_get_main_queue(), (^{ + scrollBlock(); + })); + } + else + [HelperTools dispatchAsync:NO reentrantOnQueue:dispatch_get_main_queue() withBlock:scrollBlock]; } #pragma mark - date time @@ -2925,9 +2944,9 @@ -(void) commandFPressed:(UIKeyCommand*)keyCommand # pragma mark - Textview delegate functions -- (void)textViewDidBeginEditing:(UITextView *)textView +-(void) textViewDidBeginEditing:(UITextView*) textView { - //[self scrollToBottomAnimated:YES]; + [self scrollToBottomIfNeeded]; } - (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text @@ -3135,7 +3154,6 @@ - (void)keyboardWillDisappear:(NSNotification*) aNotification - (void)keyboardDidShow:(NSNotification*)aNotification { - static BOOL firstTime = YES; //TODO grab animation info NSDictionary* info = [aNotification userInfo]; CGSize kbSize = [[info objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue].size; @@ -3147,15 +3165,7 @@ - (void)keyboardDidShow:(NSNotification*)aNotification self.messageTable.scrollIndicatorInsets = contentInsets; //this will be automatically called once the whole chat view is loaded (even if not showing a keyboard) - //--> filter that first call to not scroll a few pixels on view open - //(the few pixels come from some margin/padding only applied after viewDidAppear was called) -#if TARGET_OS_MACCATALYST [self scrollToBottomIfNeeded]; -#else - if(!firstTime) - [self scrollToBottomIfNeeded]; -#endif - firstTime = NO; } - (void)keyboardDidHide:(NSNotification*)aNotification From 0b3847b487da983d34988d020952842adc79d474 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Tue, 21 May 2024 01:13:40 +0200 Subject: [PATCH 043/143] Prepare SRV resolver for DNSSEC (but commented out) This can only be activated once the ios bug has been fixed --- Monal/Classes/MLDNSLookup.m | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/Monal/Classes/MLDNSLookup.m b/Monal/Classes/MLDNSLookup.m index 1055138fb1..8116f7a02c 100644 --- a/Monal/Classes/MLDNSLookup.m +++ b/Monal/Classes/MLDNSLookup.m @@ -45,7 +45,7 @@ -(void) doDiscoveryWithSecure:(BOOL) secure andDomain:(NSString*) domain withTim NSString* serviceDiscoveryString = [NSString stringWithFormat:@"_xmpp%@-client._tcp.%@", secure ? @"s" : @"", domain]; res = DNSServiceQueryRecord( &sdRef, - kDNSServiceFlagsReturnIntermediates, + kDNSServiceFlagsReturnIntermediates, // | kDNSServiceFlagsValidate, 0, [serviceDiscoveryString UTF8String], kDNSServiceType_SRV, @@ -125,6 +125,8 @@ -(NSArray*) doRealDnsDiscoverOnDomain:(NSString*) domain withTimeout:(NSTimeInte //wait for both dns queries to complete dispatch_barrier_sync(queue, ^{ DDLogVerbose(@"SRV DNS queries completed (xmpps AND xmpp)..."); +// [HelperTools flushLogsWithTimeout:0.100]; +// exit(0); }); @synchronized(self.discoveredServers) { @@ -240,16 +242,13 @@ void query_cb(const DNSServiceRef DNSServiceRef, const DNSServiceFlags flags, co { //make sure the compiler doesn't cry because of unused arguments (void)DNSServiceRef; - (void)flags; (void)interfaceIndex; (void)rrclass; - (void)ttl; - (void)_context; //just ignore errors (don't fill anything into the discoveredServers array) if(errorCode) { - // DDLogVerbose(@"query callback: error==%d\n", errorCode); + DDLogVerbose(@"query callback: error==%d\n", errorCode); return; } @@ -265,7 +264,7 @@ void query_cb(const DNSServiceRef DNSServiceRef, const DNSServiceFlags flags, co if(srvDomainLen > MAX_DOMAIN_NAME) return; ConvertDomainNameToCString_withescape(&srv->target, srvDomainLen, targetStr, 0); - DDLogVerbose(@"pri=%d, w=%d, port=%d, target=%s, ttl=%u\n", ntohs(srv->priority), ntohs(srv->weight), ntohs(srv->port), targetStr, ttl); + DDLogVerbose(@"pri=%d, w=%d, port=%d, target=%s, ttl=%u, flags=%u\n", ntohs(srv->priority), ntohs(srv->weight), ntohs(srv->port), targetStr, ttl, (u_int32_t)flags); NSString* theServer = [NSString stringWithUTF8String:targetStr]; NSNumber* prio = [NSNumber numberWithUnsignedInt:(ntohs(srv->priority) + (isSecure == YES ? 0 : UINT16_MAX))]; // prefer TLS over STARTTLS From 51af80da2f4be2f056470a978b9ad76a3826a975 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Mon, 20 May 2024 23:34:21 +0200 Subject: [PATCH 044/143] Only send outgoing displayed markers if request by contact (XEP-0333) --- Monal/Classes/MonalAppDelegate.m | 13 ++------ Monal/Classes/chatViewController.m | 9 ++---- Monal/Classes/xmpp.h | 2 +- Monal/Classes/xmpp.m | 52 +++++++++++++++++++++--------- 4 files changed, 43 insertions(+), 33 deletions(-) diff --git a/Monal/Classes/MonalAppDelegate.m b/Monal/Classes/MonalAppDelegate.m index 2b6300eac1..18e8c81a3e 100644 --- a/Monal/Classes/MonalAppDelegate.m +++ b/Monal/Classes/MonalAppDelegate.m @@ -951,16 +951,9 @@ -(void) userNotificationCenter:(UNUserNotificationCenter*) center didReceiveNoti NSArray* unread = [[DataLayer sharedInstance] markMessagesAsReadForBuddy:fromContact.contactJid andAccount:fromContact.accountId tillStanzaId:messageId wasOutgoing:NO]; DDLogDebug(@"Marked as read: %@", unread); - //send displayed marker for last unread message *marked as wanting chat markers* (XEP-0333) -// for(MLMessage* msg in unread) -// ; //TODO: implement this!! - - MLMessage* lastUnreadMessage = [unread lastObject]; - if(lastUnreadMessage) - { - DDLogDebug(@"Sending XEP-0333 displayed marker for message '%@'", lastUnreadMessage.messageId); - [account sendDisplayMarkerForMessage:lastUnreadMessage]; - } + //publish MDS display marker and optionally send displayed marker for last unread message (XEP-0333) + DDLogDebug(@"Sending MDS (and possibly XEP-0333 displayed marker) for messages: %@", unread); + [account sendDisplayMarkerForMessages:unread]; //remove notifications of all read messages (this will cause the MLNotificationManager to update the app badge, too) [[MLNotificationQueue currentQueue] postNotificationName:kMonalDisplayedMessagesNotice object:account userInfo:@{@"messagesArray":unread}]; diff --git a/Monal/Classes/chatViewController.m b/Monal/Classes/chatViewController.m index 0a45fdb52c..d404b74115 100644 --- a/Monal/Classes/chatViewController.m +++ b/Monal/Classes/chatViewController.m @@ -1005,12 +1005,8 @@ -(void) refreshCounter NSArray* unread = [[DataLayer sharedInstance] markMessagesAsReadForBuddy:self.contact.contactJid andAccount:self.contact.accountId tillStanzaId:nil wasOutgoing:NO]; //publish MDS display marker and optionally send displayed marker for last unread message (XEP-0333) - MLMessage* lastUnreadMessage = [unread lastObject]; - if(lastUnreadMessage) - { - DDLogDebug(@"Sending XEP-0333 displayed marker for message '%@'", lastUnreadMessage.messageId); - [self.xmppAccount sendDisplayMarkerForMessage:lastUnreadMessage]; - } + DDLogDebug(@"Sending MDS (and possibly XEP-0333 displayed marker) for messages: %@", unread); + [self.xmppAccount sendDisplayMarkerForMessages:unread]; //now switch back to the main thread, we are reading only (and self.contact should only be accessed from the main thread) dispatch_async(dispatch_get_main_queue(), ^{ @@ -1943,6 +1939,7 @@ -(void) scrollToBottomAnimated:(BOOL) animated [self.messageTable scrollToRowAtIndexPath:path1 atScrollPosition:UITableViewScrollPositionBottom animated:animated]; self->_isAtBottom = YES; } + [self refreshCounter]; }; if(animated) { diff --git a/Monal/Classes/xmpp.h b/Monal/Classes/xmpp.h index a34be781da..49302cd42f 100644 --- a/Monal/Classes/xmpp.h +++ b/Monal/Classes/xmpp.h @@ -228,7 +228,7 @@ typedef void (^monal_iq_handler_t)(XMPPIQ* _Nullable); -(NSMutableArray*) getOrderedMamPageFor:(NSString*) mamQueryId; -(void) bindResource:(NSString*) resource; -(void) initSession; --(void) sendDisplayMarkerForMessage:(MLMessage*) msg; +-(void) sendDisplayMarkerForMessages:(NSArray*) unread; -(void) publishAvatar:(UIImage*) image; -(void) publishStatusMessage:(NSString*) message; -(void) delayIncomingMessageStanzasForArchiveJid:(NSString*) archiveJid; diff --git a/Monal/Classes/xmpp.m b/Monal/Classes/xmpp.m index ae9cdc2531..c567e94e6b 100644 --- a/Monal/Classes/xmpp.m +++ b/Monal/Classes/xmpp.m @@ -5451,42 +5451,62 @@ -(void) publishMDSMarkerForMessage:(MLMessage*) msg }]; } --(void) sendDisplayMarkerForMessage:(MLMessage*) msg +-(void) sendDisplayMarkerForMessages:(NSArray*) unread { + //ignore empty arrays + if(unread.count == 0) + return; + + //send displayed marker for last unread message *marked as wanting chat markers* (XEP-0333) + MLMessage* lastMarkableMessage = nil; + for(MLMessage* msg in unread) + if(msg.displayMarkerWanted) + lastMarkableMessage = msg; + + //last unread message used for mds + MLMessage* lastUnreadMessage = [unread lastObject]; + if(![[HelperTools defaultsDB] boolForKey:@"SendDisplayedMarkers"]) { DDLogVerbose(@"Not sending chat marker, configured to not do so..."); - [self publishMDSMarkerForMessage:msg]; //always publish mds marker + [self publishMDSMarkerForMessage:lastUnreadMessage]; //always publish mds marker return; } - //don't send chatmarkers in channels - if(msg.isMuc && [@"channel" isEqualToString:msg.mucType]) + //don't send chatmarkers in channels (all messages have the same muc attributes, randomly pick the last one) + if(lastUnreadMessage.isMuc && [@"channel" isEqualToString:lastUnreadMessage.mucType]) { DDLogVerbose(@"Not sending XEP-0333 chat marker in channel..."); - [self publishMDSMarkerForMessage:msg]; //always publish mds marker + [self publishMDSMarkerForMessage:lastUnreadMessage]; //always publish mds marker return; } - MLContact* contact = [MLContact createContactFromJid:msg.buddyName andAccountNo:msg.accountId]; + //all messages have the same contact, randomly pick the last one + MLContact* contact = [MLContact createContactFromJid:lastUnreadMessage.buddyName andAccountNo:lastUnreadMessage.accountId]; //don't send chatmarkers to 1:1 chats with users in our contact list that did not subscribe us (e.g. are not allowed to see us) if(!contact.isGroup && !contact.isSubscribedFrom) { DDLogVerbose(@"Not sending chat marker, we are not subscribed from this contact..."); - [self publishMDSMarkerForMessage:msg]; //always publish mds marker + [self publishMDSMarkerForMessage:lastUnreadMessage]; //always publish mds marker return; } - XMPPMessage* displayedNode = [[XMPPMessage alloc] initToContact:contact]; - [displayedNode setDisplayed:msg.isMuc && msg.stanzaId != nil ? msg.stanzaId : msg.messageId]; - if([self.connectionProperties.accountFeatures containsObject:@"urn:xmpp:mds:server-assist:0"]) - [displayedNode setMDSDisplayed:msg.stanzaId withStanzaIdBy:(msg.isMuc ? msg.buddyName : self.connectionProperties.identity.jid)]; - [displayedNode setStoreHint]; - DDLogVerbose(@"Sending display marker: %@", displayedNode); - [self send:displayedNode]; + //only send chatmarkers if requested by contact + BOOL assistedMDS = [self.connectionProperties.accountFeatures containsObject:@"urn:xmpp:mds:server-assist:0"] && lastMarkableMessage == lastUnreadMessage; + if(lastMarkableMessage != nil) + { + XMPPMessage* displayedNode = [[XMPPMessage alloc] initToContact:contact]; + [displayedNode setDisplayed:lastMarkableMessage.isMuc && lastMarkableMessage.stanzaId != nil ? lastMarkableMessage.stanzaId : lastMarkableMessage.messageId]; + if(assistedMDS) + [displayedNode setMDSDisplayed:lastMarkableMessage.stanzaId withStanzaIdBy:(lastMarkableMessage.isMuc ? lastMarkableMessage.buddyName : self.connectionProperties.identity.jid)]; + [displayedNode setStoreHint]; + DDLogVerbose(@"Sending display marker: %@", displayedNode); + [self send:displayedNode]; + } - if(![self.connectionProperties.accountFeatures containsObject:@"urn:xmpp:mds:server-assist:0"]) - [self publishMDSMarkerForMessage:msg]; //always publish mds marker + //send mds if not already done by server using mds-assist + if(!assistedMDS) + [self publishMDSMarkerForMessage:lastUnreadMessage]; //always publish mds marker } -(void) removeFromServerWithCompletion:(void (^)(NSString* _Nullable error)) completion From d4c21167a838d510f6be0769231d93344b11ed75 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Tue, 21 May 2024 01:12:12 +0200 Subject: [PATCH 045/143] Fix unread badge that got broken by MLContact singleton implementation --- Monal/Classes/MLContact.m | 75 ++++++++++++++++++++++----------------- 1 file changed, 42 insertions(+), 33 deletions(-) diff --git a/Monal/Classes/MLContact.m b/Monal/Classes/MLContact.m index a3314d02c8..e84cb20804 100644 --- a/Monal/Classes/MLContact.m +++ b/Monal/Classes/MLContact.m @@ -194,6 +194,40 @@ +(NSString*) ownDisplayNameForAccount:(xmpp*) account return nilDefault(displayName, @""); } ++(MLContact*) createContactFromDatabaseWithJid:(NSString*) jid andAccountNo:(NSNumber*) accountNo +{ + NSDictionary* contactDict = [[DataLayer sharedInstance] contactDictionaryForUsername:jid forAccount:accountNo]; + + // check if we know this contact and return a dummy one if not + if(contactDict == nil) + { + DDLogInfo(@"Returning dummy MLContact for %@ on accountNo %@", jid, accountNo); + return [self contactFromDictionary:@{ + @"buddy_name": jid.lowercaseString, + @"nick_name": @"", + @"full_name": @"", + @"subscription": kSubNone, + @"ask": @"", + @"account_id": accountNo, + //@"muc_subject": nil, + //@"muc_nick": nil, + @"Muc": @NO, + @"mentionOnly": @NO, + @"pinned": @NO, + @"blocked": @NO, + @"encrypt": @NO, + @"muted": @NO, + @"status": @"", + @"state": @"offline", + @"count": @0, + @"isActiveChat": @NO, + @"lastInteraction": nilWrapper(nil), + }]; + } + else + return [self contactFromDictionary:contactDict]; +} + +(MLContact*) createContactFromJid:(NSString*) jid andAccountNo:(NSNumber*) accountNo { MLAssert(jid != nil, @"jid must not be nil"); @@ -209,37 +243,7 @@ +(MLContact*) createContactFromJid:(NSString*) jid andAccountNo:(NSNumber*) acco [_singletonCache removeObjectForKey:cacheKey]; } - NSDictionary* contactDict = [[DataLayer sharedInstance] contactDictionaryForUsername:jid forAccount:accountNo]; - MLContact* retval = nil; - - // check if we know this contact and return a dummy one if not - if(contactDict == nil) - { - DDLogInfo(@"Returning dummy MLContact for %@ on accountNo %@", jid, accountNo); - retval = [self contactFromDictionary:@{ - @"buddy_name": jid.lowercaseString, - @"nick_name": @"", - @"full_name": @"", - @"subscription": kSubNone, - @"ask": @"", - @"account_id": accountNo, - //@"muc_subject": nil, - //@"muc_nick": nil, - @"Muc": @NO, - @"mentionOnly": @NO, - @"pinned": @NO, - @"blocked": @NO, - @"encrypt": @NO, - @"muted": @NO, - @"status": @"", - @"state": @"offline", - @"count": @0, - @"isActiveChat": @NO, - @"lastInteraction": nilWrapper(nil), - }]; - } - else - retval = [self contactFromDictionary:contactDict]; + MLContact* retval = [self createContactFromDatabaseWithJid:jid andAccountNo:accountNo]; _singletonCache[cacheKey] = [[WeakContainer alloc] initWithObj:retval]; return retval; @@ -253,7 +257,11 @@ -(instancetype) init [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleLastInteractionTimeUpdate:) name:kMonalLastInteractionUpdatedNotice object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleBlockListRefresh:) name:kMonalBlockListRefresh object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleContactRefresh:) name:kMonalContactRefresh object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleContactRefresh:) name:kMonalContactRemoved object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleMucSubjectChange:) name:kMonalMucSubjectChanged object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateUnreadCount) name:kMonalNewMessageNotice object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateUnreadCount) name:kMonalDeletedMessageNotice object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateUnreadCount) name:kMLMessageSentToContact object:nil]; return self; } @@ -294,7 +302,8 @@ -(void) handleContactRefresh:(NSNotification*) notification MLContact* contact = data[@"contact"]; if(![self.contactJid isEqualToString:contact.contactJid] || self.accountId.intValue != contact.accountId.intValue) return; // ignore other accounts or contacts - [self updateWithContact:contact]; + [self refresh]; + [self updateUnreadCount]; //only handle avatar updates if the property was already used and the old avatar is cached in this contact if(_avatar != nil) { @@ -319,7 +328,7 @@ -(void) handleMucSubjectChange:(NSNotification*) notification -(void) refresh { - [self updateWithContact:[MLContact createContactFromJid:self.contactJid andAccountNo:self.accountId]]; + [self updateWithContact:[[self class] createContactFromDatabaseWithJid:self.contactJid andAccountNo:self.accountId]]; } -(void) updateUnreadCount From 089794d6809b61efe1907ccc42feadf778a1bf8a Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Tue, 21 May 2024 03:48:24 +0200 Subject: [PATCH 046/143] Rework settings to have more natural categories This resembles more or less what Conversations uses for its new categorized settings view --- Monal/Classes/ActiveChatsViewController.h | 1 + Monal/Classes/ActiveChatsViewController.m | 6 + Monal/Classes/BackgroundSettings.swift | 7 +- Monal/Classes/ContactDetails.swift | 2 +- Monal/Classes/GeneralSettings.swift | 381 ++++++++++++++++++ Monal/Classes/MLSettingsTableViewController.m | 28 +- Monal/Classes/MonalAppDelegate.m | 2 +- ...ings.swift => NotificationDebugging.swift} | 37 +- Monal/Classes/PrivacySettings.swift | 315 --------------- Monal/Classes/SwiftuiHelpers.swift | 40 +- Monal/Monal.xcodeproj/project.pbxproj | 16 +- 11 files changed, 435 insertions(+), 400 deletions(-) create mode 100644 Monal/Classes/GeneralSettings.swift rename Monal/Classes/{NotificationSettings.swift => NotificationDebugging.swift} (81%) delete mode 100644 Monal/Classes/PrivacySettings.swift diff --git a/Monal/Classes/ActiveChatsViewController.h b/Monal/Classes/ActiveChatsViewController.h index 0b3eb189e3..ef345377a9 100644 --- a/Monal/Classes/ActiveChatsViewController.h +++ b/Monal/Classes/ActiveChatsViewController.h @@ -39,6 +39,7 @@ NS_ASSUME_NONNULL_BEGIN -(void) deleteConversation; -(void) showSettings; -(void) showPrivacySettings; +-(void) showNotificationSettings; -(void) showDetails; -(void) showRegisterWithUsername:(NSString*) username onHost:(NSString*) host withToken:(NSString* _Nullable) token usingCompletion:(monal_id_block_t _Nullable) callback; -(void) showAddContactWithJid:(NSString*) jid preauthToken:(NSString* _Nullable) preauthToken prefillAccount:(xmpp* _Nullable) account andOmemoFingerprints:(NSDictionary* _Nullable) fingerprints; diff --git a/Monal/Classes/ActiveChatsViewController.m b/Monal/Classes/ActiveChatsViewController.m index 039a0ccb6f..38bcf37b88 100755 --- a/Monal/Classes/ActiveChatsViewController.m +++ b/Monal/Classes/ActiveChatsViewController.m @@ -508,6 +508,12 @@ -(void) openConversationPlaceholder:(MLContact*) contact } } +-(void) showNotificationSettings +{ + UIViewController* view = [[SwiftuiInterface new] makeViewWithName:@"ActiveChatsNotificatioSettings"]; + [self presentViewController:view animated:YES completion:^{}]; +} + -(void) showPrivacySettings { UIViewController* view = [[SwiftuiInterface new] makeViewWithName:@"ActiveChatsPrivacySettings"]; diff --git a/Monal/Classes/BackgroundSettings.swift b/Monal/Classes/BackgroundSettings.swift index 890d3a2af0..87d100ae9a 100644 --- a/Monal/Classes/BackgroundSettings.swift +++ b/Monal/Classes/BackgroundSettings.swift @@ -23,11 +23,9 @@ struct BackgroundSettings: View { @State private var showingImagePicker = false @State private var inputImage: UIImage? let contact: ObservableKVOWrapper? - let delegate: SheetDismisserProtocol - init(contact: ObservableKVOWrapper?, delegate: SheetDismisserProtocol) { + init(contact: ObservableKVOWrapper?) { self.contact = contact - self.delegate = delegate _inputImage = State(initialValue:MLImageManager.sharedInstance().getBackgroundFor(self.contact?.obj)) } @@ -129,8 +127,7 @@ struct BackgroundSettings: View { } struct BackgroundSettings_Previews: PreviewProvider { - static var delegate = SheetDismisserProtocol() static var previews: some View { - BackgroundSettings(contact:nil, delegate:delegate) + BackgroundSettings(contact:nil) } } diff --git a/Monal/Classes/ContactDetails.swift b/Monal/Classes/ContactDetails.swift index 96f7194437..ca42b7d17e 100644 --- a/Monal/Classes/ContactDetails.swift +++ b/Monal/Classes/ContactDetails.swift @@ -333,7 +333,7 @@ struct ContactDetails: View { } } - NavigationLink(destination: LazyClosureView(BackgroundSettings(contact:contact, delegate:delegate))) { + NavigationLink(destination: LazyClosureView(BackgroundSettings(contact:contact))) { Text("Change Chat Background") } diff --git a/Monal/Classes/GeneralSettings.swift b/Monal/Classes/GeneralSettings.swift new file mode 100644 index 0000000000..361b3e4ff4 --- /dev/null +++ b/Monal/Classes/GeneralSettings.swift @@ -0,0 +1,381 @@ +// +// GeneralSettings.swift +// Monal +// +// Created by Vaidik Dubey on 22/03/24. +// Copyright © 2024 monal-im.org. All rights reserved. +// + + +func getNotificationPrivacyOption(_ option: NotificationPrivacySettingOption) -> String { + switch option{ + case .DisplayNameAndMessage: + return NSLocalizedString("Display Name And Message", comment: "") + case .DisplayOnlyName: + return NSLocalizedString("Display Only Name", comment: "") + case .DisplayOnlyPlaceholder: + return NSLocalizedString("Display Only Placeholder", comment: "") + } +} + +class GeneralSettingsDefaultsDB: ObservableObject { + @defaultsDB("NotificationPrivacySetting") + var notificationPrivacySetting: Int + + @defaultsDB("OMEMODefaultOn") + var omemoDefaultOn:Bool + + @defaultsDB("AutodeleteAllMessagesAfter3Days") + var autodeleteAllMessagesAfter3Days: Bool + + @defaultsDB("SendLastUserInteraction") + var sendLastUserInteraction: Bool + + @defaultsDB("SendLastChatState") + var sendLastChatState: Bool + + @defaultsDB("SendReceivedMarkers") + var sendReceivedMarkers: Bool + + @defaultsDB("SendDisplayedMarkers") + var sendDisplayedMarkers: Bool + + @defaultsDB("ShowGeoLocation") + var showGeoLocation: Bool + + @defaultsDB("ShowURLPreview") + var showURLPreview: Bool + + @defaultsDB("webrtcAllowP2P") + var webrtcAllowP2P: Bool + + @defaultsDB("webrtcUseFallbackTurn") + var webrtcUseFallbackTurn: Bool + + @defaultsDB("allowVersionIQ") + var allowVersionIQ: Bool + + @defaultsDB("allowNonRosterContacts") + var allowNonRosterContacts: Bool + + @defaultsDB("allowCallsFromNonRosterContacts") + var allowCallsFromNonRosterContacts: Bool + + @defaultsDB("HasSeenPrivacySettings") + var hasSeenPrivacySettings: Bool + + @defaultsDB("AutodownloadFiletransfers") + var autodownloadFiletransfers : Bool + + @defaultsDB("AutodownloadFiletransfersWifiMaxSize") + var autodownloadFiletransfersWifiMaxSize : UInt + + @defaultsDB("AutodownloadFiletransfersMobileMaxSize") + var autodownloadFiletransfersMobileMaxSize : UInt + + @defaultsDB("ImageUploadQuality") + var imageUploadQuality : Float + + @defaultsDB("showKeyboardOnChatOpen") + var showKeyboardOnChatOpen: Bool + + @defaultsDB("useDnssecForAllConnections") + var useDnssecForAllConnections: Bool +} + + +struct GeneralSettings: View { + @ObservedObject var generalSettingsDefaultsDB = GeneralSettingsDefaultsDB() + + var body: some View { + Form { + Section(header:Text("General Settings")) { + NavigationLink(destination: LazyClosureView(UserInterfaceSettings())) { + HStack{ + Image(systemName: "hand.tap.fill") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 20, height: 20) + Text("User Interface") + } + } + NavigationLink(destination: LazyClosureView(SecuritySettings())) { + HStack{ + Image(systemName: "shield.checkerboard") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 20, height: 20) + Text("Security") + } + } + NavigationLink(destination: LazyClosureView(PrivacySettings())) { + HStack{ + Image(systemName: "eye") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 20, height: 20) + Text("Privacy") + } + } + NavigationLink(destination: LazyClosureView(NotificationSettings())) { + HStack{ + Image(systemName: "text.bubble") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 20, height: 20) + Text("Notifications") + } + } + NavigationLink(destination: LazyClosureView(AttachmentSettings())) { + HStack{ + Image(systemName: "paperclip") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 20, height: 20) + Text("Attachments") + } + } + } + } + .navigationBarTitle("General Settings") + .onAppear { + generalSettingsDefaultsDB.hasSeenPrivacySettings = true + } + } +} + +struct UserInterfaceSettings: View { + @ObservedObject var generalSettingsDefaultsDB = GeneralSettingsDefaultsDB() + + var body: some View { + Form { + Section(header: Text("Previews")) { + Toggle(isOn: $generalSettingsDefaultsDB.showGeoLocation) { + Text("Show inline geo location").font(.body) + Text("Received geo locations are shared with Apple's Maps App.").font(.footnote) + } + Toggle(isOn: $generalSettingsDefaultsDB.showURLPreview) { + Text("Show URL previews").font(.body) + Text("The operator of the webserver providing that URL may see your IP address.").font(.footnote) + } + } + + Section(header: Text("Input")) { + Toggle(isOn: $generalSettingsDefaultsDB.showKeyboardOnChatOpen) { + Text("Autofocus text input on chat open").font(.body) + Text("Will focus the textfield on macOS or iOS with hardware keyboard attached, will open the software keyboard otherwise.").font(.footnote) + } + } + + Section(header: Text("Appearance")) { + NavigationLink(destination: LazyClosureView(BackgroundSettings(contact:nil))) { + Text("Chat background image").font(.body) + Text("Configure the background image displayed in open chats.").font(.footnote) + } + } + } + .navigationBarTitle("User Interface", displayMode: .inline) + } +} + +struct SecuritySettings: View { + @ObservedObject var generalSettingsDefaultsDB = GeneralSettingsDefaultsDB() + + var body: some View { + Form { + Section(header: Text("Encryption")) { + Toggle(isOn: $generalSettingsDefaultsDB.omemoDefaultOn) { + Text("Enable encryption by default for new chats").font(.body) + Text("Every new contact will have encryption enabled, but already known contacts will preserve their encryption settings.").font(.footnote) + } + + if #available(iOS 16.0, macCatalyst 16.0, *) { + Toggle(isOn: $generalSettingsDefaultsDB.useDnssecForAllConnections) { + Text("Use DNSSEC validation for all connections").font(.body) + Text( +""" +Use DNSSEC to validate all DNS query responses before connecting to the IP address designated \ +in the DNS response.\n\ +While being more secure, this can lead to connection problems in certain networks \ +like hotel wifi, ugly mobile carriers etc. +""" + ).font(.footnote) + } + } + + Toggle(isOn: $generalSettingsDefaultsDB.webrtcAllowP2P) { + Text("Calls: Allow P2P sessions").font(.body) + Text("Allow your phone to establish a direct network connection to the remote party. This might leak your IP address to the caller/callee.").font(.footnote) + } + } + + Section(header: Text("On this device")) { + Toggle(isOn: $generalSettingsDefaultsDB.autodeleteAllMessagesAfter3Days) { + Text("Autodelete all messages after 3 days") + } + } + } + .navigationBarTitle("Security", displayMode: .inline) + } +} + +struct PrivacySettings: View { + @ObservedObject var generalSettingsDefaultsDB = GeneralSettingsDefaultsDB() + + var body: some View { + Form { + Section(header: Text("Activity indications")) { + Toggle(isOn: $generalSettingsDefaultsDB.sendReceivedMarkers) { + Text("Send message received").font(.body) + Text("Let your contacts know if you received a message.").font(.footnote) + } + Toggle(isOn: $generalSettingsDefaultsDB.sendDisplayedMarkers) { + Text("Send message displayed state").font(.body) + Text("Let your contacts know if you read a message.").font(.footnote) + } + Toggle(isOn: $generalSettingsDefaultsDB.sendLastChatState) { + Text("Send typing notifications").font(.body) + Text("Let your contacts know if you are typing a message.").font(.footnote) + } + Toggle(isOn: $generalSettingsDefaultsDB.sendLastUserInteraction) { + Text("Send last interaction time").font(.body) + Text("Let your contacts know when you last opened the app.").font(.footnote) + } + } + + Section(header: Text("Interactions")) { + Toggle(isOn: $generalSettingsDefaultsDB.allowNonRosterContacts) { + Text("Accept incoming messages from strangers").font(.body) + Text("Allow contacts not in your contact list to contact you.").font(.footnote) + } + Toggle(isOn: $generalSettingsDefaultsDB.allowCallsFromNonRosterContacts) { + Text("Accept incoming calls from strangers").font(.body) + Text("Allow contacts not in your contact list to call you.").font(.footnote) + }.disabled(!generalSettingsDefaultsDB.allowNonRosterContacts) + } + + Section(header: Text("Misc")) { + Toggle(isOn: $generalSettingsDefaultsDB.allowVersionIQ) { + Text("Publish version").font(.body) + Text("Allow contacts in your contact list to query your Monal and iOS versions.").font(.footnote) + } + Toggle(isOn: $generalSettingsDefaultsDB.webrtcUseFallbackTurn) { + Text("Calls: Allow TURN fallback to Monal-Servers").font(.body) + Text("This will make calls possible even if your XMPP server does not provide a TURN server.").font(.footnote) + } + } + } + .navigationBarTitle("Privacy", displayMode: .inline) + } +} + +struct NotificationSettings: View { + @ObservedObject var generalSettingsDefaultsDB = GeneralSettingsDefaultsDB() + @State private var pushPermissionEnabled = false + + private var pushNotEnabled: Bool { + let xmppAccountInfo = MLXMPPManager.sharedInstance().connectedXMPP as! [xmpp] + var pushNotEnabled = false + for account in xmppAccountInfo { + pushNotEnabled = pushNotEnabled || !account.connectionProperties.pushEnabled + } + return pushNotEnabled + } + + var body: some View { + Form { + Section(header: Text("Settings")) { + Picker(selection: $generalSettingsDefaultsDB.notificationPrivacySetting, label: Text("Notification privacy")) { + ForEach(NotificationPrivacySettingOption.allCases, id: \.self) { option in + Text(getNotificationPrivacyOption(option)).tag(option.rawValue) + } + } + .frame(height: 56, alignment: .trailing) + } + + Section(header: Text("Debugging")) { + NavigationLink(destination: LazyClosureView(NotificationDebugging())) { + buildNotificationStateLabel(Text("Debug Notification Problems"), isWorking: !self.pushNotEnabled && self.pushPermissionEnabled) + } + } + } + .onAppear { + UNUserNotificationCenter.current().getNotificationSettings { (settings) -> Void in + self.pushPermissionEnabled = (settings.authorizationStatus == .authorized || settings.authorizationStatus == .provisional); + } + } + .navigationBarTitle("Notifications", displayMode: .inline) + } +} + +struct AttachmentSettings: View { + @ObservedObject var generalSettingsDefaultsDB = GeneralSettingsDefaultsDB() + + var body: some View { + Form { + Section(header: Text("General File Transfer Settings")) { + Toggle(isOn: $generalSettingsDefaultsDB.autodownloadFiletransfers) { + Text("Auto-Download Media") + } + } + + Section(header: Text("Download Settings")) { + Text("Adjust the maximum file size for auto-downloads over WiFi") + .foregroundColor(.secondary) + .font(.footnote) + Slider( + value: $generalSettingsDefaultsDB.autodownloadFiletransfersWifiMaxSize.bytecount(mappedTo: 1024*1024), + in: 1.0...100.0, + step: 1.0, + minimumValueLabel: Text("1 MiB"), + maximumValueLabel: Text("100 MiB"), + label: { + Text("Load over wifi") + } + ) + Text("Load over WiFi up to: \(UInt(generalSettingsDefaultsDB.autodownloadFiletransfersWifiMaxSize/(1024*1024))) MiB") + } + + Section { + Text("Adjust the maximum file size for auto-downloads over cellular network") + .foregroundColor(.secondary) + .font(.footnote) + Slider( + value: $generalSettingsDefaultsDB.autodownloadFiletransfersMobileMaxSize.bytecount(mappedTo: 1024*1024), + in: 0.0...100.0, + step: 1.0, + minimumValueLabel: Text("1 MiB"), + maximumValueLabel: Text("100 MiB"), + label: { + Text("Load over Cellular") + } + ) + Text("Load over cellular up to: \(Int(generalSettingsDefaultsDB.autodownloadFiletransfersMobileMaxSize/(1024*1024))) MiB") + } + + Section(header: Text("Upload Settings")) { + Text("Adjust the quality of images uploaded") + .foregroundColor(.secondary) + .font(.footnote) + Slider( + value: $generalSettingsDefaultsDB.imageUploadQuality, + in: 0.33...1.0, + step: 0.01, + minimumValueLabel: Text("33%"), + maximumValueLabel: Text("100%"), + label: { + Text("Upload Settings") + } + ) + Text("Image Upload Quality: \(String(format: "%.0f%%", generalSettingsDefaultsDB.imageUploadQuality*100))") + } + } + } +} + + +struct ContentView_Previews: PreviewProvider { + static var previews: some View { + PrivacySettings() + } +} diff --git a/Monal/Classes/MLSettingsTableViewController.m b/Monal/Classes/MLSettingsTableViewController.m index f67b0e4b80..64d5b11c67 100644 --- a/Monal/Classes/MLSettingsTableViewController.m +++ b/Monal/Classes/MLSettingsTableViewController.m @@ -32,9 +32,7 @@ }; enum SettingsAppRows { - PrivacySettingsRow, - NotificationsRow, - BackgroundsRow, + GeneralSettingsRow, SoundsRow, SettingsAppRowsCnt }; @@ -204,14 +202,8 @@ -(UITableViewCell*) tableView:(UITableView*) tableView cellForRowAtIndexPath:(NS } case kSettingSectionApp: { switch(indexPath.row) { - case PrivacySettingsRow: - [cell initTapCell:NSLocalizedString(@"Privacy Settings", @"")]; - break; - case NotificationsRow: - [cell initTapCell:NSLocalizedString(@"Notifications", @"")]; - break; - case BackgroundsRow: - [cell initTapCell:NSLocalizedString(@"Backgrounds", @"")]; + case GeneralSettingsRow: + [cell initTapCell:NSLocalizedString(@"General Settings", @"")]; break; case SoundsRow: [cell initTapCell:NSLocalizedString(@"Sounds", @"")]; @@ -322,21 +314,11 @@ -(void)tableView:(UITableView*) tableView didSelectRowAtIndexPath:(NSIndexPath*) case kSettingSectionApp: { switch(indexPath.row) { - case PrivacySettingsRow: { - UIViewController* privacyViewController = [[SwiftuiInterface new] makeViewWithName:@"PrivacySettings"]; + case GeneralSettingsRow: { + UIViewController* privacyViewController = [[SwiftuiInterface new] makeViewWithName:@"GeneralSettings"]; [self showDetailViewController:privacyViewController sender:self]; break; } - case NotificationsRow: { - UIViewController* notificationSettingsController = [[SwiftuiInterface new] makeViewWithName:@"NotificationSettings"]; - [self showDetailViewController:notificationSettingsController sender:self]; - break; - } - case BackgroundsRow: { - UIViewController* backgroundSettingsController = [[SwiftuiInterface new] makeBackgroundSettings:nil]; - [self showDetailViewController:backgroundSettingsController sender:self]; - break; - } case SoundsRow: [self performSegueWithIdentifier:@"showSounds" sender:self]; break; diff --git a/Monal/Classes/MonalAppDelegate.m b/Monal/Classes/MonalAppDelegate.m index 18e8c81a3e..81f3f6fbee 100644 --- a/Monal/Classes/MonalAppDelegate.m +++ b/Monal/Classes/MonalAppDelegate.m @@ -1016,7 +1016,7 @@ -(void) userNotificationCenter:(UNUserNotificationCenter*) center openSettingsFo while(self.activeChats == nil) usleep(100000); dispatch_async(dispatch_get_main_queue(), ^{ - [(ActiveChatsViewController*)self.activeChats showPrivacySettings]; + [(ActiveChatsViewController*)self.activeChats showNotificationSettings]; }); }); } diff --git a/Monal/Classes/NotificationSettings.swift b/Monal/Classes/NotificationDebugging.swift similarity index 81% rename from Monal/Classes/NotificationSettings.swift rename to Monal/Classes/NotificationDebugging.swift index 8e37883ebb..859a7506ba 100644 --- a/Monal/Classes/NotificationSettings.swift +++ b/Monal/Classes/NotificationDebugging.swift @@ -1,5 +1,5 @@ // -// NotificationSettings.swift +// NotificationDebugging.swift // Monal // // Created by Jan on 02.05.22. @@ -8,28 +8,7 @@ import OrderedCollections -struct NotificationSettings: View { - @ViewBuilder - func buildLabel(_ description: Text, isWorking: Bool) -> some View { - if(isWorking == true) { - Label(title: { - description - }, icon: { - Image(systemName: "checkmark.seal") - .foregroundColor(.green) - }) - } else { - Label(title: { - description - }, icon: { - Image(systemName: "xmark.seal") - .foregroundColor(.red) - }) - } - } - - var delegate: SheetDismisserProtocol - +struct NotificationDebugging: View { private let applePushEnabled: Bool private let applePushToken: String private let xmppAccountInfo: [xmpp] @@ -46,7 +25,7 @@ struct NotificationSettings: View { Group { Section(header: Text("Status").font(.title3)) { VStack(alignment: .leading) { - buildLabel(Text("Apple Push Service"), isWorking: self.applePushEnabled); + buildNotificationStateLabel(Text("Apple Push Service"), isWorking: self.applePushEnabled); Divider() Text("Apple push service should always be on. If it is off, your device can not talk to Apple's server.").font(.footnote) if !self.applePushEnabled, let apnsError = MLXMPPManager.sharedInstance().apnsError { @@ -70,7 +49,7 @@ struct NotificationSettings: View { } Section { VStack(alignment: .leading) { - buildLabel(Text("Can Show Notifications"), isWorking: self.pushPermissionEnabled); + buildNotificationStateLabel(Text("Can Show Notifications"), isWorking: self.pushPermissionEnabled); Divider() Text("If Monal can't show notifications, you will not see alerts when a message arrives. This happens if you tapped 'Decline' when Monal first asked permission. Fix it by going to iOS Settings -> Monal -> Notifications and select 'Allow Notifications'.").font(.footnote) } @@ -79,7 +58,7 @@ struct NotificationSettings: View { Section { VStack(alignment: .leading) { ForEach(self.xmppAccountInfo, id: \.self) { account in - buildLabel(Text(account.connectionProperties.identity.jid), isWorking: account.connectionProperties.pushEnabled) + buildNotificationStateLabel(Text(account.connectionProperties.identity.jid), isWorking: account.connectionProperties.pushEnabled) Divider() } Text("If this is off your device could not activate push on your xmpp server, make sure to have configured it to support XEP-0357.").font(.footnote) @@ -123,11 +102,10 @@ struct NotificationSettings: View { }); } - init(delegate: SheetDismisserProtocol) { + init() { self.applePushEnabled = MLXMPPManager.sharedInstance().hasAPNSToken; self.applePushToken = MLXMPPManager.sharedInstance().pushToken; self.xmppAccountInfo = MLXMPPManager.sharedInstance().connectedXMPP as! [xmpp] - self.delegate = delegate // push server selector self.availablePushServers = HelperTools.getAvailablePushServers() @@ -136,8 +114,7 @@ struct NotificationSettings: View { } struct PushSettings_Previews: PreviewProvider { - static var delegate = SheetDismisserProtocol() static var previews: some View { - NotificationSettings(delegate:delegate) + NotificationSettings() } } diff --git a/Monal/Classes/PrivacySettings.swift b/Monal/Classes/PrivacySettings.swift deleted file mode 100644 index 4d601c1d5b..0000000000 --- a/Monal/Classes/PrivacySettings.swift +++ /dev/null @@ -1,315 +0,0 @@ -// -// PrivacySettings.swift -// Monal -// -// Created by Vaidik Dubey on 22/03/24. -// Copyright © 2024 monal-im.org. All rights reserved. -// - - -func getNotificationPrivacyOption(_ option: NotificationPrivacySettingOption) -> String { - switch option{ - case .DisplayNameAndMessage: - return NSLocalizedString("Display Name And Message", comment: "") - case .DisplayOnlyName: - return NSLocalizedString("Display Only Name", comment: "") - case .DisplayOnlyPlaceholder: - return NSLocalizedString("Display Only Placeholder", comment: "") - } -} - -class PrivacyDefaultDB: ObservableObject { - @defaultsDB("NotificationPrivacySetting") - var notificationPrivacySetting: Int - - @defaultsDB("OMEMODefaultOn") - var omemoDefaultOn:Bool - - @defaultsDB("AutodeleteAllMessagesAfter3Days") - var autodeleteAllMessagesAfter3Days: Bool - - @defaultsDB("SendLastUserInteraction") - var sendLastUserInteraction: Bool - - @defaultsDB("SendLastChatState") - var sendLastChatState: Bool - - @defaultsDB("SendReceivedMarkers") - var sendReceivedMarkers: Bool - - @defaultsDB("SendDisplayedMarkers") - var sendDisplayedMarkers: Bool - - @defaultsDB("ShowGeoLocation") - var showGeoLocation: Bool - - @defaultsDB("ShowURLPreview") - var showURLPreview: Bool - - @defaultsDB("webrtcAllowP2P") - var webrtcAllowP2P: Bool - - @defaultsDB("webrtcUseFallbackTurn") - var webrtcUseFallbackTurn: Bool - - @defaultsDB("allowVersionIQ") - var allowVersionIQ: Bool - - @defaultsDB("allowNonRosterContacts") - var allowNonRosterContacts: Bool - - @defaultsDB("allowCallsFromNonRosterContacts") - var allowCallsFromNonRosterContacts: Bool - - @defaultsDB("HasSeenPrivacySettings") - var hasSeenPrivacySettings: Bool - - @defaultsDB("AutodownloadFiletransfers") - var autodownloadFiletransfers : Bool - - @defaultsDB("AutodownloadFiletransfersWifiMaxSize") - var autodownloadFiletransfersWifiMaxSize : UInt - - @defaultsDB("AutodownloadFiletransfersMobileMaxSize") - var autodownloadFiletransfersMobileMaxSize : UInt - - @defaultsDB("ImageUploadQuality") - var imageUploadQuality : Float - - @defaultsDB("showKeyboardOnChatOpen") - var showKeyboardOnChatOpen: Bool - - @defaultsDB("useDnssecForAllConnections") - var useDnssecForAllConnections: Bool -} - - -struct PrivacySettings: View { - @ObservedObject var privacyDefaultDB = PrivacyDefaultDB() - - var body: some View { - Form { - Section(header:Text("Privacy and security settings")) { - NavigationLink(destination: PrivacyScreen()) { - HStack{ - Image(systemName: "lock.shield") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 20, height: 20) - Text("Privacy & Security") - } - } - NavigationLink(destination: PublishingScreen()) { - HStack{ - Image(systemName: "eye") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 20, height: 20) - Text("Publishing & Appearance") - } - } - NavigationLink(destination: PreviewsScreen()) { - HStack{ - Image(systemName: "doc.text.magnifyingglass") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 20, height: 20) - Text("Previews") - } - } - NavigationLink(destination: CommunicationScreen()) { - HStack{ - Image(systemName: "bubble.left.and.bubble.right") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 20, height: 20) - Text("Communication") - } - } - - NavigationLink(destination: MLAutoDownloadFiletransferSettingView()) { - HStack{ - Image(systemName: "square.and.arrow.down") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 20, height: 20) - Text("Media Upload & Download") - } - } - } - } - .navigationBarTitle("Privacy Settings") - .onAppear { - privacyDefaultDB.hasSeenPrivacySettings = true - } - } -} - -struct PrivacyScreen: View { - @ObservedObject var privacyDefaultDB = PrivacyDefaultDB() - - var body: some View { - Form { - Picker(selection: $privacyDefaultDB.notificationPrivacySetting, label: Text("Notification privacy")) { - ForEach(NotificationPrivacySettingOption.allCases, id: \.self) { option in - Text(getNotificationPrivacyOption(option)).tag(option.rawValue) - } - } - .frame(height: 56, alignment: .trailing) - Toggle(isOn: $privacyDefaultDB.omemoDefaultOn) { - Text("Enable encryption by default for new chats") - } - Toggle(isOn: $privacyDefaultDB.autodeleteAllMessagesAfter3Days) { - Text("Autodelete all messages after 3 days") - } - if #available(iOS 16.0, macCatalyst 16.0, *) { - Text("Use DNSSEC to validate all DNS query responses before connecting to the IP designated in the DNS response. While being more secure, this can lead to connection problems in certain networks like Hotel WiFi etc.") - Toggle(isOn: $privacyDefaultDB.useDnssecForAllConnections) { - Text("Use DNSSEC validation for all connections") - } - } - } - .navigationBarTitle("Privacy & security", displayMode: .inline) - } -} - -struct PublishingScreen: View { - @ObservedObject var privacyDefaultDB = PrivacyDefaultDB() - - var body: some View { - Form { - Section(header: Text("Publishing")) { - Toggle(isOn: $privacyDefaultDB.sendLastUserInteraction) { - Text("Send last interaction time") - } - Toggle(isOn: $privacyDefaultDB.sendLastChatState) { - Text("Send typing notifications") - } - Toggle(isOn: $privacyDefaultDB.sendReceivedMarkers) { - Text("Send message received state") - } - Toggle(isOn: $privacyDefaultDB.sendDisplayedMarkers) { - Text("Send message displayed state") - } - } - Section(header: Text("Appearance")) { - Toggle(isOn: $privacyDefaultDB.showKeyboardOnChatOpen) { - Text("Autofocus text input on chat open") - } - } - } - .navigationBarTitle("Publishing & Appearance", displayMode: .inline) - } -} - -struct PreviewsScreen: View { - @ObservedObject var privacyDefaultDB = PrivacyDefaultDB() - - var body: some View { - Form { - Toggle(isOn: $privacyDefaultDB.showGeoLocation) { - Text("Show inline geo location") - } - Toggle(isOn: $privacyDefaultDB.showURLPreview) { - Text("Show URL previews") - } - } - .navigationBarTitle("Previews", displayMode: .inline) - } -} - -struct CommunicationScreen: View { - @ObservedObject var privacyDefaultDB = PrivacyDefaultDB() - - var body: some View { - Form { - Toggle(isOn: $privacyDefaultDB.allowNonRosterContacts) { - Text("Allow contacts not in my contact list to contact me") - } - Toggle(isOn: $privacyDefaultDB.allowVersionIQ) { - Text("Allow approved contacts to query my Monal and iOS version") - } - Toggle(isOn: $privacyDefaultDB.allowCallsFromNonRosterContacts) { - Text("Calls: Allow contacts not in my contact list to call me") - } - Toggle(isOn: $privacyDefaultDB.webrtcAllowP2P) { - Text("Calls: Allow P2P sessions") - } - Toggle(isOn: $privacyDefaultDB.webrtcUseFallbackTurn) { - Text("Calls: Allow TURN fallback to Monal-Servers") - } - } - .navigationBarTitle("Communication", displayMode: .inline) - } -} - -struct MLAutoDownloadFiletransferSettingView: View { - @ObservedObject var privacyDefaultDB = PrivacyDefaultDB() - - var body: some View { - Form { - Section(header: Text("General File Transfer Settings")) { - Toggle(isOn: $privacyDefaultDB.autodownloadFiletransfers) { - Text("Auto-Download Media") - } - } - - Section(header: Text("Download Settings")) { - - Text("Adjust the maximum file size for auto-downloads over WiFi") - .foregroundColor(.secondary) - .font(.footnote) - Slider( - value: $privacyDefaultDB.autodownloadFiletransfersWifiMaxSize.bytecount(mappedTo: 1024*1024), - in: 1.0...100.0, - step: 1.0, - minimumValueLabel: Text("1 MiB"), - maximumValueLabel: Text("100 MiB"), - label: { - Text("Load over wifi") - } - ) - Text("Load over WiFi up to: \(UInt(privacyDefaultDB.autodownloadFiletransfersWifiMaxSize/(1024*1024))) MiB") - } - - Text("Adjust the maximum file size for auto-downloads over cellular network") - .foregroundColor(.secondary) - .font(.footnote) - Slider( - value: $privacyDefaultDB.autodownloadFiletransfersMobileMaxSize.bytecount(mappedTo: 1024*1024), - in: 0.0...100.0, - step: 1.0, - minimumValueLabel: Text("1 MiB"), - maximumValueLabel: Text("100 MiB"), - label: { - Text("Load over Cellular") - } - ) - Text("Load over cellular up to: \(Int(privacyDefaultDB.autodownloadFiletransfersMobileMaxSize/(1024*1024))) MiB") - - Section(header: Text("Upload Settings")) { - Text("Adjust the quality of images uploaded") - .foregroundColor(.secondary) - .font(.footnote) - Slider( - value: $privacyDefaultDB.imageUploadQuality, - in: 0.33...1.0, - step: 0.01, - minimumValueLabel: Text("33%"), - maximumValueLabel: Text("100%"), - label: { - Text("Upload Settings") - } - ) - Text("Image Upload Quality: \(String(format: "%.0f%%", privacyDefaultDB.imageUploadQuality*100))") - } - } - } -} - - -struct ContentView_Previews: PreviewProvider { - static var previews: some View { - PrivacySettings() - } -} diff --git a/Monal/Classes/SwiftuiHelpers.swift b/Monal/Classes/SwiftuiHelpers.swift index 4596d2f35a..afec677650 100644 --- a/Monal/Classes/SwiftuiHelpers.swift +++ b/Monal/Classes/SwiftuiHelpers.swift @@ -109,6 +109,25 @@ func mucAffiliationToString(_ affiliation: String?) -> String { return NSLocalizedString("", comment:"muc affiliation") } +@ViewBuilder +func buildNotificationStateLabel(_ description: Text, isWorking: Bool) -> some View { + if(isWorking == true) { + Label(title: { + description + }, icon: { + Image(systemName: "checkmark.seal") + .foregroundColor(.green) + }) + } else { + Label(title: { + description + }, icon: { + Image(systemName: "xmark.seal") + .foregroundColor(.red) + }) + } +} + //see here for some ideas used herein: https://blog.logrocket.com/adding-gifs-ios-app-flanimatedimage-swiftui/#using-flanimatedimage-with-swift struct GIFViewer: UIViewRepresentable { typealias UIViewType = FLAnimatedImageView @@ -483,19 +502,6 @@ class SwiftuiInterface : NSObject { return host } - @objc - func makeBackgroundSettings(_ contact: MLContact?) -> UIViewController { - let delegate = SheetDismisserProtocol() - let host = UIHostingController(rootView:AnyView(EmptyView())) - delegate.host = host - var contactArg:ObservableKVOWrapper? = nil; - if let contact = contact { - contactArg = ObservableKVOWrapper(contact) - } - host.rootView = AnyView(UIKitWorkaround(BackgroundSettings(contact:contactArg, delegate:delegate))) - return host - } - @objc func makeAddContactView(dismisser: @escaping (MLContact) -> ()) -> UIViewController { let delegate = SheetDismisserProtocol() @@ -520,8 +526,6 @@ class SwiftuiInterface : NSObject { let host = UIHostingController(rootView:AnyView(EmptyView())) delegate.host = host switch(name) { // TODO names are currently taken from the segue identifier, an enum would be nice once everything is ported to SwiftUI - case "NotificationSettings": - host.rootView = AnyView(UIKitWorkaround(NotificationSettings(delegate:delegate))) case "DebugView": host.rootView = AnyView(UIKitWorkaround(DebugView())) case "WelcomeLogIn": @@ -534,10 +538,12 @@ class SwiftuiInterface : NSObject { host.rootView = AnyView(AddTopLevelNavigation(withDelegate: delegate, to: CreateGroupMenu(delegate: delegate))) case "ChatPlaceholder": host.rootView = AnyView(ChatPlaceholder()) - case "PrivacySettings" : - host.rootView = AnyView(UIKitWorkaround(PrivacySettings())) + case "GeneralSettings" : + host.rootView = AnyView(UIKitWorkaround(GeneralSettings())) case "ActiveChatsPrivacySettings": host.rootView = AnyView(AddTopLevelNavigation(withDelegate: delegate, to: PrivacySettings())) + case "ActiveChatsNotificatioSettings": + host.rootView = AnyView(AddTopLevelNavigation(withDelegate: delegate, to: NotificationSettings())) default: unreachable() } diff --git a/Monal/Monal.xcodeproj/project.pbxproj b/Monal/Monal.xcodeproj/project.pbxproj index de395b13ad..50f3c39688 100644 --- a/Monal/Monal.xcodeproj/project.pbxproj +++ b/Monal/Monal.xcodeproj/project.pbxproj @@ -10,7 +10,7 @@ 08CAF17FA202CF3CB760D93C /* Pods_NotificationService.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2B7A5555D807EE78C95217FD /* Pods_NotificationService.framework */; }; 1D3623260D0F684500981E51 /* MonalAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 1D3623250D0F684500981E51 /* MonalAppDelegate.m */; }; 1D60589B0D05DD56006BFB54 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 29B97316FDCFA39411CA2CEA /* main.m */; }; - 20ED55852BADDA5C0005783E /* PrivacySettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20ED55842BADDA5C0005783E /* PrivacySettings.swift */; }; + 20ED55852BADDA5C0005783E /* GeneralSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20ED55842BADDA5C0005783E /* GeneralSettings.swift */; }; 2601D9CB0FBF25EF004DB939 /* sworim.sqlite in Resources */ = {isa = PBXBuildFile; fileRef = 2601D9CA0FBF25EF004DB939 /* sworim.sqlite */; }; 260773C4232FC4E800BFD50F /* NotificationService.m in Sources */ = {isa = PBXBuildFile; fileRef = 260773C3232FC4E800BFD50F /* NotificationService.m */; }; 2609B5291FD5B26800F09FA1 /* MLSplitViewDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 2609B5281FD5B26800F09FA1 /* MLSplitViewDelegate.m */; }; @@ -83,7 +83,7 @@ 38720923251EDE07001837EB /* MLXEPSlashMeHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 38720921251EDE07001837EB /* MLXEPSlashMeHandler.m */; }; 389E298C25E901CA009A5268 /* MLAudioRecoderManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 389E298925E901CA009A5268 /* MLAudioRecoderManager.m */; }; 389E298D25E901CA009A5268 /* MLAudioRecoderManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 389E298B25E901CA009A5268 /* MLAudioRecoderManager.h */; }; - 3D06A515281FFCC000DDAE90 /* NotificationSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D06A514281FFCC000DDAE90 /* NotificationSettings.swift */; }; + 3D06A515281FFCC000DDAE90 /* NotificationDebugging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D06A514281FFCC000DDAE90 /* NotificationDebugging.swift */; }; 3D27D956290B0BB60014748B /* AddContactMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D27D955290B0BB60014748B /* AddContactMenu.swift */; }; 3D27D958290B0BC80014748B /* ContactRequestsMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D27D957290B0BC80014748B /* ContactRequestsMenu.swift */; }; 3D5A91422842B4AE008CE57E /* MemberList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D5A91412842B4AE008CE57E /* MemberList.swift */; }; @@ -303,7 +303,7 @@ 1D3623240D0F684500981E51 /* MonalAppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = MonalAppDelegate.h; path = Classes/MonalAppDelegate.h; sourceTree = ""; }; 1D3623250D0F684500981E51 /* MonalAppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = MonalAppDelegate.m; path = Classes/MonalAppDelegate.m; sourceTree = ""; }; 1D46F251C198E3D8FA55692F /* Pods-Monal.appstore.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Monal.appstore.xcconfig"; path = "Target Support Files/Pods-Monal/Pods-Monal.appstore.xcconfig"; sourceTree = ""; }; - 20ED55842BADDA5C0005783E /* PrivacySettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacySettings.swift; sourceTree = ""; }; + 20ED55842BADDA5C0005783E /* GeneralSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralSettings.swift; sourceTree = ""; }; 213F5BFD4599EC9317B99E97 /* Pods-Monal.appstore-quicksy.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Monal.appstore-quicksy.xcconfig"; path = "Target Support Files/Pods-Monal/Pods-Monal.appstore-quicksy.xcconfig"; sourceTree = ""; }; 21E99538324C14220843F325 /* Pods-shareSheet.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-shareSheet.debug.xcconfig"; path = "Target Support Files/Pods-shareSheet/Pods-shareSheet.debug.xcconfig"; sourceTree = ""; }; 222F09C97CFF93A2CF1007F3 /* Pods-MonalUITests.alpha-ios.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MonalUITests.alpha-ios.xcconfig"; path = "Target Support Files/Pods-MonalUITests/Pods-MonalUITests.alpha-ios.xcconfig"; sourceTree = ""; }; @@ -499,7 +499,7 @@ 389E298B25E901CA009A5268 /* MLAudioRecoderManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MLAudioRecoderManager.h; sourceTree = ""; }; 39B989B9775C0725A810D271 /* Pods-MonalUITests.adhoc.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MonalUITests.adhoc.xcconfig"; path = "Target Support Files/Pods-MonalUITests/Pods-MonalUITests.adhoc.xcconfig"; sourceTree = ""; }; 39DB4C9159DA578D1A34990D /* Pods-monalxmpp.alpha.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-monalxmpp.alpha.xcconfig"; path = "Target Support Files/Pods-monalxmpp/Pods-monalxmpp.alpha.xcconfig"; sourceTree = ""; }; - 3D06A514281FFCC000DDAE90 /* NotificationSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettings.swift; sourceTree = ""; }; + 3D06A514281FFCC000DDAE90 /* NotificationDebugging.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationDebugging.swift; sourceTree = ""; }; 3D27D955290B0BB60014748B /* AddContactMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContactMenu.swift; sourceTree = ""; }; 3D27D957290B0BC80014748B /* ContactRequestsMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactRequestsMenu.swift; sourceTree = ""; }; 3D5A91412842B4AE008CE57E /* MemberList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberList.swift; sourceTree = ""; }; @@ -996,11 +996,11 @@ 2644D4981FF29E5600F46AB5 /* MLSettingsTableViewController.m */, 26B0CA8721AE2E3C0080B133 /* MLSoundsTableViewController.h */, 26B0CA8821AE2E3C0080B133 /* MLSoundsTableViewController.m */, - 3D06A514281FFCC000DDAE90 /* NotificationSettings.swift */, + 3D06A514281FFCC000DDAE90 /* NotificationDebugging.swift */, E8CF9CBF26249640001A1952 /* MLSettingsAboutViewController.h */, E8CF9CC026249640001A1952 /* MLSettingsAboutViewController.m */, 8441EFF82921B53500E851E9 /* BackgroundSettings.swift */, - 20ED55842BADDA5C0005783E /* PrivacySettings.swift */, + 20ED55842BADDA5C0005783E /* GeneralSettings.swift */, ); name = Settings; sourceTree = ""; @@ -2036,7 +2036,7 @@ 26158AF21FFA6E4500E53BDC /* MLWebViewController.m in Sources */, C1943A4C25309A9D0036172F /* MLReloadCell.m in Sources */, 262E51971AD8CB7200788351 /* MLTextInputCell.m in Sources */, - 20ED55852BADDA5C0005783E /* PrivacySettings.swift in Sources */, + 20ED55852BADDA5C0005783E /* GeneralSettings.swift in Sources */, 84E55E7D2964424E003E191A /* ActiveChatsViewController.m in Sources */, C1E8A7F72B8E47C300760220 /* EditGroupSubject.swift in Sources */, 263DFAC32187D0E00038E716 /* MLLinkCell.m in Sources */, @@ -2056,7 +2056,7 @@ 54F0B81928231691003664BD /* WelcomeLogIn.swift in Sources */, E8CF9CC726249640001A1952 /* MLSettingsAboutViewController.m in Sources */, C10490492612ED2F0054AC9E /* MLEmoji.swift in Sources */, - 3D06A515281FFCC000DDAE90 /* NotificationSettings.swift in Sources */, + 3D06A515281FFCC000DDAE90 /* NotificationDebugging.swift in Sources */, 845D636B2AD4AEDA0066EFFB /* ImageViewer.swift in Sources */, 2636C43F177BD58C001CA71F /* XMPPEdit.m in Sources */, 84FC375928981A5600634E3E /* PasswordMigration.swift in Sources */, From caecd6964c41643de85c954c7049d506f7f285ff Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Tue, 21 May 2024 04:08:51 +0200 Subject: [PATCH 047/143] Clean up some older code --- Monal/Classes/RegisterAccount.swift | 3 +-- Monal/Classes/SwiftHelpers.swift | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Monal/Classes/RegisterAccount.swift b/Monal/Classes/RegisterAccount.swift index 7df568471e..3d729949dc 100644 --- a/Monal/Classes/RegisterAccount.swift +++ b/Monal/Classes/RegisterAccount.swift @@ -74,8 +74,7 @@ struct RegisterAccount: View { self._username = State(wrappedValue:(registerData["username"] as? String) ?? "") self._registerToken = State(wrappedValue:registerData["token"] as? String) if let completion = registerData["completion"] { - //see https://stackoverflow.com/a/40592109/3528174 - self._completionHandler = State(wrappedValue:unsafeBitCast(completion, to:monal_id_block_t.self)) + self._completionHandler = State(wrappedValue:objcCast(completion) as monal_id_block_t) } DDLogVerbose("registerToken is now: \(String(describing:self.registerToken))") DDLogVerbose("Completion handler is now: \(String(describing:self.completionHandler))") diff --git a/Monal/Classes/SwiftHelpers.swift b/Monal/Classes/SwiftHelpers.swift index 4577e4879a..a2b0d39adb 100644 --- a/Monal/Classes/SwiftHelpers.swift +++ b/Monal/Classes/SwiftHelpers.swift @@ -28,6 +28,7 @@ let BGFETCH_DEFAULT_INTERVAL = HelperTools.getObjcDefinedValue(.BGFETCH_DEFAULT_ public typealias monal_void_block_t = @convention(block) () -> Void; public typealias monal_id_block_t = @convention(block) (AnyObject?) -> Void; +//see https://stackoverflow.com/a/40592109/3528174 public func objcCast(_ obj: Any) -> T { return unsafeBitCast(obj as AnyObject, to:T.self) } From f3b1e60b5dc205a1164bb46a2b79470ed540be6b Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Tue, 21 May 2024 04:09:02 +0200 Subject: [PATCH 048/143] Make sure privacy settings get displayed after first login/register --- Monal/Classes/RegisterAccount.swift | 8 ++++++++ Monal/Classes/WelcomeLogIn.swift | 14 ++++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/Monal/Classes/RegisterAccount.swift b/Monal/Classes/RegisterAccount.swift index 3d729949dc..4d3e449ece 100644 --- a/Monal/Classes/RegisterAccount.swift +++ b/Monal/Classes/RegisterAccount.swift @@ -387,6 +387,14 @@ struct RegisterAccount: View { Alert(title: alertPrompt.title, message: alertPrompt.message, dismissButton: .default(alertPrompt.dismissLabel, action: { if(self.registerComplete == true) { self.delegate.dismiss() + + if !HelperTools.defaultsDB().bool(forKey:"HasSeenPrivacySettings") { + let appDelegate = UIApplication.shared.delegate as! MonalAppDelegate + if let activeChats = appDelegate.activeChats { + activeChats.showPrivacySettings() + } + } + if let completion = self.completionHandler { DDLogVerbose("Calling reg completion handler...") completion(self.registeredAccountNo as NSNumber) diff --git a/Monal/Classes/WelcomeLogIn.swift b/Monal/Classes/WelcomeLogIn.swift index 0379ee5d73..d1c277a944 100644 --- a/Monal/Classes/WelcomeLogIn.swift +++ b/Monal/Classes/WelcomeLogIn.swift @@ -110,6 +110,16 @@ struct WelcomeLogIn: View { } } } + + private func dismissAndShowPrivacySettings() { + self.delegate.dismiss() + if !HelperTools.defaultsDB().bool(forKey:"HasSeenPrivacySettings") { + let appDelegate = UIApplication.shared.delegate as! MonalAppDelegate + if let activeChats = appDelegate.activeChats { + activeChats.showPrivacySettings() + } + } + } var body: some View { ScrollView { @@ -176,7 +186,7 @@ struct WelcomeLogIn: View { .alert(isPresented: $showAlert) { Alert(title: alertPrompt.title, message: alertPrompt.message, dismissButton: .default(alertPrompt.dismissLabel, action: { if(self.loginComplete == true) { - self.delegate.dismiss() + dismissAndShowPrivacySettings() } })) } @@ -215,7 +225,7 @@ struct WelcomeLogIn: View { if(DataLayer.sharedInstance().enabledAccountCnts() == 0) { Button(action: { - self.delegate.dismiss() + dismissAndShowPrivacySettings() }){ Text("Set up account later") .frame(maxWidth: .infinity) From 1bb2e0c1021c126ebb14295253359619d4047abf Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Tue, 21 May 2024 17:10:48 +0200 Subject: [PATCH 049/143] Fix some very rare race conditions on incoming calls --- Monal/Classes/MLVoIPProcessor.m | 8 +++++--- Monal/Classes/MonalAppDelegate.m | 6 +++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/Monal/Classes/MLVoIPProcessor.m b/Monal/Classes/MLVoIPProcessor.m index 36021c6131..4dc2d490ad 100644 --- a/Monal/Classes/MLVoIPProcessor.m +++ b/Monal/Classes/MLVoIPProcessor.m @@ -201,7 +201,7 @@ -(void) voipRegistration -(void) pushRegistry:(PKPushRegistry*) registry didUpdatePushCredentials:(PKPushCredentials*) credentials forType:(NSString*) type { NSString* token = [HelperTools stringFromToken:credentials.token]; - DDLogDebug(@"Ignoring APNS voip token string: %@", token); + DDLogDebug(@"Ignoring APNS voip token string for type %@: %@", type, token); } -(void) pushRegistry:(PKPushRegistry*) registry didInvalidatePushTokenForType:(NSString*) type @@ -332,14 +332,16 @@ -(void) processIncomingCall:(NSDictionary* _Nonnull) userInfo withCompletion:(vo //this will be done once the app delegate started to connect our xmpp accounts above //do this in an extra thread to not block this callback thread (could be main thread or otherwise restricted by apple) dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + DDLogDebug(@"Sending jmi ringing message..."); + [call sendJmiRinging]; + //wait for our account to connect before initializing webrtc using XEP-0215 iq stanzas //if the user proceeds the call before we are bound, the outgoing proceed message stanza will be queued and sent once we are bound //outgoing iq messages are not queued in all cases (e.g. non-smacks reconnect), hence this waiting loop while(call.account.accountState < kStateBound) [NSThread sleepForTimeInterval:0.250]; - DDLogDebug(@"Account is connected, now send jmi ringing message and really initialize WebRTC..."); - [call sendJmiRinging]; + DDLogDebug(@"Account is connected, now really initialize WebRTC..."); [self initWebRTCForPendingCall:call]; }); } diff --git a/Monal/Classes/MonalAppDelegate.m b/Monal/Classes/MonalAppDelegate.m index 81f3f6fbee..3a189b2412 100644 --- a/Monal/Classes/MonalAppDelegate.m +++ b/Monal/Classes/MonalAppDelegate.m @@ -387,9 +387,6 @@ -(BOOL) application:(UIApplication*) application willFinishLaunchingWithOptions: [[MLImageManager sharedInstance] cleanupHashes]; }); - //initialize callkit - _voipProcessor = [MLVoIPProcessor new]; - //only proceed with launching if the NotificationServiceExtension is *not* running if([MLProcessLock checkRemoteRunning:@"NotificationServiceExtension"]) { @@ -587,6 +584,9 @@ -(BOOL) application:(UIApplication*) application didFinishLaunchingWithOptions:( [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(windowHandling:) name:@"NSWindowDidResignKeyNotification" object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(windowHandling:) name:@"NSWindowDidBecomeKeyNotification" object:nil]; #endif + + //initialize callkit (mus be done after connectIfNecessary to make sure the list of accounts is already populated when a voip push comes in) + _voipProcessor = [MLVoIPProcessor new]; /* NSDictionary* options = launchOptions[UIApplicationLaunchOptionsUserActivityDictionaryKey]; From c0b2f55298df24730f65ca43a9815930258062f1 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Sat, 25 May 2024 00:26:45 +0200 Subject: [PATCH 050/143] Add new addTopRight view modifier to add a top-right overlay --- Monal/Classes/BackgroundSettings.swift | 37 +++++++++++++++++--------- Monal/Classes/SwiftuiHelpers.swift | 24 +++++++++++++++++ 2 files changed, 49 insertions(+), 12 deletions(-) diff --git a/Monal/Classes/BackgroundSettings.swift b/Monal/Classes/BackgroundSettings.swift index 87d100ae9a..81ce6d6900 100644 --- a/Monal/Classes/BackgroundSettings.swift +++ b/Monal/Classes/BackgroundSettings.swift @@ -33,8 +33,9 @@ struct BackgroundSettings: View { var body: some View { VStack { Form { - Group { - Section(header:title(contact:contact)) { + Section(header:title(contact:contact)) { + VStack(spacing: 20) { + Spacer().frame(height: 0) Button(action: { #if targetEnvironment(macCatalyst) let picker = DocumentPickerViewController( @@ -56,27 +57,39 @@ struct BackgroundSettings: View { #endif }) { if let inputImage = inputImage { - ZStack(alignment: .topLeading) { - HStack(alignment: .center) { - Image(uiImage:inputImage) - .resizable() - .aspectRatio(contentMode: .fit) - .frame(maxWidth: .infinity, alignment: .center) - } + HStack(alignment: .center) { + Image(uiImage:inputImage) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(maxWidth: .infinity, alignment: .center) + } + .addTopRight { Button(action: { self.inputImage = nil }, label: { - Image(systemName: "xmark.circle.fill").foregroundColor(.red) + Image(systemName: "xmark.circle.fill") + .resizable() + .frame(width: 32.0, height: 32.0) + .accessibilityLabel("Remove Background Image") + .applyClosure { view in + if #available(iOS 15, *) { + view + .symbolRenderingMode(.palette) + .foregroundStyle(.white, .red) + } else { + view.foregroundColor(.red) + } + } }) .buttonStyle(.borderless) - .offset(x: -7, y: -7) + .offset(x: 12, y: -12) } - .frame(maxWidth: .infinity, alignment: .center) } else { Text("Select background image") .frame(maxWidth: .infinity, alignment: .center) } } + .accessibilityLabel("Change Background Image") .sheet(isPresented:$showingImagePicker) { ImagePicker(image:$inputImage) } diff --git a/Monal/Classes/SwiftuiHelpers.swift b/Monal/Classes/SwiftuiHelpers.swift index afec677650..0403f62d86 100644 --- a/Monal/Classes/SwiftuiHelpers.swift +++ b/Monal/Classes/SwiftuiHelpers.swift @@ -109,6 +109,30 @@ func mucAffiliationToString(_ affiliation: String?) -> String { return NSLocalizedString("", comment:"muc affiliation") } +struct TopRight: ViewModifier { + let overlay: T + public func body(content: Content) -> some View { + ZStack(alignment: .topLeading) { + content + VStack { + HStack { + Spacer() + overlay + } + Spacer() + } + } + } +} +extension View { + func addTopRight(view overlayClosure: @autoclosure @escaping () -> T) -> some View { + modifier(TopRight(overlay:overlayClosure())) + } + func addTopRight(@ViewBuilder _ overlayClosure: @escaping () -> some View) -> some View { + modifier(TopRight(overlay:overlayClosure())) + } +} + @ViewBuilder func buildNotificationStateLabel(_ description: Text, isWorking: Bool) -> some View { if(isWorking == true) { From 5d01419ec4402b80ded1a85b29b524e23a0b9883 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Sat, 25 May 2024 00:28:26 +0200 Subject: [PATCH 051/143] Update muc contact details to allow removal of avatars This adds new addTopRight-powered overlay for add and delete, too. --- Monal/Classes/ContactDetails.swift | 122 ++++++++++++++++++++++++----- Monal/Classes/MLContact.h | 3 +- Monal/Classes/MLContact.m | 5 ++ Monal/Classes/MLImageManager.h | 1 + Monal/Classes/MLImageManager.m | 12 +++ 5 files changed, 121 insertions(+), 22 deletions(-) diff --git a/Monal/Classes/ContactDetails.swift b/Monal/Classes/ContactDetails.swift index ca42b7d17e..1e68cddf5c 100644 --- a/Monal/Classes/ContactDetails.swift +++ b/Monal/Classes/ContactDetails.swift @@ -12,6 +12,7 @@ struct ContactDetails: View { @State private var ownRole = "participant" @State private var ownAffiliation = "none" @StateObject var contact: ObservableKVOWrapper + @State private var showingRemoveAvatarConfirmation = false @State private var showingBlockContactConfirmation = false @State private var showingCannotBlockAlert = false @State private var showingRemoveContactConfirmation = false @@ -57,7 +58,41 @@ struct ContactDetails: View { alertPrompt.title = title alertPrompt.message = message showAlert = true - self.success = true // < dismiss entire view on close + success = true // < dismiss entire view on close + } + + private func performAction(_ title: Text, action: @escaping ()->Void) { + self.account.mucProcessor.addUIHandler({_data in let data = _data as! NSDictionary + DispatchQueue.main.async { + hideLoadingOverlay(self.overlay) + let success : Bool = data["success"] as! Bool; + if !success { + errorAlert(title: title, message: Text(data["errorMessage"] as? String ?? "Unknown error!")) + } + } + }, forMuc:self.contact.contactJid) + action() + } + + 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 { @@ -72,24 +107,50 @@ struct ContactDetails: View { if ownAffiliation == "owner" { view.accessibilityLabel((contact.mucType == "group") ? "Change Group Avatar" : "Change Channel Avatar") .onTapGesture { -#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 + showImagePicker() + } + .addTopRight { + if contact.hasAvatar { + Button(action: { + showingRemoveAvatarConfirmation = true + }, label: { + Image(systemName: "xmark.circle.fill") + .resizable() + .frame(width: 24.0, height: 24.0) + .accessibilityLabel((contact.mucType == "group") ? "Remove Group Avatar" : "Remove Channel Avatar") + .applyClosure { view in + if #available(iOS 15, *) { + view + .symbolRenderingMode(.palette) + .foregroundStyle(.white, .red) + } else { + view.foregroundColor(.red) + } } - } - }, - onDismiss: { - //do nothing on dismiss - } - ) - UIApplication.shared.windows.first?.rootViewController?.present(picker, animated: true) -#else - showingImagePicker = true -#endif + }) + .buttonStyle(.borderless) + .offset(x: 8, y: -8) + } else { + Button(action: { + showImagePicker() + }, label: { + Image(systemName: "pencil.circle.fill") + .resizable() + .frame(width: 24.0, height: 24.0) + .accessibilityLabel((contact.mucType == "group") ? "Change Group Avatar" : "Change Channel Avatar") +// .applyClosure { view in +// if #available(iOS 15, *) { +// view +// .symbolRenderingMode(.palette) +// .foregroundStyle(.primary, .secondary) +// } else { +// view.foregroundColor(.primary) +// } +// } + }) + .buttonStyle(.borderless) + .offset(x: 8, y: -8) + } } } else { view.accessibilityLabel((contact.mucType == "group") ? "Group Avatar" : "Channel Avatar") @@ -103,7 +164,24 @@ struct ContactDetails: View { .sheet(isPresented:$showingImagePicker) { ImagePicker(image:$inputImage) } - + .actionSheet(isPresented: $showingRemoveAvatarConfirmation) { + ActionSheet( + title: Text("Really remove avatar?"), + message: Text("This will remove the current avatar image and revert this group/channel to the default one."), + buttons: [ + .cancel(), + .destructive( + Text("Yes"), + action: { + showLoadingOverlay(overlay, headline: NSLocalizedString("Removing avatar...", comment: "")) + performAction(Text("Error removing avatar!")) { + self.account.mucProcessor.publishAvatar(nil, forMuc: contact.contactJid) + } + } + ) + ] + ) + } Button { UIPasteboard.general.setValue(contact.contactJid as String, forPasteboardType:UTType.utf8PlainText.identifier as String) @@ -580,8 +658,10 @@ struct ContactDetails: View { })) } .onChange(of:inputImage) { _ in - showLoadingOverlay(overlay, headline: NSLocalizedString("Uploading image...", comment: "")) - self.account.mucProcessor.publishAvatar(inputImage, forMuc: contact.contactJid) + showLoadingOverlay(overlay, headline: NSLocalizedString("Uploading avatar...", comment: "")) + performAction(Text("Error changing avatar!")) { + self.account.mucProcessor.publishAvatar(inputImage, forMuc: contact.contactJid) + } } .onChange(of:contact.avatar as UIImage) { _ in hideLoadingOverlay(overlay) diff --git a/Monal/Classes/MLContact.h b/Monal/Classes/MLContact.h index 6741dfb9f1..80a2b90b11 100644 --- a/Monal/Classes/MLContact.h +++ b/Monal/Classes/MLContact.h @@ -50,7 +50,8 @@ FOUNDATION_EXPORT NSString* const kAskSubscribe; */ @property (nonatomic, readonly) NSNumber* accountId; @property (nonatomic, readonly) NSString* contactJid; -@property (nonatomic, copy) UIImage* avatar; +@property (nonatomic, readonly, copy) UIImage* avatar; +@property (nonatomic, readonly) BOOL hasAvatar; @property (nonatomic, readonly) NSString* fullName; /** usually user assigned nick name diff --git a/Monal/Classes/MLContact.m b/Monal/Classes/MLContact.m index e84cb20804..fffaaea2e1 100644 --- a/Monal/Classes/MLContact.m +++ b/Monal/Classes/MLContact.m @@ -488,6 +488,11 @@ -(void) setAvatar:(UIImage*) avatar _avatar = [UIImage new]; //empty dummy image, to not save nil (should never happen, MLImageManager has default images) } +-(BOOL) hasAvatar +{ + return [[MLImageManager sharedInstance] hasIconForContact:self]; +} + -(BOOL) isSelfChat { xmpp* account = [[MLXMPPManager sharedInstance] getConnectedAccountForID:self.accountId]; diff --git a/Monal/Classes/MLImageManager.h b/Monal/Classes/MLImageManager.h index d4016ad77b..a41b060436 100644 --- a/Monal/Classes/MLImageManager.h +++ b/Monal/Classes/MLImageManager.h @@ -35,6 +35,7 @@ /** retrieves a uiimage for the icon. returns noicon.png if nothing is found. never returns nil. */ +-(BOOL) hasIconForContact:(MLContact* _Nonnull) contact; -(UIImage* _Nullable) getIconForContact:(MLContact* _Nonnull) contact withCompletion:(void (^_Nullable)(UIImage *_Nullable))completion; -(UIImage* _Nullable) getIconForContact:(MLContact* _Nonnull) contact; +(UIImage* _Nonnull) circularImage:(UIImage* _Nonnull) image; diff --git a/Monal/Classes/MLImageManager.m b/Monal/Classes/MLImageManager.m index 97cc9543d4..07215a3288 100644 --- a/Monal/Classes/MLImageManager.m +++ b/Monal/Classes/MLImageManager.m @@ -262,6 +262,18 @@ -(void) setIconForContact:(MLContact*) contact WithData:(NSData* _Nullable) data } +-(BOOL) hasIconForContact:(MLContact*) contact +{ + NSString* filename = [self fileNameforContact:contact]; + + NSString* writablePath = [self.documentsDirectory stringByAppendingPathComponent:@"buddyicons"]; + writablePath = [writablePath stringByAppendingPathComponent:contact.accountId.stringValue]; + writablePath = [writablePath stringByAppendingPathComponent:filename]; + + DDLogVerbose(@"Checking avatar image at: %@", writablePath); + return [UIImage imageWithContentsOfFile:writablePath] != nil; +} + -(UIImage*) getIconForContact:(MLContact*) contact { return [self getIconForContact:contact withCompletion:nil]; From faf7e0fa434ce131369f5d11e98944bce59968aa Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Fri, 3 May 2024 02:19:53 +0200 Subject: [PATCH 052/143] --- 901 --- 6.3.1b1 From 67f96c27cd52fdee41749fa630147dfcda2a09f1 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Fri, 3 May 2024 02:19:21 +0200 Subject: [PATCH 053/143] Make sure our localization update runs on a clean checkout --- .github/workflows/beta.build-push.yml | 29 +++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/.github/workflows/beta.build-push.yml b/.github/workflows/beta.build-push.yml index 3ecabc36b4..f0c49a4117 100644 --- a/.github/workflows/beta.build-push.yml +++ b/.github/workflows/beta.build-push.yml @@ -13,7 +13,7 @@ on: jobs: # This workflow contains a single job called "build" buildAndPublishBeta: - # The type of runner that the job will run on + name: "Build and Publish Beta Release" runs-on: self-hosted env: APP_NAME: "Monal" @@ -70,11 +70,6 @@ jobs: run: ./scripts/uploadNonAlpha.sh beta - name: Publish catalyst to appstore connect run: xcrun altool --upload-app --file ./Monal/build/app/Monal.pkg --type macos --asc-provider S8D843U34Y -u "$(cat /Users/ci/apple_connect_upload_mail.txt)" -p "$(cat /Users/ci/apple_connect_upload_secret.txt)" --primary-bundle-id org.monal-im.prod.catalyst.monal - - name: Update translations - run: | - chmod +x ./scripts/updateLocalization.sh - chmod +x ./scripts/xliff_extractor.py - ./scripts/updateLocalization.sh BUILDSERVER - uses: actions/upload-artifact@v4 with: name: monal-catalyst @@ -95,3 +90,25 @@ jobs: name: monal-ios-dsym path: Monal/build/ios_Monal.xcarchive/dSYMs if-no-files-found: error + + updateTranslations: + name: Update Translations using Beta-Branch + runs-on: self-hosted + needs: [buildAndPublishBeta] + env: + APP_NAME: "Monal" + APP_DIR: "Monal.app" + BUILD_TYPE: "Beta" + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + - uses: actions/checkout@v4 + with: + clean: true + submodules: true + - name: Checkout submodules + run: git submodule update -f --init --remote + - name: Update translations + run: | + chmod +x ./scripts/updateLocalization.sh + chmod +x ./scripts/xliff_extractor.py + ./scripts/updateLocalization.sh BUILDSERVER From b0adb86292997f2a3ab9944c869f895085caa49d Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Sat, 4 May 2024 04:49:36 +0200 Subject: [PATCH 054/143] Better handling of objc block typedefs in swift --- Monal/Classes/MLConstants.h | 18 +++++++++++++----- Monal/Classes/SwiftHelpers.swift | 5 +++++ 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/Monal/Classes/MLConstants.h b/Monal/Classes/MLConstants.h index 9abd0df732..f74fd3d8bc 100644 --- a/Monal/Classes/MLConstants.h +++ b/Monal/Classes/MLConstants.h @@ -55,14 +55,22 @@ static const DDLogLevel ddLogLevel = LOG_LEVEL_STDOUT; #define BGFETCH_DEFAULT_INTERVAL 3600*3 #endif +// #define defineBlockType(name, returntype, ...) \ +// typedef returntype (^name)(__VA_ARGS__); \ +// name _Nonnull castTo_##name(id _Nonnull block) { return block; } +// +// #ifndef blocktypes +// defineBlockType(monal_new_void_block_t, void, void); +// #endif + @class MLContact; //some typedefs used throughout the project -typedef void (^contactCompletion)(MLContact* _Nonnull selectedContact); -typedef void (^accountCompletion)(NSInteger accountRow); -typedef void (^monal_void_block_t)(void); -typedef void (^monal_id_block_t)(id _Nonnull); -typedef void (^monal_upload_completion_t)(NSString* _Nullable url, NSString* _Nullable mimeType, NSNumber* _Nullable size, NSError* _Nullable error); +typedef void (^contactCompletion)(MLContact* _Nonnull selectedContact) NS_SWIFT_UNAVAILABLE("To be redefined in swift."); +typedef void (^accountCompletion)(NSInteger accountRow) NS_SWIFT_UNAVAILABLE("To be redefined in swift."); +typedef void (^monal_void_block_t)(void) NS_SWIFT_UNAVAILABLE("To be redefined in swift."); +typedef void (^monal_id_block_t)(id _Nonnull) NS_SWIFT_UNAVAILABLE("To be redefined in swift."); +typedef void (^monal_upload_completion_t)(NSString* _Nullable url, NSString* _Nullable mimeType, NSNumber* _Nullable size, NSError* _Nullable error) NS_SWIFT_UNAVAILABLE("To be redefined in swift."); typedef NS_ENUM(NSUInteger, MLAudioState) { MLAudioStateNormal, diff --git a/Monal/Classes/SwiftHelpers.swift b/Monal/Classes/SwiftHelpers.swift index ffd0887d55..fe9a382492 100644 --- a/Monal/Classes/SwiftHelpers.swift +++ b/Monal/Classes/SwiftHelpers.swift @@ -28,6 +28,10 @@ let BGFETCH_DEFAULT_INTERVAL = HelperTools.getObjcDefinedValue(.BGFETCH_DEFAULT_ public typealias monal_void_block_t = @convention(block) () -> Void; public typealias monal_id_block_t = @convention(block) (AnyObject?) -> Void; +public func objcCast(_ obj: Any) -> T { + return unsafeBitCast(obj as AnyObject, to:T.self) +} + public func unreachable(_ text: String = "unreachable", _ auxData: [String:AnyObject] = [String:AnyObject](), file: String = #file, line: Int = #line, function: String = #function) -> Never { DDLogError("unreachable: \(file) \(line) \(function)") HelperTools.mlAssert(withText:text, andUserData:auxData, andFile:(file as NSString).utf8String!, andLine:Int32(line), andFunc:(function as NSString).utf8String!) @@ -226,6 +230,7 @@ public struct defaultsDB { } } + @objcMembers public class SwiftHelpers: NSObject { public static func initSwiftHelpers() { From bf4e522a8432b8d7bad01dda33608b943c617b69 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Fri, 3 May 2024 01:17:57 +0200 Subject: [PATCH 055/143] Implement MUC destroy --- Monal/Classes/GroupDetailsEdit.swift | 84 +++++++++++++++-- Monal/Classes/MLMucProcessor.h | 1 + Monal/Classes/MLMucProcessor.m | 129 ++++++++++++++++++++++----- 3 files changed, 188 insertions(+), 26 deletions(-) diff --git a/Monal/Classes/GroupDetailsEdit.swift b/Monal/Classes/GroupDetailsEdit.swift index 36fb1b39b9..5c47713ea1 100644 --- a/Monal/Classes/GroupDetailsEdit.swift +++ b/Monal/Classes/GroupDetailsEdit.swift @@ -6,7 +6,6 @@ // Copyright © 2024 monal-im.org. All rights reserved. // -import SwiftUI import _PhotosUI_SwiftUI struct GroupDetailsEdit: View { @@ -15,10 +14,28 @@ struct GroupDetailsEdit: View { @State private var showingSheetEditSubject = false @State private var inputImage: UIImage? @State private var showingImagePicker = false + @State private var showingDestroyConfirmation = false + @State private var alertPrompt = AlertPrompt(dismissLabel: Text("Close")) + @State private var showAlert = false + @State private var success = false @StateObject private var overlay = LoadingOverlayState() + @State private var successCallback: monal_void_block_t? private let account: xmpp private let ownAffiliation: String? + private func errorAlert(title: Text, message: Text = Text("")) { + alertPrompt.title = title + alertPrompt.message = message + showAlert = true + } + + private func successAlert(title: Text, message: Text = Text("")) { + alertPrompt.title = title + alertPrompt.message = message + showAlert = true + self.success = true // < dismiss entire view on close + } + init(contact: ObservableKVOWrapper, ownAffiliation: String?) { MLAssert(contact.isGroup) @@ -37,7 +54,7 @@ struct GroupDetailsEdit: View { Image(uiImage: contact.avatar) .resizable() .scaledToFit() - .accessibilityLabel((contact.obj.mucType == "group") ? "Group Avatar" : "Channel Avatar") + .accessibilityLabel((contact.mucType == "group") ? "Group Avatar" : "Channel Avatar") .frame(width: 150, height: 150, alignment: .center) .shadow(radius: 7) .onTapGesture { @@ -50,7 +67,7 @@ struct GroupDetailsEdit: View { } } } - + Section { if ownAffiliation == "owner" { Button(action: { @@ -72,7 +89,7 @@ struct GroupDetailsEdit: View { }) { HStack { Image(systemName: "pencil") - if contact.obj.mucType == "group" { + if contact.mucType == "group" { Text("Group description") } else { Text("Channel description") @@ -84,9 +101,66 @@ struct GroupDetailsEdit: View { LazyClosureView(EditGroupSubject(contact: contact)) } } + + if ownAffiliation == "owner" { + Section { + Button(action: { + showingDestroyConfirmation = true + }) { + if contact.mucType == "group" { + Text("Destroy Group").foregroundColor(.red) + } else { + Text("Destroy Channel").foregroundColor(.red) + } + } + .actionSheet(isPresented: $showingDestroyConfirmation) { + ActionSheet( + title: contact.mucType == "group" ? Text("Destroy Group") : Text("Destroy Channel"), + message: contact.mucType == "group" ? Text("Do you really want to destroy this group? Every member will be kicked out and it will be destroyed afterwards.") : Text("Do you really want to destroy this channel? Every member will be kicked out and it will be destroyed afterwards."), + buttons: [ + .cancel(), + .destructive( + Text("Yes"), + action: { + showLoadingOverlay(overlay, headline: contact.mucType == "group" ? NSLocalizedString("Destroying group...", comment: "") : NSLocalizedString("Destroying channel...", comment: "")) + self.account.mucProcessor.destroyRoom(contact.contactJid as String) + self.account.mucProcessor.addUIHandler({_data in let data = _data as! NSDictionary + hideLoadingOverlay(overlay) + let success : Bool = data["success"] as! Bool; + if success { + if let callback = data["callback"] { + self.successCallback = objcCast(callback) as monal_void_block_t + } + DDLogError("callback: \(String(describing:self.successCallback))") + successAlert(title: Text("Success"), message: contact.mucType == "group" ? Text("Successfully destroyed group.") : Text("Successfully destroyed channel.")) + } else { + errorAlert(title: Text("Error destroying group!"), message: Text(data["errorMessage"] as! String)) + } + }, forMuc:contact.contactJid) + } + ) + ] + ) + } + } + } } .addLoadingOverlay(overlay) - .navigationTitle((contact.obj.mucType == "group") ? "Edit group" : "Edit channel") + .navigationTitle((contact.mucType == "group") ? NSLocalizedString("Edit group", comment: "") : NSLocalizedString("Edit channel", comment: "")) + .alert(isPresented: $showAlert) { + Alert(title: alertPrompt.title, message: alertPrompt.message, dismissButton:.default(Text("Close"), action: { + showAlert = false + if self.success == true { + //close muc ui and leave chat ui of this muc + if let callback = self.successCallback { + callback() + } + if let activeChats = (UIApplication.shared.delegate as! MonalAppDelegate).activeChats { + activeChats.presentChat(with:nil) + } + } + })) + } .onChange(of:inputImage) { _ in showLoadingOverlay(overlay, headline: NSLocalizedString("Uploading image...", comment: "")) self.account.mucProcessor.publishAvatar(inputImage, forMuc: contact.contactJid) diff --git a/Monal/Classes/MLMucProcessor.h b/Monal/Classes/MLMucProcessor.h index f3d82f8f65..d8fd57c84b 100644 --- a/Monal/Classes/MLMucProcessor.h +++ b/Monal/Classes/MLMucProcessor.h @@ -28,6 +28,7 @@ NS_ASSUME_NONNULL_BEGIN //muc management methods -(NSString* _Nullable) generateMucJid; -(NSString* _Nullable) createGroup:(NSString*) room; +-(void) destroyRoom:(NSString*) room; -(void) changeNameOfMuc:(NSString*) room to:(NSString*) name; -(void) changeSubjectOfMuc:(NSString*) room to:(NSString*) subject; -(void) publishAvatar:(UIImage* _Nullable) image forMuc:(NSString*) room; diff --git a/Monal/Classes/MLMucProcessor.m b/Monal/Classes/MLMucProcessor.m index 1d67680c2d..667f36b3ef 100644 --- a/Monal/Classes/MLMucProcessor.m +++ b/Monal/Classes/MLMucProcessor.m @@ -23,7 +23,7 @@ #import "MLOMEMO.h" #import "MLImageManager.h" -#define CURRENT_MUC_STATE_VERSION @7 +#define CURRENT_MUC_STATE_VERSION @8 @interface MLMucProcessor() { @@ -33,6 +33,7 @@ @interface MLMucProcessor() NSMutableDictionary* _roomFeatures; NSMutableDictionary* _creating; NSMutableDictionary* _joining; + NSMutableSet* _destroying; NSMutableSet* _firstJoin; NSDate* _lastPing; NSMutableSet* _noUpdateBookmarks; @@ -75,6 +76,7 @@ -(id) initWithAccount:(xmpp*) account _roomFeatures = [NSMutableDictionary new]; _creating = [NSMutableDictionary new]; _joining = [NSMutableDictionary new]; + _destroying = [NSMutableSet new]; _firstJoin = [NSMutableSet new]; _uiHandler = [NSMutableDictionary new]; _lastPing = [NSDate date]; @@ -107,6 +109,7 @@ -(void) setInternalState:(NSDictionary*) state _roomFeatures = [state[@"roomFeatures"] mutableCopy]; _creating = [state[@"creating"] mutableCopy]; _joining = [state[@"joining"] mutableCopy]; + _destroying = [state[@"destroying"] mutableCopy]; _firstJoin = [state[@"firstJoin"] mutableCopy]; _lastPing = state[@"lastPing"]; _noUpdateBookmarks = [state[@"noUpdateBookmarks"] mutableCopy]; @@ -122,6 +125,7 @@ -(NSDictionary*) getInternalState @"roomFeatures": [_roomFeatures copy], @"creating": [_creating copy], @"joining": [_joining copy], + @"destroying": [_destroying copy], @"firstJoin": [_firstJoin copy], @"lastPing": _lastPing, @"noUpdateBookmarks": [_noUpdateBookmarks copy], @@ -148,6 +152,7 @@ -(void) handleResourceBound:(NSNotification*) notification NSDictionary* creatingCopy = [_creating copy]; for(NSString* room in creatingCopy) [self removeRoomFromCreating:room]; + _destroying = [NSMutableSet new]; //don't clear _firstJoin and _noUpdateBookmarks to make sure half-joined mucs are still added to muc bookmarks @@ -291,25 +296,35 @@ -(void) processPresence:(XMPPPresence*) presenceNode { DDLogVerbose(@"Got muc presence from full jid: %@", presenceNode.from); - //extract info if present (use an empty dict if no info is present) - NSMutableDictionary* item = [[presenceNode findFirst:@"{http://jabber.org/protocol/muc#user}x/item@@"] mutableCopy]; - if(!item) - item = [NSMutableDictionary new]; - - //update jid to be a bare jid and add muc nick to our dict - if(item[@"jid"]) - item[@"jid"] = [HelperTools splitJid:item[@"jid"]][@"user"]; - item[@"nick"] = presenceNode.fromResource; - - //handle participant updates - if([presenceNode check:@"/"] || item[@"affiliation"] == nil) - [[DataLayer sharedInstance] removeParticipant:item fromMuc:presenceNode.fromUser forAccountId:_account.accountNo]; + //don't handle this error if we ourselves are destroying this room + BOOL isDestroying = NO; + @synchronized(_stateLockObject) { + isDestroying = [_destroying containsObject:presenceNode.fromUser]; + } + if(!isDestroying) + { + //extract info if present (use an empty dict if no info is present) + NSMutableDictionary* item = [[presenceNode findFirst:@"{http://jabber.org/protocol/muc#user}x/item@@"] mutableCopy]; + if(!item) + item = [NSMutableDictionary new]; + + //update jid to be a bare jid and add muc nick to our dict + if(item[@"jid"]) + item[@"jid"] = [HelperTools splitJid:item[@"jid"]][@"user"]; + item[@"nick"] = presenceNode.fromResource; + + //handle participant updates + if([presenceNode check:@"/"] || item[@"affiliation"] == nil) + [[DataLayer sharedInstance] removeParticipant:item fromMuc:presenceNode.fromUser forAccountId:_account.accountNo]; + else + [[DataLayer sharedInstance] addParticipant:item toMuc:presenceNode.fromUser forAccountId:_account.accountNo]; + + //handle members updates + if(item[@"jid"] != nil) + [self handleMembersListUpdate:[presenceNode find:@"{http://jabber.org/protocol/muc#user}x/item@@"] forMuc:presenceNode.fromUser]; + } else - [[DataLayer sharedInstance] addParticipant:item toMuc:presenceNode.fromUser forAccountId:_account.accountNo]; - - //handle members updates - if(item[@"jid"] != nil) - [self handleMembersListUpdate:[presenceNode find:@"{http://jabber.org/protocol/muc#user}x/item@@"] forMuc:presenceNode.fromUser]; + DDLogDebug(@"Ignoring unavailable presences of room being destroyed by us..."); //handle muc status codes in reflected presences //this MUST be done after the above code to make sure the db correctly reflects our membership/participant status @@ -712,8 +727,16 @@ -(void) handleStatusCodes:(XMPPStanza*) node //(normally these have an additional status code that was already handled in the switch statement above if([node check:@"//{http://jabber.org/protocol/muc#user}x/destroy"]) { - [self handleError:[NSString stringWithFormat:NSLocalizedString(@"Group/Channel got destroyed: %@", @""), node.fromUser] forMuc:node.fromUser withNode:node andIsSevere:YES]; - [self deleteMuc:node.fromUser withBookmarksUpdate:YES keepBuddylistEntry:YES]; + //don't handle this error if we ourselves are destroying this room + BOOL isDestroying = NO; + @synchronized(_stateLockObject) { + isDestroying = [_destroying containsObject:node.fromUser]; + } + if(!isDestroying) + { + [self handleError:[NSString stringWithFormat:NSLocalizedString(@"Group/Channel got destroyed: %@", @""), node.fromUser] forMuc:node.fromUser withNode:node andIsSevere:YES]; + [self deleteMuc:node.fromUser withBookmarksUpdate:YES keepBuddylistEntry:YES]; + } } } else @@ -909,6 +932,70 @@ -(NSString* _Nullable) createGroup:(NSString*) room return room; } +-(void) destroyRoom:(NSString*) room +{ + MLAssert([[DataLayer sharedInstance] isBuddyMuc:room forAccount:_account.accountNo], @"Cannot destroy non-muc!", (@{@"room": room})); + + @synchronized(_stateLockObject) { + [_destroying addObject:room]; + } + + XMPPIQ* iqNode = [[XMPPIQ alloc] initWithType:kiqSetType to:room]; + [iqNode addChildNode:[[MLXMLNode alloc] initWithElement:@"query" andNamespace:@"http://jabber.org/protocol/muc#owner" withAttributes:@{} andChildren:@[ + [[MLXMLNode alloc] initWithElement:@"destroy" withAttributes:@{} andChildren:@[ + [[MLXMLNode alloc] initWithElement:@"reason" andData:@"Groupchat got destroyed"] + ] andData:nil], + ] andData:nil]]; + [_account sendIq:iqNode withHandler:$newHandlerWithInvalidation(self, handleRoomDestroyResult, handleRoomDestroyResultInvalidation, $ID(room))]; +} + +$$instance_handler(handleRoomDestroyResultInvalidation, account.mucProcessor, $$ID(xmpp*, account), $$ID(NSString*, room)) + DDLogError(@"Could not destroy room '%@' on account %@: invalidation called", room, account); + @synchronized(_stateLockObject) { + [_destroying removeObject:room]; + } + [self handleError:[NSString stringWithFormat:NSLocalizedString(@"Failed to destroy group/channel '%@': timeout", @""), room] forMuc:room withNode:nil andIsSevere:YES]; +$$ + +$$instance_handler(handleRoomDestroyResult, account.mucProcessor, $$ID(xmpp*, account), $$ID(XMPPIQ*, iqNode), $$ID(NSString*, room)) + @synchronized(_stateLockObject) { + [_destroying removeObject:room]; + } + if([iqNode check:@"/"]) + { + DDLogError(@"Failed to destroy room '%@' on account %@: %@", room, account, [iqNode findFirst:@"error"]); + [self handleError:[NSString stringWithFormat:NSLocalizedString(@"Failed to destroy group/channel '%@'", @""), room] forMuc:room withNode:iqNode andIsSevere:YES]; + return; + } + + DDLogInfo(@"Successfully destroyed room '%@' on account %@", room, account); + monal_id_block_t uiHandler = [self getUIHandlerForMuc:room]; + if(uiHandler) + { + //remove handler (it will only be called once) + [self removeUIHandlerForMuc:room]; + + DDLogInfo(@"Calling UI handler for muc %@...", room); + dispatch_async(dispatch_get_main_queue(), ^{ + uiHandler(@{ + @"success": @YES, + @"muc": room, + @"account": self->_account, + @"callback": ^{ + //don't even keep our bookmark in this case + [self deleteMuc:room withBookmarksUpdate:YES keepBuddylistEntry:NO]; + }, + }); + }); + } + else + { + //don't even keep our bookmark in this case + //this will handled by the ui handler callback if the ui was used to destroy this room and must be handled here otherwise + [self deleteMuc:room withBookmarksUpdate:YES keepBuddylistEntry:NO]; + } +$$ + -(void) join:(NSString*) room { [self sendDiscoQueryFor:room withJoin:YES andBookmarksUpdate:YES]; From dde4d1fcc91778a4c8391cab6c050b59e1932564 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Sat, 4 May 2024 04:51:52 +0200 Subject: [PATCH 056/143] Simplify addUIHandler() callbacks --- Monal/Classes/AddContactMenu.swift | 6 +++--- Monal/Classes/CreateGroupMenu.swift | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Monal/Classes/AddContactMenu.swift b/Monal/Classes/AddContactMenu.swift index 313570b9d8..108b8a0368 100644 --- a/Monal/Classes/AddContactMenu.swift +++ b/Monal/Classes/AddContactMenu.swift @@ -146,14 +146,14 @@ struct AddContactMenu: View { 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" { showLoadingOverlay(overlay, headline: NSLocalizedString("Adding Group/Channel...", comment: "")) - account.mucProcessor.addUIHandler({data in - let success : Bool = (data as! NSDictionary)["success"] as! Bool; + account.mucProcessor.addUIHandler({_data in let data = _data as! NSDictionary + let success : Bool = data["success"] as! Bool; hideLoadingOverlay(overlay) if success { self.newContact = MLContact.createContact(fromJid: jid, andAccountNo: account.accountNo) successAlert(title: Text("Success!"), message: Text(String.localizedStringWithFormat("Successfully joined group/channel %@!", jid))) } else { - errorAlert(title: Text("Error entering group/channel!"), message: Text((data as! NSDictionary)["errorMessage"] as! String)) + errorAlert(title: Text("Error entering group/channel!"), message: Text(data["errorMessage"] as! String)) } }, forMuc: jid) account.joinMuc(jid) diff --git a/Monal/Classes/CreateGroupMenu.swift b/Monal/Classes/CreateGroupMenu.swift index 1c99b7474c..aa3fa5b102 100644 --- a/Monal/Classes/CreateGroupMenu.swift +++ b/Monal/Classes/CreateGroupMenu.swift @@ -86,8 +86,8 @@ struct CreateGroupMenu: View { } return } - self.selectedAccount!.mucProcessor.addUIHandler({data in - let success : Bool = (data as! NSDictionary)["success"] as! Bool; + self.selectedAccount!.mucProcessor.addUIHandler({_data in let data = _data as! NSDictionary + let success : Bool = data["success"] as! Bool; if success { self.selectedAccount!.mucProcessor.changeName(ofMuc: roomJid, to: self.groupName) for user in self.selectedContacts { @@ -102,7 +102,7 @@ struct CreateGroupMenu: View { } } else { hideLoadingOverlay(overlay) - errorAlert(title: Text("Error creating group!"), message: Text((data as! NSDictionary)["errorMessage"] as! String)) + errorAlert(title: Text("Error creating group!"), message: Text(data["errorMessage"] as! String)) } }, forMuc: roomJid) }, label: { From a859aedb79e98f7bb80ac8b382c1c2a0196553ba Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Sat, 4 May 2024 06:55:26 +0200 Subject: [PATCH 057/143] Fix members list to also invite new members to muc --- Monal/Classes/MemberList.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Monal/Classes/MemberList.swift b/Monal/Classes/MemberList.swift index 472e10a80a..c0d7c4e1d4 100644 --- a/Monal/Classes/MemberList.swift +++ b/Monal/Classes/MemberList.swift @@ -109,6 +109,7 @@ struct MemberList: View { if !previousMemberList.contains(member) { // add selected group member with affiliation member affiliationChangeAction(member, affiliation: "member") + self.account.mucProcessor.inviteUser(member.contactJid, inMuc: self.group.contactJid) } } } From 0ce8b6132330e8f40733deb14ab7c71ce780565c Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Sat, 4 May 2024 08:36:51 +0200 Subject: [PATCH 058/143] Allow multiline group subjects when editing --- Monal/Classes/EditGroupSubject.swift | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Monal/Classes/EditGroupSubject.swift b/Monal/Classes/EditGroupSubject.swift index 7dd8e816ec..7647694151 100644 --- a/Monal/Classes/EditGroupSubject.swift +++ b/Monal/Classes/EditGroupSubject.swift @@ -12,9 +12,9 @@ struct EditGroupSubject: View { @StateObject var contact: ObservableKVOWrapper private let account: xmpp? @State private var subject: String - @State private var isEditingSubject: Bool = false @Environment(\.presentationMode) var presentationMode + //@Environment(\.dismiss) var dismiss init(contact: ObservableKVOWrapper) { MLAssert(contact.isGroup, "contact must be a muc") @@ -28,8 +28,8 @@ struct EditGroupSubject: View { NavigationView { VStack { Form { - Section(header: Text("Group Description")) { - TextField(NSLocalizedString("Group Description (optional)", comment: "placeholder when editing a group description"), text: $subject, onEditingChanged: { isEditingSubject = $0 }) + Section(header: Text("Group Description (optional)")) { + TextEditor(text: $subject) .multilineTextAlignment(.leading) .applyClosure { view in if #available(iOS 16.0, *) { @@ -38,7 +38,6 @@ struct EditGroupSubject: View { view } } - .addClearButton(isEditing: isEditingSubject, text:$subject) } } } From eeea2737fc8b582735ba309886244237b9675521 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Sat, 4 May 2024 08:37:37 +0200 Subject: [PATCH 059/143] Rework whole muc edit ui to be more modern --- Monal/Classes/ContactDetails.swift | 275 +++++++++++++++++++---- Monal/Classes/ContactDetailsHeader.swift | 99 -------- Monal/Classes/EditGroupName.swift | 53 ----- Monal/Classes/GroupDetailsEdit.swift | 178 --------------- Monal/Classes/MLContact.h | 1 + Monal/Classes/MLContact.m | 32 ++- Monal/Classes/MLMessageProcessor.m | 8 +- Monal/Classes/MLMucProcessor.m | 55 ++++- Monal/Monal.xcodeproj/project.pbxproj | 12 - 9 files changed, 318 insertions(+), 395 deletions(-) delete mode 100644 Monal/Classes/ContactDetailsHeader.swift delete mode 100644 Monal/Classes/EditGroupName.swift delete mode 100644 Monal/Classes/GroupDetailsEdit.swift diff --git a/Monal/Classes/ContactDetails.swift b/Monal/Classes/ContactDetails.swift index a4fdd48f8b..74d0403f20 100644 --- a/Monal/Classes/ContactDetails.swift +++ b/Monal/Classes/ContactDetails.swift @@ -6,14 +6,11 @@ // Copyright © 2021 Monal.im. All rights reserved. // -import UIKit -import SwiftUI -import monalxmpp - struct ContactDetails: View { var delegate: SheetDismisserProtocol private var account: xmpp - private var isGroupModerator = false + private var ownRole: String + private var ownAffiliation: String @StateObject var contact: ObservableKVOWrapper @State private var showingBlockContactConfirmation = false @State private var showingCannotBlockAlert = false @@ -24,6 +21,15 @@ struct ContactDetails: View { @State private var showingCannotEncryptAlert = false @State private var showingShouldDisableEncryptionAlert = false @State private var isEditingNickname = false + @State private var inputImage: UIImage? + @State private var showingImagePicker = false + @State private var showingSheetEditSubject = false + @State private var showingDestroyConfirmation = false + @State private var alertPrompt = AlertPrompt(dismissLabel: Text("Close")) + @State private var showAlert = false + @State private var success = false + @State private var successCallback: monal_void_block_t? + @StateObject private var overlay = LoadingOverlayState() init(delegate: SheetDismisserProtocol, contact: ObservableKVOWrapper) { self.delegate = delegate @@ -31,24 +37,148 @@ struct ContactDetails: View { self.account = MLXMPPManager.sharedInstance().getConnectedAccount(forID: contact.accountId)! if contact.isGroup { - let ownRole = DataLayer.sharedInstance().getOwnRole(inGroupOrChannel: contact.obj) ?? "none" - self.isGroupModerator = (ownRole == "moderator") + self.ownRole = DataLayer.sharedInstance().getOwnRole(inGroupOrChannel: contact.obj) ?? "none" + self.ownAffiliation = DataLayer.sharedInstance().getOwnAffiliation(inGroupOrChannel:contact.obj) ?? "none" + } else { + self.ownRole = "none" + self.ownAffiliation = "none" } } + private func errorAlert(title: Text, message: Text = Text("")) { + alertPrompt.title = title + alertPrompt.message = message + showAlert = true + } + + private func successAlert(title: Text, message: Text = Text("")) { + alertPrompt.title = title + alertPrompt.message = message + showAlert = true + self.success = true // < dismiss entire view on close + } + var body: some View { Form { Section { - ContactDetailsHeader(delegate:delegate, contact:contact) + VStack(spacing: 20) { + Image(uiImage: contact.avatar) + .resizable() + .scaledToFit() + .applyClosure {view in + if contact.isGroup { + if ownAffiliation == "owner" { + view.accessibilityLabel((contact.mucType == "group") ? "Change Group Avatar" : "Change Channel Avatar") + .onTapGesture { + showingImagePicker = true + } + } else { + view.accessibilityLabel((contact.mucType == "group") ? "Group Avatar" : "Channel Avatar") + } + } else { + view.accessibilityLabel("Avatar") + } + } + .frame(width: 150, height: 150, alignment: .center) + .shadow(radius: 7) + .sheet(isPresented:$showingImagePicker) { + ImagePicker(image:$inputImage) + } + + + Button { + UIPasteboard.general.setValue(contact.contactJid as String, forPasteboardType:UTType.utf8PlainText.identifier as String) + UIAccessibility.post(notification: .announcement, argument: "JID Copied") + } label: { + HStack { + Text(contact.contactJid as String) + + Image(systemName: "doc.on.doc") + .foregroundColor(.primary) + .accessibilityHidden(true) + } + .accessibilityHint("Copies JID") + } + .buttonStyle(.borderless) + + + //only show account jid if more than one is configured + if MLXMPPManager.sharedInstance().connectedXMPP.count > 1 && !contact.isSelfChat { + Text("Account: \(account.connectionProperties.identity.jid)") + } + + if !contact.isSelfChat && !contact.isGroup { + if let lastInteractionTime = contact.lastInteractionTime as Date? { + if lastInteractionTime.timeIntervalSince1970 > 0 { + Text(String(format: NSLocalizedString("Last seen: %@", comment: ""), + DateFormatter.localizedString(from: lastInteractionTime, dateStyle: DateFormatter.Style.short, timeStyle: DateFormatter.Style.short))) + } else { + Text(String(format: NSLocalizedString("Last seen: %@", comment: ""), NSLocalizedString("now", comment: ""))) + } + } else { + Text(String(format: NSLocalizedString("Last seen: %@", comment: ""), NSLocalizedString("unknown", comment: ""))) + } + } + + if !contact.isGroup && (contact.statusMessage as String).count > 0 { + VStack { + Text("Status message:") + Text(contact.statusMessage as String) + .fixedSize(horizontal: false, vertical: true) + } + } + + if contact.isGroup && ((contact.groupSubject as String).count > 0 || ownRole == "moderator") { + VStack { + if ownRole == "moderator" { + Button { + showingSheetEditSubject.toggle() + } label: { + if contact.obj.mucType == "group" { + HStack { + Text("Group subject:") + Spacer().frame(width:8) + Image(systemName: "pencil") + .foregroundColor(.primary) + .accessibilityHidden(true) + } + .accessibilityHint("Edit Group Subject") + } else { + HStack { + Text("Channel subject:") + Spacer().frame(width:8) + Image(systemName: "pencil") + .foregroundColor(.primary) + .accessibilityHidden(true) + } + .accessibilityHint("Edit Channel Subject") + } + } + .buttonStyle(.borderless) + .sheet(isPresented: $showingSheetEditSubject) { + LazyClosureView(EditGroupSubject(contact: contact)) + } + } else { + Text("Group subject:") + } + + Text(contact.groupSubject as String) + .fixedSize(horizontal: false, vertical: true) + } + } + } + .foregroundColor(.primary) + .padding([.top, .bottom]) + .frame(maxWidth: .infinity) } // info/nondestructive buttons Section { Button { - if(contact.isGroup) { - if(!contact.isMuted && !contact.isMentionOnly) { + if contact.isGroup { + if !contact.isMuted && !contact.isMentionOnly { contact.obj.toggleMentionOnly(true) - } else if(!contact.isMuted && contact.isMentionOnly) { + } else if !contact.isMuted && contact.isMentionOnly { contact.obj.toggleMentionOnly(false) contact.obj.toggleMute(true) } else { @@ -59,14 +189,14 @@ struct ContactDetails: View { contact.obj.toggleMute(!contact.isMuted) } } label: { - if(contact.isMuted) { + if contact.isMuted { Label { contact.isGroup ? Text("Notifications disabled") : Text("Contact is muted") } icon: { Image(systemName: "bell.slash.fill") .foregroundColor(.red) } - } else if(contact.isGroup && contact.isMentionOnly) { + } else if contact.isGroup && contact.isMentionOnly { Label { Text("Notify only when mentioned") } icon: { @@ -83,9 +213,9 @@ struct ContactDetails: View { } #if !DISABLE_OMEMO - if((!contact.isGroup || (contact.isGroup && contact.mucType == "group")) && !HelperTools.isContactBlacklisted(forEncryption:contact.obj)) { + if (!contact.isGroup || (contact.isGroup && contact.mucType == "group")) && !HelperTools.isContactBlacklisted(forEncryption:contact.obj) { Button { - if(contact.isEncrypted) { + if contact.isEncrypted { showingShouldDisableEncryptionAlert = true } else { showingCannotEncryptAlert = !contact.obj.toggleEncryption(!contact.isEncrypted) @@ -132,7 +262,14 @@ struct ContactDetails: View { } #endif - if(!contact.isGroup && !contact.isSelfChat) { + if contact.isGroup && ownAffiliation == "owner" { + let label = contact.obj.mucType == "group" ? NSLocalizedString("Rename Group", comment:"") : NSLocalizedString("Rename Channel", comment:"") + TextField(label, text: $contact.fullNameView, onEditingChanged: { + isEditingNickname = $0 + }) + .accessibilityLabel(contact.obj.mucType == "group" ? "Group name" : "Channel name") + .addClearButton(isEditing: isEditingNickname, text: $contact.fullNameView) + } else if !contact.isGroup && !contact.isSelfChat { TextField(NSLocalizedString("Rename Contact", comment: "placeholder text in contact details"), text: $contact.nickNameView, onEditingChanged: { isEditingNickname = $0 }) @@ -148,22 +285,22 @@ struct ContactDetails: View { Text("Pin Chat") } - if(contact.obj.isGroup && contact.obj.mucType == "group") { + if contact.obj.isGroup && contact.obj.mucType == "group" { NavigationLink(destination: LazyClosureView(MemberList(mucContact:contact))) { Text("Group Members") } - } else if(contact.obj.isGroup && contact.obj.mucType == "channel") { + } else if contact.obj.isGroup && contact.obj.mucType == "channel" { NavigationLink(destination: LazyClosureView(ChannelMemberList(mucContact:contact))) { Text("Channel Members") } } #if !DISABLE_OMEMO - if(!HelperTools.isContactBlacklisted(forEncryption:contact.obj)) { - if(!contact.isGroup) { + if !HelperTools.isContactBlacklisted(forEncryption:contact.obj) { + if !contact.isGroup { NavigationLink(destination: LazyClosureView(OmemoKeys(contact: contact))) { contact.isSelfChat ? Text("Own Encryption Keys") : Text("Encryption Keys") } - } else if(contact.mucType == "group") { + } else if contact.mucType == "group" { NavigationLink(destination: LazyClosureView(OmemoKeys(contact: contact))) { Text("Encryption Keys") } @@ -171,7 +308,7 @@ struct ContactDetails: View { } #endif - if(!contact.isGroup && !contact.isSelfChat) { + if !contact.isGroup && !contact.isSelfChat { NavigationLink(destination: LazyClosureView(ContactResources(contact: contact))) { Text("Resources") } @@ -195,13 +332,13 @@ struct ContactDetails: View { Section { // the destructive section... if !contact.isSelfChat { Button(action: { - if(!contact.isBlocked) { + if !contact.isBlocked { showingBlockContactConfirmation = true } else { showingCannotBlockAlert = !contact.obj.toggleBlocked(!contact.isBlocked) } }) { - if(!contact.isBlocked) { + if !contact.isBlocked { Text("Block Contact") .foregroundColor(.red) } else { @@ -228,12 +365,12 @@ struct ContactDetails: View { } Group { - if(contact.isInRoster) { + if contact.isInRoster { Button(action: { showingRemoveContactConfirmation = true }) { - if(contact.isGroup) { - if(contact.mucType == "group") { + if contact.isGroup { + if contact.mucType == "group" { Text("Leave Group") .foregroundColor(.red) } else { @@ -265,8 +402,8 @@ struct ContactDetails: View { Button(action: { showingAddContactConfirmation = true }) { - if(contact.isGroup) { - if(contact.mucType == "group") { + if contact.isGroup { + if contact.mucType == "group" { Text("Join Group") } else { Text("Join Channel") @@ -294,11 +431,54 @@ struct ContactDetails: View { } } + if ownAffiliation == "owner" { + Section { + Button(action: { + showingDestroyConfirmation = true + }) { + if contact.mucType == "group" { + Text("Destroy Group").foregroundColor(.red) + } else { + Text("Destroy Channel").foregroundColor(.red) + } + } + .actionSheet(isPresented: $showingDestroyConfirmation) { + ActionSheet( + title: contact.mucType == "group" ? Text("Destroy Group") : Text("Destroy Channel"), + message: contact.mucType == "group" ? Text("Do you really want to destroy this group? Every member will be kicked out and it will be destroyed afterwards.") : Text("Do you really want to destroy this channel? Every member will be kicked out and it will be destroyed afterwards."), + buttons: [ + .cancel(), + .destructive( + Text("Yes"), + action: { + showLoadingOverlay(overlay, headline: contact.mucType == "group" ? NSLocalizedString("Destroying group...", comment: "") : NSLocalizedString("Destroying channel...", comment: "")) + self.account.mucProcessor.destroyRoom(contact.contactJid as String) + self.account.mucProcessor.addUIHandler({_data in let data = _data as! NSDictionary + hideLoadingOverlay(overlay) + let success : Bool = data["success"] as! Bool; + if success { + if let callback = data["callback"] { + self.successCallback = objcCast(callback) as monal_void_block_t + } + DDLogError("callback: \(String(describing:self.successCallback))") + successAlert(title: Text("Success"), message: contact.mucType == "group" ? Text("Successfully destroyed group.") : Text("Successfully destroyed channel.")) + } else { + errorAlert(title: Text("Error destroying group!"), message: Text(data["errorMessage"] as! String)) + } + }, forMuc:contact.contactJid) + } + ) + ] + ) + } + } + } + Button(action: { showingClearHistoryConfirmation = true }) { - if(contact.isGroup) { - if(contact.obj.mucType == "group") { + if contact.isGroup { + if contact.obj.mucType == "group" { Text("Clear chat history of this group") } else { Text("Clear chat history of this channel") @@ -329,8 +509,7 @@ struct ContactDetails: View { //omemo debug stuff, should be removed in a few months Section { // only display omemo session reset button on 1:1 and private groups - if(contact.obj.isGroup == false || (contact.isGroup && contact.mucType == "group")) - { + if contact.obj.isGroup == false || (contact.isGroup && contact.mucType == "group") { Button(action: { showingResetOmemoSessionConfirmation = true }) { @@ -357,20 +536,28 @@ struct ContactDetails: View { #endif } .frame(maxWidth: .infinity, maxHeight: .infinity) + .addLoadingOverlay(overlay) .navigationBarTitle(contact.contactDisplayName as String, displayMode:.inline) - .applyClosure { view in - if contact.isGroup && isGroupModerator && self.account.accountState.rawValue >= xmppState.stateBound.rawValue { - view.toolbar { - ToolbarItem(placement:.navigationBarTrailing) { - let ownAffiliation = DataLayer.sharedInstance().getOwnAffiliation(inGroupOrChannel:contact.obj) ?? "none" - NavigationLink(destination:LazyClosureView(GroupDetailsEdit(contact:contact, ownAffiliation:ownAffiliation))) { - Text("Edit") - } + .alert(isPresented: $showAlert) { + Alert(title: alertPrompt.title, message: alertPrompt.message, dismissButton:.default(Text("Close"), action: { + showAlert = false + if self.success == true { + //close muc ui and leave chat ui of this muc + if let callback = self.successCallback { + callback() + } + if let activeChats = (UIApplication.shared.delegate as! MonalAppDelegate).activeChats { + activeChats.presentChat(with:nil) } } - } else { - view - } + })) + } + .onChange(of:inputImage) { _ in + showLoadingOverlay(overlay, headline: NSLocalizedString("Uploading image...", comment: "")) + self.account.mucProcessor.publishAvatar(inputImage, forMuc: contact.contactJid) + } + .onChange(of:contact.avatar as UIImage) { _ in + hideLoadingOverlay(overlay) } } } diff --git a/Monal/Classes/ContactDetailsHeader.swift b/Monal/Classes/ContactDetailsHeader.swift deleted file mode 100644 index e8676bd51d..0000000000 --- a/Monal/Classes/ContactDetailsHeader.swift +++ /dev/null @@ -1,99 +0,0 @@ -// -// ContactDetailsHeader.swift -// ContactDetailsHeader -// -// Created by Friedrich Altheide on 03.09.21. -// Copyright © 2021 Monal.im. All rights reserved. -// - -import MobileCoreServices -import UniformTypeIdentifiers -import SwiftUI -import monalxmpp - -struct ContactDetailsHeader: View { - var delegate: SheetDismisserProtocol - @StateObject var contact: ObservableKVOWrapper - @State private var navigationAction: String? - - var body: some View { - VStack(spacing: 20) { - Image(uiImage: contact.avatar) - .resizable() - .scaledToFit() - .accessibilityLabel("Avatar") - .frame(width: 150, height: 150, alignment: .center) - .shadow(radius: 7) - - - Button { - UIPasteboard.general.setValue(contact.contactJid as String, forPasteboardType:UTType.utf8PlainText.identifier as String) - UIAccessibility.post(notification: .announcement, argument: "JID Copied") - } label: { - HStack { - Text(contact.contactJid as String) - - Image(systemName: "doc.on.doc") - .foregroundColor(.primary) - .accessibilityHidden(true) - } - .accessibilityHint("Copies JID") - } - .buttonStyle(.borderless) - - - //only show account jid if more than one is configured - if MLXMPPManager.sharedInstance().connectedXMPP.count > 1 && !contact.isSelfChat { - Text("Account: \(MLXMPPManager.sharedInstance().getConnectedAccount(forID:contact.accountId)!.connectionProperties.identity.jid)") - } - - if !contact.isSelfChat && !contact.isGroup { - if let lastInteractionTime = contact.lastInteractionTime as Date? { - if lastInteractionTime.timeIntervalSince1970 > 0 { - Text(String(format: NSLocalizedString("Last seen: %@", comment: ""), - DateFormatter.localizedString(from: lastInteractionTime, dateStyle: DateFormatter.Style.short, timeStyle: DateFormatter.Style.short))) - } else { - Text(String(format: NSLocalizedString("Last seen: %@", comment: ""), NSLocalizedString("now", comment: ""))) - } - } else { - Text(String(format: NSLocalizedString("Last seen: %@", comment: ""), NSLocalizedString("unknown", comment: ""))) - } - } - - if(!contact.isGroup && (contact.statusMessage as String).count > 0) { - VStack { - Text("Status message:") - Text(contact.statusMessage as String) - .fixedSize(horizontal: false, vertical: true) - } - } - - if(contact.isGroup && (contact.groupSubject as String).count > 0) { - VStack { - if(contact.obj.mucType == "group") { - Text("Group subject:") - } else { - Text("Channel subject:") - } - - Text(contact.groupSubject as String) - .fixedSize(horizontal: false, vertical: true) - } - } - } - .foregroundColor(.primary) - .padding([.top, .bottom]) - .frame(maxWidth: .infinity) - } -} - -struct ContactDetailsHeader_Previews: PreviewProvider { - static var delegate = SheetDismisserProtocol() - static var previews: some View { - ContactDetailsHeader(delegate:delegate, contact:ObservableKVOWrapper(MLContact.makeDummyContact(0))) - ContactDetailsHeader(delegate:delegate, contact:ObservableKVOWrapper(MLContact.makeDummyContact(1))) - ContactDetailsHeader(delegate:delegate, contact:ObservableKVOWrapper(MLContact.makeDummyContact(2))) - ContactDetailsHeader(delegate:delegate, contact:ObservableKVOWrapper(MLContact.makeDummyContact(3))) - ContactDetailsHeader(delegate:delegate, contact:ObservableKVOWrapper(MLContact.makeDummyContact(4))) - } -} diff --git a/Monal/Classes/EditGroupName.swift b/Monal/Classes/EditGroupName.swift deleted file mode 100644 index abadd364d9..0000000000 --- a/Monal/Classes/EditGroupName.swift +++ /dev/null @@ -1,53 +0,0 @@ -// -// EditGroupName.swift -// Monal -// -// Created by Friedrich Altheide on 24.02.24. -// Copyright © 2024 monal-im.org. All rights reserved. -// - -import SwiftUI - -struct EditGroupName: View { - @StateObject var contact: ObservableKVOWrapper - private let account: xmpp? - @State private var groupName: String - @State private var isEditingGroupName: Bool = false - - @Environment(\.presentationMode) var presentationMode - - init(contact: ObservableKVOWrapper) { - MLAssert(contact.isGroup, "contact must be a muc") - - _groupName = State(wrappedValue: contact.obj.contactDisplayName) - _contact = StateObject(wrappedValue: contact) - self.account = MLXMPPManager.sharedInstance().getConnectedAccount(forID: contact.accountId)! as xmpp - } - - var body: some View { - - NavigationView { - Form { - Section(header: Text("Group name")) { - TextField(NSLocalizedString("Group Name (optional)", comment: "placeholder when editing a group name"), text: $groupName, onEditingChanged: { isEditingGroupName = $0 }) - .autocorrectionDisabled() - .autocapitalization(.none) - .addClearButton(isEditing: isEditingGroupName, text:$groupName) - } - } - .toolbar { - ToolbarItem(placement: .cancellationAction) { - Button("Abort") { - self.presentationMode.wrappedValue.dismiss() - } - } - ToolbarItem(placement: .confirmationAction) { - Button("Done") { - self.account!.mucProcessor.changeName(ofMuc: contact.contactJid, to: self.groupName) - self.presentationMode.wrappedValue.dismiss() - } - } - } - } - } -} diff --git a/Monal/Classes/GroupDetailsEdit.swift b/Monal/Classes/GroupDetailsEdit.swift deleted file mode 100644 index 5c47713ea1..0000000000 --- a/Monal/Classes/GroupDetailsEdit.swift +++ /dev/null @@ -1,178 +0,0 @@ -// -// GroupDetailsEdit.swift -// Monal -// -// Created by Friedrich Altheide on 23.02.24. -// Copyright © 2024 monal-im.org. All rights reserved. -// - -import _PhotosUI_SwiftUI - -struct GroupDetailsEdit: View { - @StateObject var contact: ObservableKVOWrapper - @State private var showingSheetEditName = false - @State private var showingSheetEditSubject = false - @State private var inputImage: UIImage? - @State private var showingImagePicker = false - @State private var showingDestroyConfirmation = false - @State private var alertPrompt = AlertPrompt(dismissLabel: Text("Close")) - @State private var showAlert = false - @State private var success = false - @StateObject private var overlay = LoadingOverlayState() - @State private var successCallback: monal_void_block_t? - private let account: xmpp - private let ownAffiliation: String? - - private func errorAlert(title: Text, message: Text = Text("")) { - alertPrompt.title = title - alertPrompt.message = message - showAlert = true - } - - private func successAlert(title: Text, message: Text = Text("")) { - alertPrompt.title = title - alertPrompt.message = message - showAlert = true - self.success = true // < dismiss entire view on close - } - - init(contact: ObservableKVOWrapper, ownAffiliation: String?) { - MLAssert(contact.isGroup) - - _contact = StateObject(wrappedValue: contact) - _inputImage = State(initialValue: contact.avatar) - self.account = MLXMPPManager.sharedInstance().getConnectedAccount(forID: contact.accountId)! as xmpp - self.ownAffiliation = ownAffiliation - } - - var body: some View { - Form { - if ownAffiliation == "owner" { - Section { - HStack { - Spacer() - Image(uiImage: contact.avatar) - .resizable() - .scaledToFit() - .accessibilityLabel((contact.mucType == "group") ? "Group Avatar" : "Channel Avatar") - .frame(width: 150, height: 150, alignment: .center) - .shadow(radius: 7) - .onTapGesture { - showingImagePicker = true - } - Spacer() - } - .sheet(isPresented:$showingImagePicker) { - ImagePicker(image:$inputImage) - } - } - } - - Section { - if ownAffiliation == "owner" { - Button(action: { - showingSheetEditName.toggle() - }) { - HStack { - Image(systemName: "person.2") - Text(contact.contactDisplayName as String) - Spacer() - } - } - .sheet(isPresented: $showingSheetEditName) { - LazyClosureView(EditGroupName(contact: contact)) - } - } - - Button(action: { - showingSheetEditSubject.toggle() - }) { - HStack { - Image(systemName: "pencil") - if contact.mucType == "group" { - Text("Group description") - } else { - Text("Channel description") - } - Spacer() - } - } - .sheet(isPresented: $showingSheetEditSubject) { - LazyClosureView(EditGroupSubject(contact: contact)) - } - } - - if ownAffiliation == "owner" { - Section { - Button(action: { - showingDestroyConfirmation = true - }) { - if contact.mucType == "group" { - Text("Destroy Group").foregroundColor(.red) - } else { - Text("Destroy Channel").foregroundColor(.red) - } - } - .actionSheet(isPresented: $showingDestroyConfirmation) { - ActionSheet( - title: contact.mucType == "group" ? Text("Destroy Group") : Text("Destroy Channel"), - message: contact.mucType == "group" ? Text("Do you really want to destroy this group? Every member will be kicked out and it will be destroyed afterwards.") : Text("Do you really want to destroy this channel? Every member will be kicked out and it will be destroyed afterwards."), - buttons: [ - .cancel(), - .destructive( - Text("Yes"), - action: { - showLoadingOverlay(overlay, headline: contact.mucType == "group" ? NSLocalizedString("Destroying group...", comment: "") : NSLocalizedString("Destroying channel...", comment: "")) - self.account.mucProcessor.destroyRoom(contact.contactJid as String) - self.account.mucProcessor.addUIHandler({_data in let data = _data as! NSDictionary - hideLoadingOverlay(overlay) - let success : Bool = data["success"] as! Bool; - if success { - if let callback = data["callback"] { - self.successCallback = objcCast(callback) as monal_void_block_t - } - DDLogError("callback: \(String(describing:self.successCallback))") - successAlert(title: Text("Success"), message: contact.mucType == "group" ? Text("Successfully destroyed group.") : Text("Successfully destroyed channel.")) - } else { - errorAlert(title: Text("Error destroying group!"), message: Text(data["errorMessage"] as! String)) - } - }, forMuc:contact.contactJid) - } - ) - ] - ) - } - } - } - } - .addLoadingOverlay(overlay) - .navigationTitle((contact.mucType == "group") ? NSLocalizedString("Edit group", comment: "") : NSLocalizedString("Edit channel", comment: "")) - .alert(isPresented: $showAlert) { - Alert(title: alertPrompt.title, message: alertPrompt.message, dismissButton:.default(Text("Close"), action: { - showAlert = false - if self.success == true { - //close muc ui and leave chat ui of this muc - if let callback = self.successCallback { - callback() - } - if let activeChats = (UIApplication.shared.delegate as! MonalAppDelegate).activeChats { - activeChats.presentChat(with:nil) - } - } - })) - } - .onChange(of:inputImage) { _ in - showLoadingOverlay(overlay, headline: NSLocalizedString("Uploading image...", comment: "")) - self.account.mucProcessor.publishAvatar(inputImage, forMuc: contact.contactJid) - } - .onChange(of:contact.avatar as UIImage) { _ in - hideLoadingOverlay(overlay) - } - } -} - -struct GroupDetailsEdit_Previews: PreviewProvider { - static var previews: some View { - GroupDetailsEdit(contact:ObservableKVOWrapper(MLContact.makeDummyContact(0)), ownAffiliation:"owner") - } -} diff --git a/Monal/Classes/MLContact.h b/Monal/Classes/MLContact.h index 49aa660b8c..0095916050 100644 --- a/Monal/Classes/MLContact.h +++ b/Monal/Classes/MLContact.h @@ -57,6 +57,7 @@ FOUNDATION_EXPORT NSString* const kAskSubscribe; */ @property (nonatomic, readonly) NSString* nickName; @property (nonatomic, strong) NSString* nickNameView; +@property (nonatomic, strong) NSString* fullNameView; /** xmpp state text diff --git a/Monal/Classes/MLContact.m b/Monal/Classes/MLContact.m index 4999d65ae9..687c614f1f 100644 --- a/Monal/Classes/MLContact.m +++ b/Monal/Classes/MLContact.m @@ -17,6 +17,7 @@ #import "MLImageManager.h" #import "MLVoIPProcessor.h" #import "MonalAppDelegate.h" +#import "MLMucProcessor.h" @import Intents; @@ -31,6 +32,7 @@ @interface MLContact () { NSInteger _unreadCount; monal_void_block_t _cancelNickChange; + monal_void_block_t _cancelFullNameChange; UIImage* _avatar; } @property (nonatomic, assign) BOOL isSelfChat; @@ -370,6 +372,7 @@ -(NSString*) nickNameView -(void) setNickNameView:(NSString*) name { + MLAssert(!self.isGroup, @"Using nickNameView only allowed for 1:1 contacts!", (@{@"contact": self})); if([self.nickName isEqualToString:name] || name == nil) return; //no change at all self.nickName = name; @@ -377,7 +380,7 @@ -(void) setNickNameView:(NSString*) name if(_cancelNickChange) _cancelNickChange(); // delay changes because we don't want to update the roster on our server too often while typing - _cancelNickChange = createTimer(1.0, (^{ + _cancelNickChange = createTimer(2.0, (^{ xmpp* account = [[MLXMPPManager sharedInstance] getConnectedAccountForID:self.accountId]; [account updateRosterItem:self withName:self.nickName]; })); @@ -388,6 +391,33 @@ +(NSSet*) keyPathsForValuesAffectingNickNameView return [NSSet setWithObjects:@"nickName", nil]; } +-(NSString*) fullNameView +{ + return nilDefault(self.fullName, @""); +} + +-(void) setFullNameView:(NSString*) name +{ + MLAssert(self.isGroup, @"Using fullNameView only allowed for mucs!", (@{@"contact": self})); + if([self.fullName isEqualToString:name] || name == nil) + return; //no change at all + self.fullName = name; + xmpp* account = [[MLXMPPManager sharedInstance] getConnectedAccountForID:self.accountId]; + [[DataLayer sharedInstance] setFullName:self.fullName forContact:self.contactJid andAccount:account.accountNo]; + // abort old change timer and start a new one + if(_cancelFullNameChange) + _cancelFullNameChange(); + // delay changes because we don't want to update the roster on our server too often while typing + _cancelFullNameChange = createTimer(2.0, (^{ + [account.mucProcessor changeNameOfMuc:self.contactJid to:self.fullName]; + })); +} + ++(NSSet*) keyPathsForValuesAffectingFullNameView +{ + return [NSSet setWithObjects:@"fullName", nil]; +} + -(UIImage*) avatar { // return already cached image diff --git a/Monal/Classes/MLMessageProcessor.m b/Monal/Classes/MLMessageProcessor.m index 9f11a9e598..59bdc62253 100644 --- a/Monal/Classes/MLMessageProcessor.m +++ b/Monal/Classes/MLMessageProcessor.m @@ -346,16 +346,16 @@ +(MLMessage* _Nullable) processMessage:(XMPPMessage*) messageNode andOuterMessag DDLogDebug(@"This is muc, inbound is now: %@ (ownNick: %@, actualFrom: %@, participantJid: %@)", inbound ? @"YES": @"NO", ownNick, actualFrom, participantJid); } - if([messageNode check:@"//subject#"]) + if([messageNode check:@"//subject"]) { if(!isMLhistory) { - NSString* subject = [messageNode findFirst:@"//subject#"]; + NSString* subject = nilDefault([messageNode findFirst:@"//subject#"], @""); subject = [subject stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; NSString* currentSubject = [[DataLayer sharedInstance] mucSubjectforAccount:account.accountNo andRoom:messageNode.fromUser]; - DDLogInfo(@"Got MUC subject for %@: %@", messageNode.fromUser, subject); + DDLogInfo(@"Got MUC subject for %@: '%@'", messageNode.fromUser, subject); - if(subject == nil || [subject isEqualToString:currentSubject]) + if([subject isEqualToString:currentSubject]) { DDLogVerbose(@"Ignoring subject, nothing changed..."); return nil; diff --git a/Monal/Classes/MLMucProcessor.m b/Monal/Classes/MLMucProcessor.m index 667f36b3ef..4702b592a8 100644 --- a/Monal/Classes/MLMucProcessor.m +++ b/Monal/Classes/MLMucProcessor.m @@ -23,7 +23,7 @@ #import "MLOMEMO.h" #import "MLImageManager.h" -#define CURRENT_MUC_STATE_VERSION @8 +#define CURRENT_MUC_STATE_VERSION @9 @interface MLMucProcessor() { @@ -35,6 +35,7 @@ @interface MLMucProcessor() NSMutableDictionary* _joining; NSMutableSet* _destroying; NSMutableSet* _firstJoin; + NSMutableDictionary* _changingName; NSDate* _lastPing; NSMutableSet* _noUpdateBookmarks; BOOL _hasFetchedBookmarks; @@ -78,6 +79,7 @@ -(id) initWithAccount:(xmpp*) account _joining = [NSMutableDictionary new]; _destroying = [NSMutableSet new]; _firstJoin = [NSMutableSet new]; + _changingName = [NSMutableDictionary new]; _uiHandler = [NSMutableDictionary new]; _lastPing = [NSDate date]; _noUpdateBookmarks = [NSMutableSet new]; @@ -111,6 +113,7 @@ -(void) setInternalState:(NSDictionary*) state _joining = [state[@"joining"] mutableCopy]; _destroying = [state[@"destroying"] mutableCopy]; _firstJoin = [state[@"firstJoin"] mutableCopy]; + _changingName = [state[@"changingName"] mutableCopy]; _lastPing = state[@"lastPing"]; _noUpdateBookmarks = [state[@"noUpdateBookmarks"] mutableCopy]; _hasFetchedBookmarks = [state[@"hasFetchedBookmarks"] boolValue]; @@ -127,6 +130,7 @@ -(NSDictionary*) getInternalState @"joining": [_joining copy], @"destroying": [_destroying copy], @"firstJoin": [_firstJoin copy], + @"changingName": [_changingName copy], @"lastPing": _lastPing, @"noUpdateBookmarks": [_noUpdateBookmarks copy], @"hasFetchedBookmarks": @(_hasFetchedBookmarks), @@ -144,6 +148,8 @@ -(void) handleResourceBound:(NSNotification*) notification { @synchronized(_stateLockObject) { _roomFeatures = [NSMutableDictionary new]; + _destroying = [NSMutableSet new]; + _changingName = [NSMutableDictionary new]; //make sure all idle timers get invalidated properly NSDictionary* joiningCopy = [_joining copy]; @@ -152,7 +158,6 @@ -(void) handleResourceBound:(NSNotification*) notification NSDictionary* creatingCopy = [_creating copy]; for(NSString* room in creatingCopy) [self removeRoomFromCreating:room]; - _destroying = [NSMutableSet new]; //don't clear _firstJoin and _noUpdateBookmarks to make sure half-joined mucs are still added to muc bookmarks @@ -192,6 +197,32 @@ -(BOOL) isJoining:(NSString*) room } } +-(BOOL) incrementNameChange:(NSString*) room +{ + @synchronized(_stateLockObject) { + if(!_changingName[room]) + { + _changingName[room] = @1; + return YES; + } + _changingName[room] = @(((NSNumber*)_changingName[room]).integerValue + 1); + return NO; + } +} + +-(BOOL) decrementNameChange:(NSString*) room +{ + @synchronized(_stateLockObject) { + if(_changingName[room] == nil) + return YES; + NSInteger oldValue = ((NSNumber*)_changingName[room]).integerValue; + _changingName[room] = @(max(0, oldValue - 1)); + if(oldValue == 0) + return YES; + return NO; + } +} + -(void) addUIHandler:(monal_id_block_t) handler forMuc:(NSString*) room { //this will replace the old handler @@ -445,6 +476,7 @@ -(void) configureMuc:(NSString*) roomJid withMandatoryOptions:(NSDictionary*) ma } $$instance_handler(handleRoomConfigFormInvalidation, account.mucProcessor, $$ID(xmpp*, account), $$ID(NSString*, roomJid), $$ID(NSDictionary*, mandatoryOptions), $$ID(NSDictionary*, optionalOptions), $$BOOL(deleteOnError)) + [self decrementNameChange:roomJid]; if(deleteOnError) { DDLogError(@"Config form fetch failed, removing muc '%@' from _creating...", roomJid); @@ -464,6 +496,7 @@ -(void) configureMuc:(NSString*) roomJid withMandatoryOptions:(NSDictionary*) ma if([iqNode check:@"/"]) { DDLogError(@"Failed to fetch room config form for '%@': %@", roomJid, [iqNode findFirst:@"error"]); + [self decrementNameChange:roomJid]; if(deleteOnError) { [self removeRoomFromCreating:roomJid]; @@ -477,6 +510,7 @@ -(void) configureMuc:(NSString*) roomJid withMandatoryOptions:(NSDictionary*) ma if(dataForm == nil) { DDLogError(@"Got empty room config form for '%@'!", roomJid); + [self decrementNameChange:roomJid]; if(deleteOnError) { [self removeRoomFromCreating:roomJid]; @@ -491,6 +525,7 @@ -(void) configureMuc:(NSString*) roomJid withMandatoryOptions:(NSDictionary*) ma if([dataForm getField:option] == nil) { DDLogError(@"Could not configure room '%@' to be a groupchat: config option '%@' not available!", roomJid, option); + [self decrementNameChange:roomJid]; if(deleteOnError) { [self removeRoomFromCreating:roomJid]; @@ -520,6 +555,7 @@ -(void) configureMuc:(NSString*) roomJid withMandatoryOptions:(NSDictionary*) ma $$ $$instance_handler(handleRoomConfigResultInvalidation, account.mucProcessor, $$ID(xmpp*, account), $$ID(NSString*, roomJid), $$ID(NSDictionary*, mandatoryOptions), $$ID(NSDictionary*, optionalOptions), $$BOOL(deleteOnError)) + [self decrementNameChange:roomJid]; if(deleteOnError) { DDLogError(@"Config form submit failed, removing muc '%@' from _creating...", roomJid); @@ -535,6 +571,7 @@ -(void) configureMuc:(NSString*) roomJid withMandatoryOptions:(NSDictionary*) ma if([iqNode check:@"/"]) { DDLogError(@"Failed to submit room config form of '%@': %@", roomJid, [iqNode findFirst:@"error"]); + [self decrementNameChange:roomJid]; if(deleteOnError) { [self removeRoomFromCreating:roomJid]; @@ -1190,6 +1227,7 @@ -(void) setAffiliation:(NSString*) affiliation ofUser:(NSString*) jid inMuc:(NSS -(void) changeNameOfMuc:(NSString*) room to:(NSString*) name { + [self incrementNameChange:room]; [self configureMuc:room withMandatoryOptions:@{ @"muc#roomconfig_roomname": name, } andOptionalOptions:@{} deletingMucOnError:NO andJoiningMucOnSuccess:NO]; @@ -1238,6 +1276,7 @@ -(void) publishAvatar:(UIImage* _Nullable) image forMuc:(NSString*) room $$ $$instance_handler(handleDiscoResponseInvalidation, account.mucProcessor, $$ID(xmpp*, account), $$ID(NSString*, roomJid)) + [self decrementNameChange:roomJid]; DDLogInfo(@"Removing muc '%@' from _joining...", roomJid); [self removeRoomFromJoining:roomJid]; $$ @@ -1258,6 +1297,7 @@ -(void) publishAvatar:(UIImage* _Nullable) image forMuc:(NSString*) room if([iqNode check:@"//error/{urn:ietf:params:xml:ns:xmpp-stanzas}gone"]) { DDLogError(@"Querying muc info returned this muc isn't available anymore: %@", [iqNode findFirst:@"error"]); + [self decrementNameChange:iqNode.fromUser]; [self removeRoomFromJoining:iqNode.fromUser]; //delete muc from favorites table to be sure we don't try to rejoin it and update bookmarks afterwards (to make sure this muc isn't accidentally left in our boomkmarks) @@ -1272,6 +1312,7 @@ -(void) publishAvatar:(UIImage* _Nullable) image forMuc:(NSString*) room if([iqNode check:@"//error"]) { DDLogError(@"Querying muc info returned a temporary error: %@", [iqNode findFirst:@"error"]); + [self decrementNameChange:iqNode.fromUser]; [self removeRoomFromJoining:iqNode.fromUser]; //do nothing: the error is only temporary (a s2s problem etc.), a muc ping will retry the join @@ -1287,6 +1328,7 @@ -(void) publishAvatar:(UIImage* _Nullable) image forMuc:(NSString*) room else if([iqNode check:@"/"]) { DDLogError(@"Querying muc info returned a persistent error: %@", [iqNode findFirst:@"error"]); + [self decrementNameChange:iqNode.fromUser]; [self removeRoomFromJoining:iqNode.fromUser]; //delete muc from favorites table to be sure we don't try to rejoin it and update bookmarks afterwards (to make sure this muc isn't accidentally left in our boomkmarks) @@ -1305,6 +1347,7 @@ -(void) publishAvatar:(UIImage* _Nullable) image forMuc:(NSString*) room if(![features containsObject:@"http://jabber.org/protocol/muc"]) { DDLogError(@"muc disco returned that this jid is not a muc!"); + [self decrementNameChange:iqNode.fromUser]; //delete muc from favorites table to be sure we don't try to rejoin it and update bookmarks afterwards (to make sure this muc isn't accidentally left in our boomkmarks) //make sure to update remote bookmarks, even if updateBookmarks == NO @@ -1324,6 +1367,7 @@ -(void) publishAvatar:(UIImage* _Nullable) image forMuc:(NSString*) room if(join && ![self isJoining:iqNode.fromUser]) { DDLogWarn(@"Ignoring muc disco result for '%@' on account %@: not joining anymore...", iqNode.fromUser, _account); + [self decrementNameChange:iqNode.fromUser]; return; } @@ -1371,12 +1415,15 @@ -(void) publishAvatar:(UIImage* _Nullable) image forMuc:(NSString*) room [[DataLayer sharedInstance] updateMucTypeTo:mucType forRoom:iqNode.fromUser andAccount:_account.accountNo]; } - if(mucName && [mucName length]) + if(!mucName || ![mucName length]) + mucName = @""; + //only handle incoming name updates if they are not our own reflected changes + if([self decrementNameChange:iqNode.fromUser]) { MLContact* mucContact = [MLContact createContactFromJid:iqNode.fromUser andAccountNo:_account.accountNo]; if(![mucName isEqualToString:mucContact.fullName]) { - DDLogInfo(@"Configuring muc %@ to use name '%@'...", iqNode.fromUser, mucName); + DDLogInfo(@"Configuring muc %@ to use name '%@' (old value: '%@')...", iqNode.fromUser, mucName, mucContact.fullName); [[DataLayer sharedInstance] setFullName:mucName forContact:iqNode.fromUser andAccount:_account.accountNo]; } } diff --git a/Monal/Monal.xcodeproj/project.pbxproj b/Monal/Monal.xcodeproj/project.pbxproj index 93f01047d2..de395b13ad 100644 --- a/Monal/Monal.xcodeproj/project.pbxproj +++ b/Monal/Monal.xcodeproj/project.pbxproj @@ -177,10 +177,7 @@ C117F7E12B086390001F2BC6 /* CreateGroupMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D88BB76295BB6DC00FB30BA /* CreateGroupMenu.swift */; }; C117F7E22B0863B3001F2BC6 /* ContactPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D631822294BAB1D00026BE7 /* ContactPicker.swift */; }; C12436142434AB5D00B8F074 /* MLAttributedLabel.m in Sources */ = {isa = PBXBuildFile; fileRef = C12436132434AB5D00B8F074 /* MLAttributedLabel.m */; }; - C13A0BCE26E78B7B00987E29 /* ContactDetailsHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = C19C919A26E26AF000F8CC57 /* ContactDetailsHeader.swift */; }; C1414E9D24312F0100948788 /* MLChatMapsCell.m in Sources */ = {isa = PBXBuildFile; fileRef = C1414E9C24312F0100948788 /* MLChatMapsCell.m */; }; - C153825F2B89BBE600EA83EC /* GroupDetailsEdit.swift in Sources */ = {isa = PBXBuildFile; fileRef = C153825E2B89BBE600EA83EC /* GroupDetailsEdit.swift */; }; - C15382622B89C38300EA83EC /* EditGroupName.swift in Sources */ = {isa = PBXBuildFile; fileRef = C15382612B89C38300EA83EC /* EditGroupName.swift */; }; C15489B925680BBE00BBA2F0 /* MLQRCodeScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = C15489B825680BBE00BBA2F0 /* MLQRCodeScanner.swift */; }; C158D40025A0AB810005AA40 /* MLMucProcessor.h in Headers */ = {isa = PBXBuildFile; fileRef = C158D3FE25A0AB810005AA40 /* MLMucProcessor.h */; }; C158D41425A0AC630005AA40 /* MLMucProcessor.m in Sources */ = {isa = PBXBuildFile; fileRef = C158D41225A0AC630005AA40 /* MLMucProcessor.m */; }; @@ -636,8 +633,6 @@ C13E640925BD406700763D6F /* pt-PT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-PT"; path = "external/pt-PT.lproj/Localizable.strings"; sourceTree = ""; }; C1414E9B24312F0100948788 /* MLChatMapsCell.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MLChatMapsCell.h; sourceTree = ""; }; C1414E9C24312F0100948788 /* MLChatMapsCell.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MLChatMapsCell.m; sourceTree = ""; }; - C153825E2B89BBE600EA83EC /* GroupDetailsEdit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupDetailsEdit.swift; sourceTree = ""; }; - C15382612B89C38300EA83EC /* EditGroupName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditGroupName.swift; sourceTree = ""; }; C15489B825680BBE00BBA2F0 /* MLQRCodeScanner.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MLQRCodeScanner.swift; sourceTree = ""; }; C1567E3528255C64006E9637 /* Monal.macos.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Monal.macos.entitlements; sourceTree = ""; }; C1567E3628255C64006E9637 /* Monal.ios.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Monal.ios.entitlements; sourceTree = ""; }; @@ -666,7 +661,6 @@ C18E7579245E8AE900AE8FB7 /* MLPipe.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MLPipe.m; sourceTree = ""; }; C1943A4A25309A9D0036172F /* MLReloadCell.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MLReloadCell.h; sourceTree = ""; }; C1943A4B25309A9D0036172F /* MLReloadCell.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MLReloadCell.m; sourceTree = ""; }; - C19C919A26E26AF000F8CC57 /* ContactDetailsHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactDetailsHeader.swift; sourceTree = ""; }; C1A80DA224D9552400B99E01 /* MLChatViewHelper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MLChatViewHelper.h; sourceTree = ""; }; C1A80DA324D9552400B99E01 /* MLChatViewHelper.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MLChatViewHelper.m; sourceTree = ""; }; C1AAC3E224B5EF4100BB15D6 /* HelperTools.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HelperTools.h; sourceTree = ""; }; @@ -1099,11 +1093,8 @@ 3D85E586282AE523006F5B3A /* OmemoQrCodeView.swift */, 3DC5035B2822F5220064C8A7 /* OmemoKeys.swift */, 3D65B78C27234B74005A30F4 /* ContactDetails.swift */, - C19C919A26E26AF000F8CC57 /* ContactDetailsHeader.swift */, 3D5A91412842B4AE008CE57E /* MemberList.swift */, C18967C62B81F61B0073C7C5 /* ChannelMemberList.swift */, - C153825E2B89BBE600EA83EC /* GroupDetailsEdit.swift */, - C15382612B89C38300EA83EC /* EditGroupName.swift */, C1E8A7F62B8E47C300760220 /* EditGroupSubject.swift */, ); name = "Contact Details"; @@ -2047,7 +2038,6 @@ 262E51971AD8CB7200788351 /* MLTextInputCell.m in Sources */, 20ED55852BADDA5C0005783E /* PrivacySettings.swift in Sources */, 84E55E7D2964424E003E191A /* ActiveChatsViewController.m in Sources */, - C15382622B89C38300EA83EC /* EditGroupName.swift in Sources */, C1E8A7F72B8E47C300760220 /* EditGroupSubject.swift in Sources */, 263DFAC32187D0E00038E716 /* MLLinkCell.m in Sources */, 3D65B78D27234B74005A30F4 /* ContactDetails.swift in Sources */, @@ -2077,8 +2067,6 @@ 2696EED21791245A00BC54B8 /* chatViewController.m in Sources */, 26A78ED823C2B59400C7CF40 /* MLPlaceholderViewController.m in Sources */, C12436142434AB5D00B8F074 /* MLAttributedLabel.m in Sources */, - C13A0BCE26E78B7B00987E29 /* ContactDetailsHeader.swift in Sources */, - C153825F2B89BBE600EA83EC /* GroupDetailsEdit.swift in Sources */, 3D85E587282AE523006F5B3A /* OmemoQrCodeView.swift in Sources */, 849A53E4287135B2007E941A /* MLVoIPProcessor.m in Sources */, C117F7E12B086390001F2BC6 /* CreateGroupMenu.swift in Sources */, From dedecaa8c2c6ec2d3723469c53d78a3baee355b5 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Sat, 4 May 2024 18:55:45 +0200 Subject: [PATCH 060/143] Fix stanzaid handling for muc reflections, fixes message retraction --- Monal/Classes/DataLayer.h | 2 ++ Monal/Classes/DataLayer.m | 16 ++++++++-------- Monal/Classes/MLMessageProcessor.m | 21 ++++++++++++++++++--- Monal/Classes/chatViewController.m | 12 ++++++++++-- Monal/Classes/xmpp.m | 1 + 5 files changed, 39 insertions(+), 13 deletions(-) diff --git a/Monal/Classes/DataLayer.h b/Monal/Classes/DataLayer.h index b030a79a5f..dc92321f78 100644 --- a/Monal/Classes/DataLayer.h +++ b/Monal/Classes/DataLayer.h @@ -179,6 +179,8 @@ extern NSString* const kMessageTypeFiletransfer; -(NSNumber*) getSmallestHistoryId; -(NSNumber*) getBiggestHistoryId; +-(NSNumber* _Nullable) hasMessageForStanzaId:(NSString*) stanzaId orMessageID:(NSString*) messageId withInboundDir:(BOOL) inbound onAccount:(NSNumber*) accountNo; + /* adds a specified message to the database */ diff --git a/Monal/Classes/DataLayer.m b/Monal/Classes/DataLayer.m index 4de33cc7a3..c68dc59d5a 100644 --- a/Monal/Classes/DataLayer.m +++ b/Monal/Classes/DataLayer.m @@ -1236,7 +1236,7 @@ -(NSNumber*) addMessageToChatBuddy:(NSString*) buddyName withInboundDir:(BOOL) i return nil; return [self.db idWriteTransaction:^{ - if(!checkForDuplicates || ![self hasMessageForStanzaId:stanzaid orMessageID:messageid withInboundDir:inbound onAccount:accountNo]) + if(!checkForDuplicates || [self hasMessageForStanzaId:stanzaid orMessageID:messageid withInboundDir:inbound onAccount:accountNo] == nil) { //this is always from a contact NSDateFormatter* formatter = [NSDateFormatter new]; @@ -1293,21 +1293,21 @@ -(NSNumber*) addMessageToChatBuddy:(NSString*) buddyName withInboundDir:(BOOL) i }]; } --(BOOL) hasMessageForStanzaId:(NSString*) stanzaId orMessageID:(NSString*) messageId withInboundDir:(BOOL) inbound onAccount:(NSNumber*) accountNo +-(NSNumber* _Nullable) hasMessageForStanzaId:(NSString*) stanzaId orMessageID:(NSString*) messageId withInboundDir:(BOOL) inbound onAccount:(NSNumber*) accountNo { if(accountNo == nil) - return NO; + return (NSNumber*)nil; - return [self.db boolWriteTransaction:^{ + return (NSNumber*)[self.db idWriteTransaction:^{ //if the stanzaid was given, this is conclusive for dedup, we don't need to check any other ids (EXCEPTION BELOW) if(stanzaId) { DDLogVerbose(@"stanzaid provided"); - NSArray* found = [self.db executeReader:@"SELECT * FROM message_history WHERE account_id=? AND stanzaid!='' AND stanzaid=?;" andArguments:@[accountNo, stanzaId]]; + NSArray* found = [self.db executeScalarReader:@"SELECT message_history_id FROM message_history WHERE account_id=? AND stanzaid!='' AND stanzaid=?;" andArguments:@[accountNo, stanzaId]]; if([found count]) { DDLogVerbose(@"stanzaid provided and could be found: %@", found); - return YES; + return found[0]; } } @@ -1328,12 +1328,12 @@ -(BOOL) hasMessageForStanzaId:(NSString*) stanzaId orMessageID:(NSString*) messa //this entry needs an update of its stanzaid [self.db executeNonQuery:@"UPDATE message_history SET stanzaid=? WHERE message_history_id=?" andArguments:@[stanzaId, historyId]]; } - return YES; + return historyId; } } DDLogVerbose(@"nothing worked --> message not found"); - return NO; + return (NSNumber*)nil; }]; } diff --git a/Monal/Classes/MLMessageProcessor.m b/Monal/Classes/MLMessageProcessor.m index 59bdc62253..41d92409d0 100644 --- a/Monal/Classes/MLMessageProcessor.m +++ b/Monal/Classes/MLMessageProcessor.m @@ -129,7 +129,6 @@ +(MLMessage* _Nullable) processMessage:(XMPPMessage*) messageNode andOuterMessag //handle incoming jmi calls (TODO: add entry to local history, once the UI for this is implemented) //only handle incoming propose messages if not older than 60 seconds - if([messageNode check:@"{urn:xmpp:jingle-message:0}*"] && ![HelperTools shouldProvideVoip]) { DDLogWarn(@"VoIP not supported, ignoring incoming JMI message!"); @@ -459,7 +458,6 @@ +(MLMessage* _Nullable) processMessage:(XMPPMessage*) messageNode andOuterMessag DDLogInfo(@"Sending out kMonalDeletedMessageNotice notification for historyId %@", historyIdToRetract); [[MLNotificationQueue currentQueue] postNotificationName:kMonalDeletedMessageNotice object:account userInfo:@{ @"message": [[[DataLayer sharedInstance] messagesForHistoryIDs:@[historyIdToRetract]] firstObject], - @"historyId": historyIdToRetract, @"contact": possiblyUnknownContact, }]; @@ -581,6 +579,7 @@ +(MLMessage* _Nullable) processMessage:(XMPPMessage*) messageNode andOuterMessag } //handle normal messages or LMC messages that can not be found + //(this will update stanzaid in database, too, if deduplication detects a duplicate/reflection) if(historyId == nil) { historyId = [[DataLayer sharedInstance] @@ -662,7 +661,6 @@ +(MLMessage* _Nullable) processMessage:(XMPPMessage*) messageNode andOuterMessag DDLogInfo(@"Sending out kMonalNewMessageNotice notification for historyId %@", historyId); [[MLNotificationQueue currentQueue] postNotificationName:kMonalNewMessageNotice object:account userInfo:@{ @"message": message, - @"historyId": historyId, @"showAlert": @(showAlert), @"contact": possiblyUnknownContact, }]; @@ -673,6 +671,23 @@ +(MLMessage* _Nullable) processMessage:(XMPPMessage*) messageNode andOuterMessag } } } + else if(!inbound) + { + //just try to use the probably reflected message to update the stanzaid of our message in the db + //messageId is always a proper origin-id in this case, because inbound == NO and Monal uses origin-ids + NSNumber* historyId = [[DataLayer sharedInstance] hasMessageForStanzaId:stanzaid orMessageID:messageId withInboundDir:inbound onAccount:account.accountNo]; + if(historyId != nil) + { + message = [[DataLayer sharedInstance] messageForHistoryID:historyId]; + DDLogDebug(@"Managed to update stanzaid of message (or stanzaid already known): %@", message); + DDLogInfo(@"Sending out kMonalNewMessageNotice notification for historyId %@", historyId); + [[MLNotificationQueue currentQueue] postNotificationName:kMonalNewMessageNotice object:account userInfo:@{ + @"message": message, + @"showAlert": @(NO), + @"contact": possiblyUnknownContact, + }]; + } + } //handle message receipts if( diff --git a/Monal/Classes/chatViewController.m b/Monal/Classes/chatViewController.m index d2718adaa0..57c9d7f821 100644 --- a/Monal/Classes/chatViewController.m +++ b/Monal/Classes/chatViewController.m @@ -2561,15 +2561,23 @@ -(UISwipeActionsConfiguration*) tableView:(UITableView*) tableView trailingSwipe copyAction.image = [[[UIImage systemImageNamed:@"doc.on.doc.fill"] imageWithHorizontallyFlippedOrientation] imageWithTintColor:UIColor.whiteColor renderingMode:UIImageRenderingModeAutomatic]; //only allow editing for the 3 newest message && only on outgoing messages - if(!message.inbound && [[DataLayer sharedInstance] checkLMCEligible:message.messageDBId encrypted:(message.encrypted || self.contact.isEncrypted) historyBaseID:nil]) + if((!message.inbound && [[DataLayer sharedInstance] checkLMCEligible:message.messageDBId encrypted:(message.encrypted || self.contact.isEncrypted) historyBaseID:nil]) && (!message.isMuc || (message.isMuc && message.stanzaId != nil)) && !message.retracted) return [UISwipeActionsConfiguration configurationWithActions:@[ quoteAction, copyAction, LMCEditAction, retractAction, ]]; + else if(!message.inbound && [[DataLayer sharedInstance] checkLMCEligible:message.messageDBId encrypted:(message.encrypted || self.contact.isEncrypted) historyBaseID:nil] && !message.retracted) + return [UISwipeActionsConfiguration configurationWithActions:@[ + quoteAction, + copyAction, + LMCEditAction, + localDeleteAction, + ]]; //only allow retraction for outgoing messages or if we are the moderator of that muc - else if(!message.inbound || (self.contact.isGroup && [[[DataLayer sharedInstance] getOwnRoleInGroupOrChannel:self.contact] isEqualToString:@"moderator"] && [[self.xmppAccount.mucProcessor getRoomFeaturesForMuc:self.contact.contactJid] containsObject:@"urn:xmpp:message-moderate:1"])) + //but only allow retraction in mucs if we already got the reflected stanzaid (or if this is an 1:1 chat) + else if((!message.inbound || (self.contact.isGroup && [[[DataLayer sharedInstance] getOwnRoleInGroupOrChannel:self.contact] isEqualToString:@"moderator"] && [[self.xmppAccount.mucProcessor getRoomFeaturesForMuc:self.contact.contactJid] containsObject:@"urn:xmpp:message-moderate:1"])) && (!message.isMuc || (message.isMuc && message.stanzaId != nil)) && !message.retracted) return [UISwipeActionsConfiguration configurationWithActions:@[ quoteAction, copyAction, diff --git a/Monal/Classes/xmpp.m b/Monal/Classes/xmpp.m index ff776e3e21..b548940207 100644 --- a/Monal/Classes/xmpp.m +++ b/Monal/Classes/xmpp.m @@ -3326,6 +3326,7 @@ -(void) retractMessage:(MLMessage*) msg MLAssert([msg.accountId isEqual:self.accountNo], @"Can not retract message from one account on another account!", (@{@"self.accountNo": self.accountNo, @"msg": msg})); XMPPMessage* messageNode = [[XMPPMessage alloc] initWithType:kMessageChatType to:msg.buddyName]; + DDLogVerbose(@"Retracting message: %@", msg); //retraction [messageNode addChildNode:[[MLXMLNode alloc] initWithElement:@"retract" andNamespace:@"urn:xmpp:message-retract:1" withAttributes:@{ @"id": msg.isMuc ? msg.stanzaId : msg.messageId, From 38d4da3824428e58ddf7e1031f3ceb6ea19e2f7a Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Sat, 4 May 2024 19:50:25 +0200 Subject: [PATCH 061/143] Only update stanzaid on jid equality --- Monal/Classes/DataLayer.h | 2 +- Monal/Classes/DataLayer.m | 8 ++++---- Monal/Classes/MLMessageProcessor.m | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Monal/Classes/DataLayer.h b/Monal/Classes/DataLayer.h index dc92321f78..2fe0fbec21 100644 --- a/Monal/Classes/DataLayer.h +++ b/Monal/Classes/DataLayer.h @@ -179,7 +179,7 @@ extern NSString* const kMessageTypeFiletransfer; -(NSNumber*) getSmallestHistoryId; -(NSNumber*) getBiggestHistoryId; --(NSNumber* _Nullable) hasMessageForStanzaId:(NSString*) stanzaId orMessageID:(NSString*) messageId withInboundDir:(BOOL) inbound onAccount:(NSNumber*) accountNo; +-(NSNumber* _Nullable) hasMessageForStanzaId:(NSString*) stanzaId orMessageID:(NSString*) messageId withInboundDir:(BOOL) inbound andJid:(NSString*) jid onAccount:(NSNumber*) accountNo; /* adds a specified message to the database diff --git a/Monal/Classes/DataLayer.m b/Monal/Classes/DataLayer.m index c68dc59d5a..626be21542 100644 --- a/Monal/Classes/DataLayer.m +++ b/Monal/Classes/DataLayer.m @@ -1236,7 +1236,7 @@ -(NSNumber*) addMessageToChatBuddy:(NSString*) buddyName withInboundDir:(BOOL) i return nil; return [self.db idWriteTransaction:^{ - if(!checkForDuplicates || [self hasMessageForStanzaId:stanzaid orMessageID:messageid withInboundDir:inbound onAccount:accountNo] == nil) + if(!checkForDuplicates || [self hasMessageForStanzaId:stanzaid orMessageID:messageid withInboundDir:inbound andJid:buddyName onAccount:accountNo] == nil) { //this is always from a contact NSDateFormatter* formatter = [NSDateFormatter new]; @@ -1293,7 +1293,7 @@ -(NSNumber*) addMessageToChatBuddy:(NSString*) buddyName withInboundDir:(BOOL) i }]; } --(NSNumber* _Nullable) hasMessageForStanzaId:(NSString*) stanzaId orMessageID:(NSString*) messageId withInboundDir:(BOOL) inbound onAccount:(NSNumber*) accountNo +-(NSNumber* _Nullable) hasMessageForStanzaId:(NSString*) stanzaId orMessageID:(NSString*) messageId withInboundDir:(BOOL) inbound andJid:(NSString*) jid onAccount:(NSNumber*) accountNo { if(accountNo == nil) return (NSNumber*)nil; @@ -1303,7 +1303,7 @@ -(NSNumber* _Nullable) hasMessageForStanzaId:(NSString*) stanzaId orMessageID:(N if(stanzaId) { DDLogVerbose(@"stanzaid provided"); - NSArray* found = [self.db executeScalarReader:@"SELECT message_history_id FROM message_history WHERE account_id=? AND stanzaid!='' AND stanzaid=?;" andArguments:@[accountNo, stanzaId]]; + NSArray* found = [self.db executeScalarReader:@"SELECT message_history_id FROM message_history WHERE account_id=? AND buddy_name=? AND stanzaid!='' AND stanzaid=?;" andArguments:@[accountNo, jid, stanzaId]]; if([found count]) { DDLogVerbose(@"stanzaid provided and could be found: %@", found); @@ -1318,7 +1318,7 @@ -(NSNumber* _Nullable) hasMessageForStanzaId:(NSString*) stanzaId orMessageID:(N // the check, if an origin-id was given, lives in MLMessageProcessor.m (it only triggers a dedup for messages either having a stanzaid or an origin-id) if(inbound == NO) { - NSNumber* historyId = (NSNumber*)[self.db executeScalar:@"SELECT message_history_id FROM message_history WHERE account_id=? AND inbound=0 AND messageid=?;" andArguments:@[accountNo, messageId]]; + NSNumber* historyId = (NSNumber*)[self.db executeScalar:@"SELECT message_history_id FROM message_history WHERE account_id=? AND buddy_name=? AND inbound=0 AND messageid=?;" andArguments:@[accountNo, jid, messageId]]; if(historyId != nil) { DDLogVerbose(@"found by origin-id or messageid"); diff --git a/Monal/Classes/MLMessageProcessor.m b/Monal/Classes/MLMessageProcessor.m index 41d92409d0..8ac9906a3c 100644 --- a/Monal/Classes/MLMessageProcessor.m +++ b/Monal/Classes/MLMessageProcessor.m @@ -675,7 +675,7 @@ +(MLMessage* _Nullable) processMessage:(XMPPMessage*) messageNode andOuterMessag { //just try to use the probably reflected message to update the stanzaid of our message in the db //messageId is always a proper origin-id in this case, because inbound == NO and Monal uses origin-ids - NSNumber* historyId = [[DataLayer sharedInstance] hasMessageForStanzaId:stanzaid orMessageID:messageId withInboundDir:inbound onAccount:account.accountNo]; + NSNumber* historyId = [[DataLayer sharedInstance] hasMessageForStanzaId:stanzaid orMessageID:messageId withInboundDir:inbound andJid:buddyName onAccount:account.accountNo]; if(historyId != nil) { message = [[DataLayer sharedInstance] messageForHistoryID:historyId]; From 0ba785c54a58072b525e007aa575239745ffc235 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Sat, 4 May 2024 20:56:06 +0200 Subject: [PATCH 062/143] Throw exception if sqlite bind and arg count don't match --- Monal/Classes/MLSQLite.m | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Monal/Classes/MLSQLite.m b/Monal/Classes/MLSQLite.m index 88b60fde22..1a7d320adb 100644 --- a/Monal/Classes/MLSQLite.m +++ b/Monal/Classes/MLSQLite.m @@ -153,10 +153,18 @@ -(sqlite3_stmt*) prepareQuery:(NSString*) query withArgs:(NSArray*) args if(sqlite3_prepare_v2(self->_database, [query cStringUsingEncoding:NSUTF8StringEncoding], -1, &statement, NULL) != SQLITE_OK) { - DDLogError(@"sqlite prepare '%@' failed: %s", query, sqlite3_errmsg(self->_database)); + [self throwErrorForQuery:query andArguments:args]; return NULL; } + if((int)args.count != sqlite3_bind_parameter_count(statement)) + @throw [NSException exceptionWithName:@"SQLite3Exception" reason:@"SQL parameter count not equals argument count!" userInfo:@{ + @"query": query, + @"args": args, + @"paramCount": @(sqlite3_bind_parameter_count(statement)), + @"argCount": @(args.count), + }]; + //bind args to statement sqlite3_reset(statement); [args enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop __unused) { From 4d75186a96902d37f1e27eb247b3bcaccb3b5186 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Sun, 5 May 2024 00:27:00 +0200 Subject: [PATCH 063/143] Let ObservableKVOWrapper return description of wrapped object --- Monal/Classes/SwiftHelpers.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Monal/Classes/SwiftHelpers.swift b/Monal/Classes/SwiftHelpers.swift index fe9a382492..4df315edec 100644 --- a/Monal/Classes/SwiftHelpers.swift +++ b/Monal/Classes/SwiftHelpers.swift @@ -91,7 +91,7 @@ class KVOObserver: NSObject { } @dynamicMemberLookup -public class ObservableKVOWrapper: ObservableObject, Hashable, Equatable { +public class ObservableKVOWrapper: ObservableObject, Hashable, Equatable, CustomStringConvertible { public var obj: ObjType private var observedMembers: NSMutableSet = NSMutableSet() private var observers: [KVOObserver] = Array() @@ -158,6 +158,10 @@ public class ObservableKVOWrapper: ObservableObject, Hashable, self.setWrapper(for:member, value:newValue as AnyObject?) } } + + public var description: String { + return "ObservableKVOWrapper<\(String(describing:self.obj))>" + } @inlinable public static func ==(lhs: ObservableKVOWrapper, rhs: ObservableKVOWrapper) -> Bool { From f1a493208b9eb4d1f09ddb4bd99a89978776b6e2 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Sun, 5 May 2024 01:38:33 +0200 Subject: [PATCH 064/143] Make ObservableKVOWrapper conform to Identifiable --- Monal/Classes/SwiftHelpers.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Monal/Classes/SwiftHelpers.swift b/Monal/Classes/SwiftHelpers.swift index 4df315edec..82ad5e63d2 100644 --- a/Monal/Classes/SwiftHelpers.swift +++ b/Monal/Classes/SwiftHelpers.swift @@ -91,7 +91,7 @@ class KVOObserver: NSObject { } @dynamicMemberLookup -public class ObservableKVOWrapper: ObservableObject, Hashable, Equatable, CustomStringConvertible { +public class ObservableKVOWrapper: ObservableObject, Hashable, Equatable, CustomStringConvertible, Identifiable { public var obj: ObjType private var observedMembers: NSMutableSet = NSMutableSet() private var observers: [KVOObserver] = Array() From 6d4b835a054365cb63c7c97c63b77ddc7190c44e Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Sun, 5 May 2024 00:45:32 +0200 Subject: [PATCH 065/143] Rework whole group members list ui --- Monal/Classes/ChannelMemberList.swift | 17 +- Monal/Classes/ContactDetails.swift | 23 +- Monal/Classes/CreateGroupMenu.swift | 15 +- Monal/Classes/DataLayer.m | 2 +- Monal/Classes/MemberList.swift | 295 +++++++++++--------------- Monal/Classes/SwiftuiHelpers.swift | 48 ++--- Monal/Classes/chatViewController.m | 17 +- 7 files changed, 188 insertions(+), 229 deletions(-) diff --git a/Monal/Classes/ChannelMemberList.swift b/Monal/Classes/ChannelMemberList.swift index a3cbd0444d..d25ea59078 100644 --- a/Monal/Classes/ChannelMemberList.swift +++ b/Monal/Classes/ChannelMemberList.swift @@ -11,7 +11,7 @@ import monalxmpp import OrderedCollections struct ChannelMemberList: View { - @State private var channelMembers: OrderedDictionary + @State private var channelParticipants: OrderedDictionary @StateObject var channel: ObservableKVOWrapper private let account: xmpp @@ -26,31 +26,30 @@ struct ChannelMemberList: View { nickSet.updateValue((jidDict["affiliation"] as? String) ?? "none", forKey:nick) } } - _channelMembers = State(wrappedValue: nickSet) + _channelParticipants = State(wrappedValue: nickSet) } var body: some View { List { Section(header: Text(self.channel.obj.contactDisplayName)) { - ForEach(self.channelMembers.sorted(by: <), id: \.self.key) { - member in + ForEach(self.channelParticipants.sorted(by: <), id: \.self.key) { participant in ZStack(alignment: .topLeading) { HStack(alignment: .center) { - Text(member.key) + Text(participant.key) Spacer() - if member.value == "owner" { + if participant.value == "owner" { Text(NSLocalizedString("Owner", comment: "")) - } else if member.value == "admin" { + } else if participant.value == "admin" { Text(NSLocalizedString("Admin", comment: "")) } else { - Text(NSLocalizedString("Member", comment: "")) + Text(NSLocalizedString("Participant", comment: "")) } } } } } } - .navigationBarTitle("Channel Members", displayMode: .inline) + .navigationBarTitle(NSLocalizedString("Channel Participants", comment: ""), displayMode: .inline) } } diff --git a/Monal/Classes/ContactDetails.swift b/Monal/Classes/ContactDetails.swift index 74d0403f20..4ecc37c02d 100644 --- a/Monal/Classes/ContactDetails.swift +++ b/Monal/Classes/ContactDetails.swift @@ -285,15 +285,6 @@ struct ContactDetails: View { Text("Pin Chat") } - if contact.obj.isGroup && contact.obj.mucType == "group" { - NavigationLink(destination: LazyClosureView(MemberList(mucContact:contact))) { - Text("Group Members") - } - } else if contact.obj.isGroup && contact.obj.mucType == "channel" { - NavigationLink(destination: LazyClosureView(ChannelMemberList(mucContact:contact))) { - Text("Channel Members") - } - } #if !DISABLE_OMEMO if !HelperTools.isContactBlacklisted(forEncryption:contact.obj) { if !contact.isGroup { @@ -326,6 +317,16 @@ struct ContactDetails: View { NavigationLink(destination: LazyClosureView(BackgroundSettings(contact:contact, delegate:delegate))) { Text("Change Chat Background") } + + if contact.obj.isGroup && contact.obj.mucType == "group" { + NavigationLink(destination: LazyClosureView(MemberList(mucContact:contact))) { + Text("Group Members") + } + } else if contact.obj.isGroup && contact.obj.mucType == "channel" { + NavigationLink(destination: LazyClosureView(ChannelMemberList(mucContact:contact))) { + Text("Channel Members") + } + } } .listStyle(.plain) @@ -392,6 +393,8 @@ struct ContactDetails: View { Text("Yes"), action: { contact.obj.removeFromRoster() //this will dismiss the chatview via kMonalContactRemoved notification + //this will do nothing for contact details opened through group members list (which is fine!) + //NOTE: this holds for all delegate.dismiss() calls self.delegate.dismiss() } ) @@ -542,10 +545,10 @@ struct ContactDetails: View { Alert(title: alertPrompt.title, message: alertPrompt.message, dismissButton:.default(Text("Close"), action: { showAlert = false if self.success == true { - //close muc ui and leave chat ui of this muc if let callback = self.successCallback { callback() } + //close muc ui and leave chat ui of this muc if let activeChats = (UIApplication.shared.delegate as! MonalAppDelegate).activeChats { activeChats.presentChat(with:nil) } diff --git a/Monal/Classes/CreateGroupMenu.swift b/Monal/Classes/CreateGroupMenu.swift index aa3fa5b102..b83f96ed6c 100644 --- a/Monal/Classes/CreateGroupMenu.swift +++ b/Monal/Classes/CreateGroupMenu.swift @@ -14,21 +14,16 @@ import OrderedCollections struct CreateGroupMenu: View { private var appDelegate: MonalAppDelegate - + private var delegate: SheetDismisserProtocol @State private var connectedAccounts: [xmpp] @State private var selectedAccount: xmpp? @State private var groupName: String = "" - @State private var showAlert = false // note: dismissLabel is not accessed but defined at the .alert() section @State private var alertPrompt = AlertPrompt(dismissLabel: Text("Close")) - @State private var selectedContacts : OrderedSet> = [] - + @State private var selectedContacts: OrderedSet> = [] @State private var isEditingGroupName = false - @StateObject private var overlay = LoadingOverlayState() - - private var delegate: SheetDismisserProtocol init(delegate: SheetDismisserProtocol) { self.appDelegate = UIApplication.shared.delegate as! MonalAppDelegate @@ -66,9 +61,9 @@ struct CreateGroupMenu: View { .autocapitalization(.none) .addClearButton(isEditing: isEditingGroupName, text:$groupName) - NavigationLink(destination: LazyClosureView(ContactPicker(account: self.selectedAccount!, selectedContacts: $selectedContacts)), label: { - Text("Change Group Members") - }) + NavigationLink(destination: LazyClosureView(ContactPicker(account: self.selectedAccount!, selectedContacts: $selectedContacts))) { + Text("Change Group Members") + } Button(action: { guard let generatedJid = self.selectedAccount!.mucProcessor.generateMucJid() else { errorAlert(title: Text("Error creating group!"), message: Text("Your server does not provide a MUC component.")) diff --git a/Monal/Classes/DataLayer.m b/Monal/Classes/DataLayer.m index 626be21542..1effe8c0e5 100644 --- a/Monal/Classes/DataLayer.m +++ b/Monal/Classes/DataLayer.m @@ -529,7 +529,7 @@ -(NSDictionary* _Nullable) contactDictionaryForUsername:(NSString*) username for -(NSArray*) possibleGroupMembersForAccount:(NSNumber*) accountNo { return [self.db idReadTransaction:^{ - //list all contacts and group chats + //list all contacts without groupchats and self contact NSString* query = @"SELECT B.buddy_name, B.account_id, IFNULL(IFNULL(NULLIF(B.nick_name, ''), NULLIF(B.full_name, '')), B.buddy_name) FROM buddylist as B INNER JOIN account AS A ON A.account_id=B.account_id WHERE B.account_id=? AND B.muc=0 AND B.buddy_name != (A.username || '@' || A.domain)"; NSMutableArray* toReturn = [NSMutableArray new]; for(NSDictionary* dic in [self.db executeReader:query andArguments:@[accountNo]]) diff --git a/Monal/Classes/MemberList.swift b/Monal/Classes/MemberList.swift index c0d7c4e1d4..be6e3343a2 100644 --- a/Monal/Classes/MemberList.swift +++ b/Monal/Classes/MemberList.swift @@ -6,228 +6,181 @@ // Copyright © 2022 Monal.im. All rights reserved. // -import SwiftUI -import monalxmpp import OrderedCollections +struct ActionSheetPrompt { + var title: Text = Text("") + var message: Text = Text("") + var closure: ()->Void = { } +} + struct MemberList: View { private let account: xmpp private let ownAffiliation: String; @StateObject var group: ObservableKVOWrapper @State private var memberList: OrderedSet> - @State private var affiliation: Dictionary - @State private var openAccountSelection : Bool = false + @State private var affiliations: Dictionary, String> + @State private var navigationActive: ObservableKVOWrapper? @State private var showAlert = false @State private var alertPrompt = AlertPrompt(dismissLabel: Text("Close")) - @State private var selectedMember: MLContact? + @State private var showActionSheet = false + @State private var actionSheetPrompt = ActionSheetPrompt() init(mucContact: ObservableKVOWrapper) { - self.account = MLXMPPManager.sharedInstance().getConnectedAccount(forID: mucContact.accountId)! as xmpp + account = MLXMPPManager.sharedInstance().getConnectedAccount(forID: mucContact.accountId)! as xmpp _group = StateObject(wrappedValue: mucContact) _memberList = State(wrappedValue: getContactList(viewContact: mucContact)) - self.ownAffiliation = DataLayer.sharedInstance().getOwnAffiliation(inGroupOrChannel:mucContact.obj) ?? "none" - var affiliationTmp = Dictionary() - for memberInfo in Array(DataLayer.sharedInstance().getMembersAndParticipants(ofMuc: mucContact.contactJid, forAccountId: self.account.accountNo)) { + ownAffiliation = DataLayer.sharedInstance().getOwnAffiliation(inGroupOrChannel:mucContact.obj) ?? "none" + var affiliationTmp = Dictionary, String>() + for memberInfo in Array(DataLayer.sharedInstance().getMembersAndParticipants(ofMuc: mucContact.contactJid, forAccountId: account.accountNo)) { guard let jid = memberInfo["participant_jid"] as? String ?? memberInfo["member_jid"] as? String else { continue } - affiliationTmp.updateValue((memberInfo["affiliation"] as? String) ?? "none", forKey: jid) + let contact = ObservableKVOWrapper(MLContact.createContact(fromJid:jid, andAccountNo:account.accountNo)) + affiliationTmp[contact] = memberInfo["affiliation"] as? String ?? "none" } - _affiliation = State(wrappedValue: affiliationTmp) + _affiliations = State(wrappedValue: affiliationTmp) } - func showAlert(title: String, description: String) { - self.alertPrompt.title = Text(title) - self.alertPrompt.message = Text(description) + func showAlert(title: Text, description: Text) { + self.alertPrompt.title = title + self.alertPrompt.message = description self.showAlert = true } + + func showActionSheet(title: Text, description: Text, closure: @escaping ()->Void) { + self.actionSheetPrompt.title = title + self.actionSheetPrompt.message = description + self.actionSheetPrompt.closure = closure + self.showActionSheet = true + } func ownUserHasAffiliationToRemove(contact: ObservableKVOWrapper) -> Bool { - if contact.obj.contactJid == self.account.connectionProperties.identity.jid { + if contact.contactJid == account.connectionProperties.identity.jid { return false } - if let contactAffiliation = self.affiliation[contact.contactJid] { - if self.ownAffiliation == "owner" { + if let contactAffiliation = affiliations[contact] { + if ownAffiliation == "owner" { return true - } else if self.ownAffiliation == "admin" && contactAffiliation == "member" { + } else if ownAffiliation == "admin" && (contactAffiliation != "owner" && contactAffiliation != "admin") { return true } } return false } + + func affiliationToText(_ affiliation: String?) -> some View { + if let affiliation = affiliation { + if affiliation == "owner" { + return Text("Owner") + } else if affiliation == "admin" { + return Text("Admin") + } else if affiliation == "member" { + return Text("Member") + } else if affiliation == "outcast" { + return Text("Blocked") + } else if affiliation == "profile" { + return Text("Open contact details") + } + } + return Text("") + } var body: some View { List { Section(header: Text(self.group.obj.contactDisplayName)) { - if self.ownAffiliation == "owner" || self.ownAffiliation == "admin" { - NavigationLink(destination: LazyClosureView(ContactPicker(account: self.account, selectedContacts: $memberList, existingMembers: self.memberList)), label: { - Text("Add Group Members") - }) + if ownAffiliation == "owner" || ownAffiliation == "admin" { + NavigationLink(destination: LazyClosureView(ContactPicker(account: account, selectedContacts: $memberList, existingMembers: memberList))) { + Text("Add Group Members") + } } - ForEach(self.memberList, id: \.self.obj) { - contact in - HStack(alignment: .center) { - Image(uiImage: contact.avatar) - .resizable() - .frame(width: 40, height: 40, alignment: .center) - Text(contact.contactDisplayName as String) - Spacer() - if let contactAffiliation = self.affiliation[contact.contactJid] { - if contactAffiliation == "owner" { - Text(NSLocalizedString("Owner", comment: "muc affiliation")) - } else if contactAffiliation == "admin" { - Text(NSLocalizedString("Admin", comment: "muc affiliation")) - } else if contactAffiliation == "member" { - Text(NSLocalizedString("Member", comment: "muc affiliation")) - } else if contactAffiliation == "outcast" { - Text(NSLocalizedString("Outcast", comment: "muc affiliation")) + ForEach(memberList, id:\.self) { contact in + if !contact.isSelfChat { + HStack(alignment: .center) { + Image(uiImage: contact.avatar) + .resizable() + .frame(width: 40, height: 40, alignment: .center) + Text(contact.contactDisplayName as String) + + Spacer() + + if ownAffiliation == "owner" || ownAffiliation == "admin" { + Picker(selection: Binding( + get: { affiliations[contact] ?? "none" }, + set: { newAffiliation in + if newAffiliation == "profile" { + DDLogVerbose("Activating navigation to \(String(describing:contact))") + navigationActive = contact + } else if newAffiliation == "outcast" { + showActionSheet(title: Text("Block user?"), description: Text("Do you want to block this user from entering this group/channel?")) { + DDLogVerbose("Changing affiliation of \(String(describing:contact)) to: \(String(describing:newAffiliation))...") + affiliations[contact] = newAffiliation + account.mucProcessor.setAffiliation(newAffiliation, ofUser:contact.contactJid, inMuc:self.group.contactJid) + } + } else { + DDLogVerbose("Changing affiliation of \(String(describing:contact)) to: \(String(describing:newAffiliation))...") + affiliations[contact] = newAffiliation + account.mucProcessor.setAffiliation(newAffiliation, ofUser:contact.contactJid, inMuc:self.group.contactJid) + } + } + ), label: EmptyView()) { + ForEach(["profile", "owner", "admin", "member", "outcast"], id:\.self) { affiliation in + affiliationToText(affiliation).tag(affiliation) + } + } + .pickerStyle(.menu) } else { - Text(NSLocalizedString("", comment: "muc affiliation")) + affiliationToText(affiliations[contact]) } } + .deleteDisabled( + !ownUserHasAffiliationToRemove(contact: contact) + ) + //invisible navigation link triggered programmatically + .background( + NavigationLink(destination: LazyClosureView(ContactDetails(delegate:SheetDismisserProtocol(), contact:contact)), tag:contact, selection:$navigationActive) { EmptyView() } + .opacity(0) + ) } - .onTapGesture(perform: { - if contact.obj.contactJid != self.account.connectionProperties.identity.jid { - self.selectedMember = contact.obj - } - }) - .deleteDisabled( - !ownUserHasAffiliationToRemove(contact: contact) - ) } .onDelete(perform: { memberIdx in - let member = self.memberList[memberIdx.first!] - self.account.mucProcessor.setAffiliation("none", ofUser: member.contactJid, inMuc: self.group.contactJid) - - self.showAlert(title: "Member deleted", description: self.memberList[memberIdx.first!].contactJid) - self.memberList.remove(at: memberIdx.first!) - }) - } - .onChange(of: self.memberList) { [previousMemberList = self.memberList] newMemberList in - // only handle new members (added via the contact picker) - for member in newMemberList { - if !previousMemberList.contains(member) { - // add selected group member with affiliation member - affiliationChangeAction(member, affiliation: "member") - self.account.mucProcessor.inviteUser(member.contactJid, inMuc: self.group.contactJid) + let member = memberList[memberIdx.first!] + showActionSheet(title: Text("Remove user?"), description: self.group.mucType == "group" ? Text("Do you want to remove this user from this group? The user won't be able to enter it again until added back to the group.") : Text("Do you want to remove this user from this channel? The user will be able to enter it again.")) { + account.mucProcessor.setAffiliation("none", ofUser: member.contactJid, inMuc: self.group.contactJid) + self.showAlert(title: Text("User removed"), description: Text("\(memberList[memberIdx.first!].obj.contactJid)")) + memberList.remove(at: memberIdx.first!) } - } + }) } - .alert(isPresented: $showAlert, content: { - Alert(title: alertPrompt.title, message: alertPrompt.message, dismissButton: .default(alertPrompt.dismissLabel)) - }) - .sheet(item: self.$selectedMember, content: { selectedMemberUnobserved in - let selectedMember = ObservableKVOWrapper(selectedMemberUnobserved) - VStack { - Form { - Section { - HStack { - Spacer() - Image(uiImage: selectedMember.avatar) - .resizable() - .frame(width: 150, height: 150, alignment: .center) - Spacer() - } - HStack { - Spacer() - Text(selectedMember.contactDisplayName as String) - Spacer() - } - } - Section(header: Text("Configure Membership")) { - if self.ownAffiliation == "owner" && self.affiliation[selectedMember.contactJid] == "owner" { - makeAdmin(selectedMember) - makeMember(selectedMember) - removeUserButton(selectedMember) - block(selectedMember) - } - if self.ownAffiliation == "owner" && self.affiliation[selectedMember.contactJid] == "admin" { - makeOwner(selectedMember) - makeMember(selectedMember) - removeUserButton(selectedMember) - block(selectedMember) - } - if self.ownAffiliation == "owner" && self.affiliation[selectedMember.contactJid] == "member" { - makeOwner(selectedMember) - makeAdmin(selectedMember) - removeUserButton(selectedMember) - block(selectedMember) - } - if self.ownAffiliation == "admin" && self.affiliation[selectedMember.contactJid] == "member" { - removeUserButton(selectedMember) - block(selectedMember) - } - if (self.ownAffiliation == "admin" || self.ownAffiliation == "owner") && self.affiliation[selectedMember.contactJid] == "outcast" { - makeMember(selectedMember) - } - } - } - } - }) } - .navigationBarTitle("Group Members", displayMode: .inline) - } - - func removeUserButton(_ selectedMember: ObservableKVOWrapper) -> some View { - if #available(iOS 15, *) { - return Button(role: .destructive, action: { - self.account.mucProcessor.setAffiliation("none", ofUser: selectedMember.contactJid, inMuc: self.group.contactJid) - self.showAlert(title: "Member deleted", description: selectedMember.contactJid) - if let index = self.memberList.firstIndex(of: selectedMember) { - self.memberList.remove(at: index) + .onChange(of: memberList) { [previousMemberList = memberList] newMemberList in + // only handle new members (added via the contact picker) + for member in newMemberList { + if !previousMemberList.contains(member) { + // add selected group member with affiliation member + affiliations[member] = "member" + account.mucProcessor.setAffiliation("member", ofUser:member.contactJid, inMuc:self.group.contactJid) + account.mucProcessor.inviteUser(member.contactJid, inMuc: self.group.contactJid) } - self.selectedMember = nil - }) { - Text("Remove from group") } - } else { - return AnyView(EmptyView()) - } - } - - func affiliationChangeAction(_ selectedMember: ObservableKVOWrapper, affiliation: String) { - self.account.mucProcessor.setAffiliation(affiliation, ofUser: selectedMember.contactJid, inMuc: self.group.contactJid) - self.affiliation[selectedMember.contactJid] = affiliation - } - - func affiliationButton(_ selectedMember: ObservableKVOWrapper, affiliation: String, @ViewBuilder label: () -> Label) -> some View { - return Button(action: { - affiliationChangeAction(selectedMember, affiliation: affiliation) - // dismiss sheet - self.selectedMember = nil - }) { - label() } - } - - func makeOwner(_ selectedMember: ObservableKVOWrapper) -> some View { - return affiliationButton(selectedMember, affiliation: "owner", label: { - Text("Make owner") + .alert(isPresented: $showAlert, content: { + Alert(title: alertPrompt.title, message: alertPrompt.message, dismissButton: .default(alertPrompt.dismissLabel)) }) - } - - func makeAdmin(_ selectedMember: ObservableKVOWrapper) -> some View { - return affiliationButton(selectedMember, affiliation: "admin", label: { - Text("Make admin") - }) - } - - func makeMember(_ selectedMember: ObservableKVOWrapper) -> some View { - return affiliationButton(selectedMember, affiliation: "member", label: { - Text("Make member") - }) - } - - func block(_ selectedMember: ObservableKVOWrapper) -> AnyView { - if self.group.mucType != "group" { - return AnyView( - affiliationButton(selectedMember, affiliation: "outcast", label: { - Text("Block from group") - }) + .actionSheet(isPresented: $showActionSheet) { + ActionSheet( + title: actionSheetPrompt.title, + message: actionSheetPrompt.message, + buttons: [ + .cancel(), + .destructive( + Text("Yes"), + action: actionSheetPrompt.closure + ) + ] ) - } else { - return AnyView(EmptyView()) } + .navigationBarTitle("Group Members", displayMode: .inline) } } diff --git a/Monal/Classes/SwiftuiHelpers.swift b/Monal/Classes/SwiftuiHelpers.swift index 8a58e4547d..e7ff5206ba 100644 --- a/Monal/Classes/SwiftuiHelpers.swift +++ b/Monal/Classes/SwiftuiHelpers.swift @@ -66,6 +66,28 @@ class SheetDismisserProtocol: ObservableObject { } } +func getContactList(viewContact: (ObservableKVOWrapper?)) -> OrderedSet> { + if let contact = viewContact { + if(contact.isGroup && contact.mucType == "group") { + //this uses the account the muc belongs to and treats every other account to be remote, + //even when multiple accounts of the same monal instance are in the same group + var contactList : OrderedSet> = OrderedSet() + for memberInfo in Array(DataLayer.sharedInstance().getMembersAndParticipants(ofMuc: contact.contactJid, forAccountId: contact.accountId)) { + //jid can be participant_jid (if currently joined to muc) or member_jid (if not joined but member of muc) + guard let jid = memberInfo["participant_jid"] as? String ?? memberInfo["member_jid"] as? String else { + continue + } + contactList.append(ObservableKVOWrapper(MLContact.createContact(fromJid: jid, andAccountNo: contact.accountId))) + } + return contactList + } else { + return [contact] + } + } else { + return [] + } +} + //see here for some ideas used herein: https://blog.logrocket.com/adding-gifs-ios-app-flanimatedimage-swiftui/#using-flanimatedimage-with-swift struct GIFViewer: UIViewRepresentable { typealias UIViewType = FLAnimatedImageView @@ -501,29 +523,3 @@ class SwiftuiInterface : NSObject { return host } } - -func getContactList(viewContact: (ObservableKVOWrapper?)) -> OrderedSet> { - if let contact = viewContact { - if(contact.isGroup && contact.mucType == "group") { - //this uses the account the muc belongs to and treats every other account to be remote, even when multiple accounts of the same monal instance are in the same group - let jidList = Array(DataLayer.sharedInstance().getMembersAndParticipants(ofMuc: contact.contactJid, forAccountId: contact.accountId)) - var contactList : OrderedSet> = OrderedSet() - for jidDict in jidList { - //jid can be participant_jid (if currently joined to muc) or member_jid (if not joined but member of muc) - var jid : String? = jidDict["participant_jid"] as? String - if(jid == nil) { - jid = jidDict["member_jid"] as? String - } - if(jid != nil) { - let contact = MLContact.createContact(fromJid: jid!, andAccountNo: contact.accountId) - contactList.append(ObservableKVOWrapper(contact)) - } - } - return contactList - } else { - return [contact] - } - } else { - return [] - } -} diff --git a/Monal/Classes/chatViewController.m b/Monal/Classes/chatViewController.m index 57c9d7f821..34c78eb104 100644 --- a/Monal/Classes/chatViewController.m +++ b/Monal/Classes/chatViewController.m @@ -636,8 +636,21 @@ -(void) updateUIElements if(self.contact.isGroup) { NSArray* members = [[DataLayer sharedInstance] getMembersAndParticipantsOfMuc:self.contact.contactJid forAccountId:self.xmppAccount.accountNo]; - if(members.count > 0) - jidLabelText = [NSString stringWithFormat:@"%@ (%ld)", contactDisplayName, members.count]; + NSInteger membercount = members.count; + if([self.contact.mucType isEqualToString:@"group"]) + { + NSMutableSet* memberSet = [NSMutableSet new]; + for(NSDictionary* entry in members) + { + if(entry[@"participant_jid"] != nil) + [memberSet addObject:entry[@"participant_jid"]]; + if(entry[@"member_jid"] != nil) + [memberSet addObject:entry[@"member_jid"]]; + } + membercount = memberSet.count; + } + if(membercount > 1) + jidLabelText = [NSString stringWithFormat:@"%@ (%ld)", contactDisplayName, membercount - 1]; //don't count ourselves } // change text values dispatch_async(dispatch_get_main_queue(), ^{ From 27804e7951c6203764c478e6bfa532ea22c8d90d Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Sun, 5 May 2024 01:39:04 +0200 Subject: [PATCH 066/143] Fix contact picker and completely rework it --- Monal/Classes/ContactPicker.swift | 71 ++++++++++++++--------------- Monal/Classes/CreateGroupMenu.swift | 2 +- Monal/Classes/MemberList.swift | 2 +- 3 files changed, 37 insertions(+), 38 deletions(-) diff --git a/Monal/Classes/ContactPicker.swift b/Monal/Classes/ContactPicker.swift index a58279fde0..f3fec92b4b 100644 --- a/Monal/Classes/ContactPicker.swift +++ b/Monal/Classes/ContactPicker.swift @@ -41,37 +41,40 @@ struct ContactPickerEntry: View { struct ContactPicker: View { @Environment(\.presentationMode) private var presentationMode - - @State var contacts: OrderedSet> - let account: xmpp - @Binding var selectedContacts: OrderedSet> - let existingMembers: OrderedSet> + @Binding var returnedContacts: OrderedSet> + @State var allContacts: OrderedSet> + @State var selectedContacts: OrderedSet> @State var searchText = "" + @State var isEditingSearchInput = false + let allowRemoval: Bool - @State var isEditingSearchInput: Bool = false - - init(account: xmpp, selectedContacts: Binding>>) { - self.init(account: account, selectedContacts: selectedContacts, existingMembers: OrderedSet()) - } - - init(account: xmpp, selectedContacts: Binding>>, existingMembers: OrderedSet>) { - self.account = account - self._selectedContacts = selectedContacts - self.existingMembers = existingMembers - + init(_ account: xmpp, binding returnedContacts: Binding>>, allowRemoval: Bool = true) { + self.allowRemoval = allowRemoval var contactsTmp: OrderedSet> = OrderedSet() + + //build currently selected list of contacts + contactsTmp.removeAll() + for contact in returnedContacts.wrappedValue { + contactsTmp.append(contact) + } + _selectedContacts = State(wrappedValue: contactsTmp) + + //build list of all possible contacts on this account (excluding selfchat and other mucs) + contactsTmp.removeAll() for contact in DataLayer.sharedInstance().possibleGroupMembers(forAccount: account.accountNo) { contactsTmp.append(ObservableKVOWrapper(contact)) } - _contacts = State(wrappedValue: contactsTmp) + _allContacts = State(wrappedValue: contactsTmp) + + _returnedContacts = returnedContacts } private var searchResults : OrderedSet> { if searchText.isEmpty { - return self.contacts + return self.allContacts } else { var filteredContacts: OrderedSet> = OrderedSet() - for contact in self.contacts { + for contact in self.allContacts { if (contact.contactDisplayName as String).lowercased().contains(searchText.lowercased()) || (contact.contactJid as String).contains(searchText.lowercased()) { filteredContacts.append(contact) @@ -82,26 +85,24 @@ struct ContactPicker: View { } var body: some View { - if(contacts.isEmpty) { + if(allContacts.isEmpty) { Text("No contacts to show :(") .navigationTitle("Contact Lists") } else { - List { - ForEach(searchResults, id: \.self.obj) { contact in - let contactIsSelected = self.selectedContacts.contains(contact); - let contactIsAlreadyMember = self.existingMembers.contains(contact); - ContactPickerEntry(contact: contact, isPicked: contactIsSelected, isExistingMember: contactIsAlreadyMember) - .onTapGesture(perform: { + List(searchResults) { contact in + let contactIsSelected = self.selectedContacts.contains(contact); + let contactIsAlreadyMember = self.returnedContacts.contains(contact); + ContactPickerEntry(contact: contact, isPicked: contactIsSelected, isExistingMember: !(!contactIsAlreadyMember || allowRemoval)) + .onTapGesture { // only allow changes to members that are not already part of the group - if(!contactIsAlreadyMember) { + if(!contactIsAlreadyMember || allowRemoval) { if(contactIsSelected) { self.selectedContacts.remove(contact) } else { self.selectedContacts.append(contact) } } - }) - } + } } .applyClosure { view in if #available(iOS 15.0, *) { @@ -111,13 +112,11 @@ struct ContactPicker: View { } } .listStyle(.inset) - .navigationBarTitle("Contact Selection", displayMode: .inline) - .navigationBarBackButtonHidden(true) - .toolbar { - ToolbarItem(placement: .navigationBarLeading) { - Button("Back", action: { - self.presentationMode.wrappedValue.dismiss() - }) + .navigationBarTitle(NSLocalizedString("Contact Selection", comment: ""), displayMode: .inline) + .onDisappear { + returnedContacts.removeAll() + for contact in selectedContacts { + returnedContacts.append(contact) } } } diff --git a/Monal/Classes/CreateGroupMenu.swift b/Monal/Classes/CreateGroupMenu.swift index b83f96ed6c..6a58f2ae5e 100644 --- a/Monal/Classes/CreateGroupMenu.swift +++ b/Monal/Classes/CreateGroupMenu.swift @@ -61,7 +61,7 @@ struct CreateGroupMenu: View { .autocapitalization(.none) .addClearButton(isEditing: isEditingGroupName, text:$groupName) - NavigationLink(destination: LazyClosureView(ContactPicker(account: self.selectedAccount!, selectedContacts: $selectedContacts))) { + NavigationLink(destination: LazyClosureView(ContactPicker(self.selectedAccount!, binding: $selectedContacts))) { Text("Change Group Members") } Button(action: { diff --git a/Monal/Classes/MemberList.swift b/Monal/Classes/MemberList.swift index be6e3343a2..1d1c7f17a9 100644 --- a/Monal/Classes/MemberList.swift +++ b/Monal/Classes/MemberList.swift @@ -90,7 +90,7 @@ struct MemberList: View { List { Section(header: Text(self.group.obj.contactDisplayName)) { if ownAffiliation == "owner" || ownAffiliation == "admin" { - NavigationLink(destination: LazyClosureView(ContactPicker(account: account, selectedContacts: $memberList, existingMembers: memberList))) { + NavigationLink(destination: LazyClosureView(ContactPicker(account, binding: $memberList, allowRemoval: false))) { Text("Add Group Members") } } From 95f2d45572d3f8f396f759555d57932950c73153 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Sun, 5 May 2024 01:44:13 +0200 Subject: [PATCH 067/143] Slight overhaul of create group menu --- Monal/Classes/CreateGroupMenu.swift | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/Monal/Classes/CreateGroupMenu.swift b/Monal/Classes/CreateGroupMenu.swift index 6a58f2ae5e..b884a31844 100644 --- a/Monal/Classes/CreateGroupMenu.swift +++ b/Monal/Classes/CreateGroupMenu.swift @@ -61,9 +61,6 @@ struct CreateGroupMenu: View { .autocapitalization(.none) .addClearButton(isEditing: isEditingGroupName, text:$groupName) - NavigationLink(destination: LazyClosureView(ContactPicker(self.selectedAccount!, binding: $selectedContacts))) { - Text("Change Group Members") - } Button(action: { guard let generatedJid = self.selectedAccount!.mucProcessor.generateMucJid() else { errorAlert(title: Text("Error creating group!"), message: Text("Your server does not provide a MUC component.")) @@ -104,15 +101,17 @@ struct CreateGroupMenu: View { Text("Create new group") }) } - if self.selectedContacts.count > 0 { - Section(header: Text("Selected Group Members")) { - ForEach(self.selectedContacts, id: \.obj.contactJid) { contact in - ContactEntry(contact: contact) - } - .onDelete(perform: { indexSet in - self.selectedContacts.remove(at: indexSet.first!) - }) + + Section(header: Text("Selected Group Members")) { + NavigationLink(destination: LazyClosureView(ContactPicker(self.selectedAccount!, binding: $selectedContacts))) { + Text("Change Group Members") } + ForEach(self.selectedContacts, id: \.obj.contactJid) { contact in + ContactEntry(contact: contact) + } + .onDelete(perform: { indexSet in + self.selectedContacts.remove(at: indexSet.first!) + }) } } } @@ -122,7 +121,7 @@ struct CreateGroupMenu: View { })) } .addLoadingOverlay(overlay) - .navigationBarTitle("Create new group", displayMode: .inline) + .navigationBarTitle(NSLocalizedString("Create new group", comment:""), displayMode: .inline) .navigationViewStyle(.stack) } } From dde2972d2307db4295b59b454c9941c7182fb2a8 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Sun, 5 May 2024 02:35:30 +0200 Subject: [PATCH 068/143] Clean up swiftui implementations This changes every display of a contact with ContactEntry() to not replicate the same code over and over again. --- Monal/Classes/AccountPicker.swift | 17 ++----------- Monal/Classes/BackgroundSettings.swift | 4 ---- Monal/Classes/ChannelMemberList.swift | 2 -- Monal/Classes/ChatPlaceholder.swift | 2 -- Monal/Classes/ContactEntry.swift | 18 ++++++++++---- Monal/Classes/ContactPicker.swift | 10 +------- Monal/Classes/ContactRequestsMenu.swift | 3 --- Monal/Classes/CreateGroupMenu.swift | 4 ---- Monal/Classes/EditGroupSubject.swift | 2 -- Monal/Classes/LoadingOverlay.swift | 3 --- Monal/Classes/MLContact.h | 1 + Monal/Classes/MLContact.m | 28 ++++++++++++++++++---- Monal/Classes/MLQRCodeScanner.swift | 4 ---- Monal/Classes/MemberList.swift | 5 +--- Monal/Classes/OmemoKeys.swift | 4 +--- Monal/Classes/OmemoQrCodeView.swift | 2 -- Monal/Classes/QRCodeScannerLoginView.swift | 2 -- Monal/Classes/RichAlert.swift | 1 - Monal/Classes/WelcomeLogIn.swift | 3 --- Monal/Classes/ZoomableContainer.swift | 4 ---- 20 files changed, 43 insertions(+), 76 deletions(-) diff --git a/Monal/Classes/AccountPicker.swift b/Monal/Classes/AccountPicker.swift index f2c2520e59..2b321cdc9c 100644 --- a/Monal/Classes/AccountPicker.swift +++ b/Monal/Classes/AccountPicker.swift @@ -6,9 +6,6 @@ // Copyright © 2023 monal-im.org. All rights reserved. // -import SwiftUI -import monalxmpp - struct AccountPicker: View { let delegate: SheetDismisserProtocol let contacts: [MLContact] @@ -43,25 +40,15 @@ struct AccountPicker: View { .frame(maxWidth: .infinity) .background(Color(UIColor.systemBackground)) - let appDelegate = UIApplication.shared.delegate as! MonalAppDelegate List { ForEach(contacts) { contact in if let accountEntry = DataLayer.sharedInstance().details(forAccount:contact.accountId) { let accountJid = "\(accountEntry["username"] ?? "" as NSString)@\(accountEntry["domain"] ?? "" as NSString)" - let accountDisplayName = MLContact.ownDisplayName(forAccount:MLXMPPManager.sharedInstance().getConnectedAccount(forID: contact.accountId)!) as String let accountContact = MLContact.createContact(fromJid:accountJid, andAccountNo:accountEntry["account_id"] as! NSNumber) Button { - appDelegate.activeChats!.call(contact, with:callType) + (UIApplication.shared.delegate as! MonalAppDelegate).activeChats!.call(contact, with:callType) } label: { - HStack(alignment: .center) { - Image(uiImage: MLImageManager.sharedInstance().getIconFor(accountContact)!) - .resizable() - .frame(width: 40, height: 40, alignment: .center) - VStack(alignment: .leading) { - Text(accountDisplayName) - Text(accountJid).font(.footnote).opacity(0.6) - } - } + ContactEntry(contact:ObservableKVOWrapper(accountContact), selfnotesPrefix:false) } } } diff --git a/Monal/Classes/BackgroundSettings.swift b/Monal/Classes/BackgroundSettings.swift index bbb0985c9f..12b45d6081 100644 --- a/Monal/Classes/BackgroundSettings.swift +++ b/Monal/Classes/BackgroundSettings.swift @@ -6,10 +6,6 @@ // Copyright © 2022 monal-im.org. All rights reserved. // -import SwiftUI -import UniformTypeIdentifiers -import monalxmpp - @ViewBuilder func title(contact: ObservableKVOWrapper?) -> some View { if let contact = contact { diff --git a/Monal/Classes/ChannelMemberList.swift b/Monal/Classes/ChannelMemberList.swift index d25ea59078..3258d56e22 100644 --- a/Monal/Classes/ChannelMemberList.swift +++ b/Monal/Classes/ChannelMemberList.swift @@ -6,8 +6,6 @@ // Copyright © 2024 monal-im.org. All rights reserved. // -import SwiftUI -import monalxmpp import OrderedCollections struct ChannelMemberList: View { diff --git a/Monal/Classes/ChatPlaceholder.swift b/Monal/Classes/ChatPlaceholder.swift index 3888438dbe..cd6a407995 100644 --- a/Monal/Classes/ChatPlaceholder.swift +++ b/Monal/Classes/ChatPlaceholder.swift @@ -6,8 +6,6 @@ // Copyright © 2022 monal-im.org. All rights reserved. // -import SwiftUI - struct ChatPlaceholder: View { @Environment(\.colorScheme) var colorScheme var body: some View { diff --git a/Monal/Classes/ContactEntry.swift b/Monal/Classes/ContactEntry.swift index 9ff3eac03f..4171715a22 100644 --- a/Monal/Classes/ContactEntry.swift +++ b/Monal/Classes/ContactEntry.swift @@ -6,11 +6,15 @@ // Copyright © 2023 monal-im.org. All rights reserved. // -import SwiftUI - struct ContactEntry: View { - let contact : ObservableKVOWrapper - + let contact: ObservableKVOWrapper + let selfnotesPrefix: Bool + + init(contact:ObservableKVOWrapper, selfnotesPrefix: Bool = true) { + self.contact = contact + self.selfnotesPrefix = selfnotesPrefix + } + var body:some View { ZStack(alignment: .topLeading) { HStack(alignment: .center) { @@ -18,7 +22,11 @@ struct ContactEntry: View { .resizable() .frame(width: 40, height: 40, alignment: .center) VStack(alignment: .leading) { - Text(contact.contactDisplayName as String) + if selfnotesPrefix { + Text(contact.contactDisplayName as String) + } else { + Text(contact.contactDisplayNameWithoutSelfnotesPrefix as String) + } Text(contact.contactJid as String).font(.footnote).opacity(0.6) } } diff --git a/Monal/Classes/ContactPicker.swift b/Monal/Classes/ContactPicker.swift index f3fec92b4b..0ca4f833e2 100644 --- a/Monal/Classes/ContactPicker.swift +++ b/Monal/Classes/ContactPicker.swift @@ -6,8 +6,6 @@ // Copyright © 2022 monal-im.org. All rights reserved. // -import SwiftUI -import monalxmpp import OrderedCollections struct ContactPickerEntry: View { @@ -27,13 +25,7 @@ struct ContactPickerEntry: View { } else { Image(systemName: "circle") } - Image(uiImage: contact.avatar) - .resizable() - .frame(width: 40, height: 40, alignment: .center) - VStack(alignment: .leading) { - Text(contact.contactDisplayName as String) - Text(contact.contactJid as String).font(.footnote).opacity(0.6) - } + ContactEntry(contact: contact) } } } diff --git a/Monal/Classes/ContactRequestsMenu.swift b/Monal/Classes/ContactRequestsMenu.swift index ebce742ba8..80c873ff1a 100644 --- a/Monal/Classes/ContactRequestsMenu.swift +++ b/Monal/Classes/ContactRequestsMenu.swift @@ -6,9 +6,6 @@ // Copyright © 2022 monal-im.org. All rights reserved. // -import SwiftUI -import monalxmpp - struct ContactRequestsMenuEntry: View { let contact : MLContact let doDelete: () -> () diff --git a/Monal/Classes/CreateGroupMenu.swift b/Monal/Classes/CreateGroupMenu.swift index b884a31844..f0f41f7d0e 100644 --- a/Monal/Classes/CreateGroupMenu.swift +++ b/Monal/Classes/CreateGroupMenu.swift @@ -6,10 +6,6 @@ // Copyright © 2022 monal-im.org. All rights reserved. // -import MobileCoreServices -import UniformTypeIdentifiers -import SwiftUI -import monalxmpp import OrderedCollections struct CreateGroupMenu: View { diff --git a/Monal/Classes/EditGroupSubject.swift b/Monal/Classes/EditGroupSubject.swift index 7647694151..f90e1eee7c 100644 --- a/Monal/Classes/EditGroupSubject.swift +++ b/Monal/Classes/EditGroupSubject.swift @@ -6,8 +6,6 @@ // Copyright © 2024 monal-im.org. All rights reserved. // -import SwiftUI - struct EditGroupSubject: View { @StateObject var contact: ObservableKVOWrapper private let account: xmpp? diff --git a/Monal/Classes/LoadingOverlay.swift b/Monal/Classes/LoadingOverlay.swift index 49450045a0..ac713970bc 100644 --- a/Monal/Classes/LoadingOverlay.swift +++ b/Monal/Classes/LoadingOverlay.swift @@ -6,9 +6,6 @@ // Copyright © 2022 Monal.im. All rights reserved. // -import SwiftUI -import monalxmpp - //data class for overlay state class LoadingOverlayState : ObservableObject { var enabled: Bool diff --git a/Monal/Classes/MLContact.h b/Monal/Classes/MLContact.h index 0095916050..6741dfb9f1 100644 --- a/Monal/Classes/MLContact.h +++ b/Monal/Classes/MLContact.h @@ -91,6 +91,7 @@ FOUNDATION_EXPORT NSString* const kAskSubscribe; @property (nonatomic, readonly) NSString* ask; //whether we have tried to subscribe @property (nonatomic, readonly) NSString* contactDisplayName; +@property (nonatomic, readonly) NSString* contactDisplayNameWithoutSelfnotesPrefix; -(NSString*) contactDisplayNameWithFallback:(NSString* _Nullable) fallbackName; -(void) updateWithContact:(MLContact*) contact; diff --git a/Monal/Classes/MLContact.m b/Monal/Classes/MLContact.m index 687c614f1f..499559b31c 100644 --- a/Monal/Classes/MLContact.m +++ b/Monal/Classes/MLContact.m @@ -305,6 +305,11 @@ -(void) updateUnreadCount } -(NSString*) contactDisplayNameWithFallback:(NSString* _Nullable) fallbackName; +{ + return [self contactDisplayNameWithFallback:fallbackName andSelfnotesPrefix:YES]; +} + +-(NSString*) contactDisplayNameWithFallback:(NSString* _Nullable) fallbackName andSelfnotesPrefix:(BOOL) hasSelfnotesPrefix { DDLogVerbose(@"Calculating contact display name..."); NSString* displayName; @@ -338,11 +343,16 @@ -(NSString*) contactDisplayNameWithFallback:(NSString* _Nullable) fallbackName; else { xmpp* account = [[MLXMPPManager sharedInstance] getConnectedAccountForID:self.accountId]; - //add "Note to self: " prefix for selfchats - if([[DataLayer sharedInstance] enabledAccountCnts].intValue > 1) - displayName = [NSString stringWithFormat:NSLocalizedString(@"Notes to self: %@", @""), [[self class] ownDisplayNameForAccount:account]]; + if(hasSelfnotesPrefix) + { + //add "Note to self: " prefix for selfchats + if([[DataLayer sharedInstance] enabledAccountCnts].intValue > 1) + displayName = [NSString stringWithFormat:NSLocalizedString(@"Notes to self: %@", @""), [[self class] ownDisplayNameForAccount:account]]; + else + displayName = NSLocalizedString(@"Notes to self", @""); + } else - displayName = NSLocalizedString(@"Notes to self", @""); + displayName = [[self class] ownDisplayNameForAccount:account]; } DDLogVerbose(@"Calculated contactDisplayName for '%@': %@", self.contactJid, displayName); @@ -365,6 +375,16 @@ +(NSSet*) keyPathsForValuesAffectingContactDisplayName return [NSSet setWithObjects:@"nickName", @"fullName", @"contactJid", nil]; } +-(NSString*) contactDisplayNameWithoutSelfnotesPrefix +{ + return [self contactDisplayNameWithFallback:nil andSelfnotesPrefix:NO]; +} + ++(NSSet*) keyPathsForValuesAffectingContactDisplayNameWithoutSelfnotesPrefix +{ + return [NSSet setWithObjects:@"nickName", @"fullName", @"contactJid", nil]; +} + -(NSString*) nickNameView { return nilDefault(self.nickName, @""); diff --git a/Monal/Classes/MLQRCodeScanner.swift b/Monal/Classes/MLQRCodeScanner.swift index d52f8c0a1d..db5f92eeee 100644 --- a/Monal/Classes/MLQRCodeScanner.swift +++ b/Monal/Classes/MLQRCodeScanner.swift @@ -6,10 +6,6 @@ // Copyright © 2020 Monal.im. All rights reserved. // -import CocoaLumberjack -import AVFoundation -import UIKit -import SwiftUI import SafariServices @objc protocol MLLQRCodeScannerAccountLoginDelegate : AnyObject diff --git a/Monal/Classes/MemberList.swift b/Monal/Classes/MemberList.swift index 1d1c7f17a9..bc126d5618 100644 --- a/Monal/Classes/MemberList.swift +++ b/Monal/Classes/MemberList.swift @@ -97,10 +97,7 @@ struct MemberList: View { ForEach(memberList, id:\.self) { contact in if !contact.isSelfChat { HStack(alignment: .center) { - Image(uiImage: contact.avatar) - .resizable() - .frame(width: 40, height: 40, alignment: .center) - Text(contact.contactDisplayName as String) + ContactEntry(contact:contact) Spacer() diff --git a/Monal/Classes/OmemoKeys.swift b/Monal/Classes/OmemoKeys.swift index 8621abc8dd..19aaf5bc28 100644 --- a/Monal/Classes/OmemoKeys.swift +++ b/Monal/Classes/OmemoKeys.swift @@ -5,10 +5,8 @@ // Created by Jan on 04.05.22. // Copyright © 2022 Monal.im. All rights reserved. // -import UniformTypeIdentifiers -import SwiftUI + import OrderedCollections -import monalxmpp struct OmemoKeysEntry: View { private let contactJid: String diff --git a/Monal/Classes/OmemoQrCodeView.swift b/Monal/Classes/OmemoQrCodeView.swift index fc880295aa..e3b59b09f4 100644 --- a/Monal/Classes/OmemoQrCodeView.swift +++ b/Monal/Classes/OmemoQrCodeView.swift @@ -6,9 +6,7 @@ // Copyright © 2022 Monal.im. All rights reserved. // -import SwiftUI import CoreImage.CIFilterBuiltins -import monalxmpp func createQrCode(value: String) -> UIImage { diff --git a/Monal/Classes/QRCodeScannerLoginView.swift b/Monal/Classes/QRCodeScannerLoginView.swift index fcb0b33296..913a4b283c 100644 --- a/Monal/Classes/QRCodeScannerLoginView.swift +++ b/Monal/Classes/QRCodeScannerLoginView.swift @@ -6,8 +6,6 @@ // Copyright © 2022 Monal.im. All rights reserved. // -import SwiftUI - struct QRCodeScannerLoginView: UIViewControllerRepresentable { @Binding private var account : String @Binding private var password : String diff --git a/Monal/Classes/RichAlert.swift b/Monal/Classes/RichAlert.swift index 4b2a12a329..0c0983f08c 100644 --- a/Monal/Classes/RichAlert.swift +++ b/Monal/Classes/RichAlert.swift @@ -6,7 +6,6 @@ // Copyright © 2022 monal-im.org. All rights reserved. // -import SwiftUI import ViewExtractor struct RichAlertView: ViewModifier where TitleContent: View, BodyContent: View, ButtonContent: View { diff --git a/Monal/Classes/WelcomeLogIn.swift b/Monal/Classes/WelcomeLogIn.swift index 765852a813..0379ee5d73 100644 --- a/Monal/Classes/WelcomeLogIn.swift +++ b/Monal/Classes/WelcomeLogIn.swift @@ -6,9 +6,6 @@ // Copyright © 2022 Monal.im. All rights reserved. // -import SwiftUI -import monalxmpp - struct WelcomeLogIn: View { static private let credFaultyPattern = "^.+@.+\\..{2,}$" diff --git a/Monal/Classes/ZoomableContainer.swift b/Monal/Classes/ZoomableContainer.swift index 9a1884c410..64d9e40e2f 100644 --- a/Monal/Classes/ZoomableContainer.swift +++ b/Monal/Classes/ZoomableContainer.swift @@ -6,10 +6,6 @@ // Copyright © 2023 monal-im.org. All rights reserved. // -import Foundation -import UIKit -import SwiftUI - //based upon: https://stackoverflow.com/a/76649224/3528174 struct ZoomableContainer: View { let content: Content From d65bc847a65db964f150cd781a9286562dd0bc36 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Tue, 7 May 2024 03:52:05 +0200 Subject: [PATCH 069/143] Fix mds bug when receiving while not in catchup --- Monal/Classes/MLPubSubProcessor.m | 2 +- Monal/Classes/xmpp.m | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/Monal/Classes/MLPubSubProcessor.m b/Monal/Classes/MLPubSubProcessor.m index d06615ecde..7dc4527be3 100644 --- a/Monal/Classes/MLPubSubProcessor.m +++ b/Monal/Classes/MLPubSubProcessor.m @@ -33,7 +33,7 @@ -(NSString*) calculateNickForMuc:(NSString*) room; @implementation MLPubSubProcessor $$class_handler(mdsHandler, $$ID(xmpp*, account), $$ID(NSString*, jid), $$ID(NSString*, type), $_ID((NSDictionary*), data)) - DDLogDebug(@"Got new mds displayed status from '%@'", jid); + DDLogDebug(@"Got new mds displayed status from '%@' (should be own jid)...", jid); if(![jid isEqualToString:account.connectionProperties.identity.jid]) { DDLogWarn(@"Ignoring mds update not coming from our own jid"); diff --git a/Monal/Classes/xmpp.m b/Monal/Classes/xmpp.m index b548940207..ae9cdc2531 100644 --- a/Monal/Classes/xmpp.m +++ b/Monal/Classes/xmpp.m @@ -5349,7 +5349,18 @@ -(void) handleFinishedCatchup -(void) updateMdsData:(NSDictionary*) mdsData { for(NSString* jid in mdsData) + { + //update cached data _mdsData[jid] = mdsData[jid]; + + //handle mds update directly, if not in catchup for this jid + //everything else will be handled once the catchup is finished + NSString* catchupJid = self.connectionProperties.identity.jid; + if([[DataLayer sharedInstance] isBuddyMuc:jid forAccount:self.accountNo]) + catchupJid = jid; + if(_inCatchup[catchupJid] == nil && _mdsData[jid] != nil) + [self handleMdsData:_mdsData[jid] forJid:jid]; + } } -(void) handleMdsData:(MLXMLNode*) data forJid:(NSString*) jid From 28f67ad248806af71164aa7e9804a41f0e53df55 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Tue, 30 Apr 2024 18:48:28 +0200 Subject: [PATCH 070/143] Improve WebRTC handling and add macOS CallKit experiment - Add com.apple.security.network.server entitlement to allow WebRTC to open listening sockets - Implement non-xep conforming TCP transport for ICE candidates only active in alpha builds - Bump WebRTC loglevel to verbose in alpha builds (it's info otherwise) - Activate VoIP for alpha builds on macOS - Circumvent CallKit audio session activation problems on macOS Catalyst to activate VoIP as much as possible on macOS --- Monal/Alpha.Monal.macos.entitlements | 2 + Monal/Classes/HelperTools.m | 6 ++- Monal/Classes/MLCall.m | 50 +++++++++++++++++- Monal/Classes/WebRTCClient.swift | 4 ++ Monal/Monal.ios.entitlements | 2 + .../NotificationService/NotificationService.m | 51 ++++++++++--------- rust/sdp-to-jingle/src/xep_0176.rs | 4 +- 7 files changed, 91 insertions(+), 28 deletions(-) diff --git a/Monal/Alpha.Monal.macos.entitlements b/Monal/Alpha.Monal.macos.entitlements index f8560de197..9134ea69aa 100644 --- a/Monal/Alpha.Monal.macos.entitlements +++ b/Monal/Alpha.Monal.macos.entitlements @@ -22,6 +22,8 @@ com.apple.security.network.client + com.apple.security.network.server + com.apple.security.personal-information.location keychain-access-groups diff --git a/Monal/Classes/HelperTools.m b/Monal/Classes/HelperTools.m index 0054d9e159..aee39aa79e 100644 --- a/Monal/Classes/HelperTools.m +++ b/Monal/Classes/HelperTools.m @@ -648,9 +648,11 @@ +(NSURL*) getFailoverTurnApiServer +(BOOL) shouldProvideVoip { - BOOL shouldProvideVoip; + BOOL shouldProvideVoip = NO; #if TARGET_OS_MACCATALYST - shouldProvideVoip = NO; +#ifdef IS_ALPHA + shouldProvideVoip = YES; +#endif #else shouldProvideVoip = YES; #endif diff --git a/Monal/Classes/MLCall.m b/Monal/Classes/MLCall.m index 186a65c204..9731a95a49 100644 --- a/Monal/Classes/MLCall.m +++ b/Monal/Classes/MLCall.m @@ -399,6 +399,13 @@ -(void) setIsConnected:(BOOL) isConnected if(self.isConnected && self.audioSession != nil) [self startCallDuartionTimer]; } + +#ifdef IS_ALPHA +#if TARGET_OS_MACCATALYST + //set audio session to default one + self.audioSession = [AVAudioSession sharedInstance]; +#endif +#endif } -(BOOL) isConnected { @@ -415,7 +422,13 @@ -(void) setAudioSession:(AVAudioSession*) audioSession DDLogWarn(@"Trying to activate same audio session a second time, ignoring..."); return; } - if(audioSession != nil) + BOOL assertActivated = YES; +#ifdef IS_ALPHA +#if TARGET_OS_MACCATALYST + assertActivated = NO; +#endif +#endif + if(assertActivated && audioSession != nil) MLAssert(_audioSession == nil, @"Audio session should never be activated without deactivating old audio session first!", (@{ @"oldAudioSession": nilWrapper(_audioSession), @"newAudioSession": nilWrapper(audioSession), @@ -1110,6 +1123,25 @@ -(void) webRTCClient:(WebRTCClient*) webRTCClient didDiscoverLocalCandidate:(RTC DDLogError(@"Failed to convert raw sdp candidate to jingle, ignoring this candidate: %@", candidate); return; } +#ifdef IS_ALPHA + if([contentNode check:@"{urn:xmpp:jingle:transports:ice-udp:1}transport/candidate"]) + { + //add tcptype because that attribute is apparently not supported by our mozilla sdp lib + MLXMLNode* candidateNode = [contentNode findFirst:@"{urn:xmpp:jingle:transports:ice-udp:1}transport/candidate"]; + if([candidate.sdp containsString:@"typ host tcptype active"]) + candidateNode.attributes[@"tcptype"] = @"active"; + else if([candidate.sdp containsString:@"typ host tcptype passive"]) + candidateNode.attributes[@"tcptype"] = @"passive"; + else + DDLogWarn(@"Unknown type-tcptype combination!"); + } +#else + if([contentNode check:@"{urn:xmpp:jingle:transports:ice-udp:1}transport/candidate"]) + { + DDLogError(@"Ignoring raw sdp candidate, because it's using tcp instead of udp: %@", candidate); + return; + } +#endif //see https://webrtc.googlesource.com/src/+/refs/heads/main/sdk/objc/api/peerconnection/RTCIceCandidate.h XMPPIQ* candidateIq = [[XMPPIQ alloc] initWithType:kiqSetType to:self.fullRemoteJid]; [candidateIq addChildNode:[[MLXMLNode alloc] initWithElement:@"jingle" andNamespace:@"urn:xmpp:jingle:1" withAttributes:@{ @@ -1269,6 +1301,22 @@ -(void) processRemoteICECandidate:(XMPPIQ*) iqNode { RTCIceCandidate* incomingCandidate = nil; NSString* rawSdp = [HelperTools xml2candidate:[iqNode findFirst:@"{urn:xmpp:jingle:1}jingle"] withInitiator:self.direction==MLCallDirectionIncoming]; +#ifdef IS_ALPHA + if([iqNode check:@"{urn:xmpp:jingle:1}jingle/content/{urn:xmpp:jingle:transports:ice-udp:1}transport/candidate"]) + { + NSString* type = [iqNode findFirst:@"{urn:xmpp:jingle:1}jingle/content/{urn:xmpp:jingle:transports:ice-udp:1}transport/candidate@type"]; + NSString* tcptype = [iqNode findFirst:@"{urn:xmpp:jingle:1}jingle/content/{urn:xmpp:jingle:transports:ice-udp:1}transport/candidate@tcptype"]; + DDLogDebug(@"Patching raw sdp type=%@ to contain tcptype: %@", type, tcptype); + rawSdp = [rawSdp stringByReplacingOccurrencesOfString:[NSString stringWithFormat:@"typ %@", type] withString:[NSString stringWithFormat:@"typ %@ tcptype %@", type, tcptype]]; + } +#else + if([iqNode check:@"{urn:xmpp:jingle:1}jingle/content/{urn:xmpp:jingle:transports:ice-udp:1}transport/candidate"]) + { + DDLogWarn(@"Got tcp candidate, ignoring: %@", [iqNode findFirst:@"{urn:xmpp:jingle:1}jingle/content/{urn:xmpp:jingle:transports:ice-udp:1}transport/candidate"]); + rawSdp = nil; + } +#endif + DDLogVerbose(@"Got raw remote sdp: %@", rawSdp); if(rawSdp == nil) { DDLogError(@"Failed to convert jingle candidate to raw sdp!"); diff --git a/Monal/Classes/WebRTCClient.swift b/Monal/Classes/WebRTCClient.swift index 481ed73ad7..5805a5f072 100644 --- a/Monal/Classes/WebRTCClient.swift +++ b/Monal/Classes/WebRTCClient.swift @@ -90,7 +90,11 @@ final class WebRTCClient: NSObject { @objc required init(iceServers: [RTCIceServer], audioOnly: Bool, forceRelay: Bool) { +#if IS_ALPHA + RTCSetMinDebugLogLevel(.verbose) +#else RTCSetMinDebugLogLevel(.info) +#endif var peerConnection = WebRTCClient.createPeerConnection(iceServers: iceServers, forceRelay: forceRelay) if peerConnection == nil { diff --git a/Monal/Monal.ios.entitlements b/Monal/Monal.ios.entitlements index cb5cda403c..485d26ec64 100644 --- a/Monal/Monal.ios.entitlements +++ b/Monal/Monal.ios.entitlements @@ -24,6 +24,8 @@ com.apple.security.network.client + com.apple.security.network.server + com.apple.security.personal-information.location com.apple.security.personal-information.photos-library diff --git a/Monal/NotificationService/NotificationService.m b/Monal/NotificationService/NotificationService.m index 0eaf40f866..88d21331a7 100644 --- a/Monal/NotificationService/NotificationService.m +++ b/Monal/NotificationService/NotificationService.m @@ -160,32 +160,37 @@ -(BOOL) feedNextHandler -(void) handleIncomingVoipCall:(NSNotification*) notification { DDLogInfo(@"Got incoming VOIP call"); - if(@available(iOS 14.5, macCatalyst 14.5, *)) + if([HelperTools shouldProvideVoip]) { - //disconnect while still being in the receive queue to make sure we don't process any other stanza after this jmi one - //(we don't want to handle a second jmi stanza for example: that could confuse tie-breaking and other parts of our call handling) - xmpp* account = [[MLXMPPManager sharedInstance] getConnectedAccountForID:notification.userInfo[@"accountNo"]]; - [account disconnect]; - - //now disconnect all other accounts, post the voip push and kill the appex - //do this in an extra thread to avoid deadlocks via: receive_queue -> disconnect_thread -> receive_queue - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - //directly disconnect without handling any possibly queued stanzas (they will be handled in mainapp once we wake it up) - [self disconnectAndFeedAllWaitingHandlers]; - - DDLogInfo(@"Dispatching voip call to mainapp..."); - NSString* payload = [HelperTools encodeBase64WithData:[HelperTools serializeObject:notification.userInfo]]; - [CXProvider reportNewIncomingVoIPPushPayload:@{@"base64Payload": payload} completion:^(NSError* _Nullable error) { - if(error != nil) - DDLogError(@"Got error for reportNewIncomingVoIPPushPayload: %@", error); - else - DDLogInfo(@"Successfully called reportNewIncomingVoIPPushPayload"); - [self killAppex]; - }]; - }); + if(@available(iOS 14.5, macCatalyst 14.5, *)) + { + //disconnect while still being in the receive queue to make sure we don't process any other stanza after this jmi one + //(we don't want to handle a second jmi stanza for example: that could confuse tie-breaking and other parts of our call handling) + xmpp* account = [[MLXMPPManager sharedInstance] getConnectedAccountForID:notification.userInfo[@"accountNo"]]; + [account disconnect]; + + //now disconnect all other accounts, post the voip push and kill the appex + //do this in an extra thread to avoid deadlocks via: receive_queue -> disconnect_thread -> receive_queue + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + //directly disconnect without handling any possibly queued stanzas (they will be handled in mainapp once we wake it up) + [self disconnectAndFeedAllWaitingHandlers]; + + DDLogInfo(@"Dispatching voip call to mainapp..."); + NSString* payload = [HelperTools encodeBase64WithData:[HelperTools serializeObject:notification.userInfo]]; + [CXProvider reportNewIncomingVoIPPushPayload:@{@"base64Payload": payload} completion:^(NSError* _Nullable error) { + if(error != nil) + DDLogError(@"Got error for reportNewIncomingVoIPPushPayload: %@", error); + else + DDLogInfo(@"Successfully called reportNewIncomingVoIPPushPayload"); + [self killAppex]; + }]; + }); + } + else + DDLogError(@"iOS < 14.5 detected, ignoring incoming call!"); } else - DDLogError(@"iOS < 14.5 detected, ignoring incoming call!"); + DDLogError(@"shouldProvideVoip returned NO, ignoring incoming call!"); } -(void) disconnectAndFeedAllWaitingHandlers diff --git a/rust/sdp-to-jingle/src/xep_0176.rs b/rust/sdp-to-jingle/src/xep_0176.rs index f9db742026..d868aa3d9f 100644 --- a/rust/sdp-to-jingle/src/xep_0176.rs +++ b/rust/sdp-to-jingle/src/xep_0176.rs @@ -173,7 +173,7 @@ impl JingleTransportCandidate { priority: candidate.priority, protocol: match candidate.transport { SdpAttributeCandidateTransport::Udp => "udp".to_string(), - //SdpAttributeCandidateTransport::Tcp => "tcp".to_string(), //not specced in xep-0176 + SdpAttributeCandidateTransport::Tcp => "tcp".to_string(), //not specced in xep-0176 _ => { return Err(SdpParserInternalError::Generic( "Encountered some candidate transport (like tcp) not specced in XEP-0176!" @@ -196,7 +196,7 @@ impl JingleTransportCandidate { component: self.component, transport: match self.protocol.as_str() { "udp" => Ok(SdpAttributeCandidateTransport::Udp), - //"tcp" => Ok(SdpAttributeCandidateTransport::Tcp), + "tcp" => Ok(SdpAttributeCandidateTransport::Tcp), //not specced in xep-0176 _ => Err(SdpParserInternalError::Generic( "Encountered some candidate transport (like tcp) not specced in XEP-0176!" .to_string(), From 634f1087c6f8853045764b847fe2ab768e184aac Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Tue, 7 May 2024 07:35:46 +0200 Subject: [PATCH 071/143] Activate DNSSEC validation for all HTTP requests and TCP connections --- Monal/Classes/HelperTools.h | 2 ++ Monal/Classes/HelperTools.m | 8 ++++++++ Monal/Classes/MLFiletransfer.m | 6 ++++-- Monal/Classes/MLHTTPRequest.m | 7 ++++--- Monal/Classes/MLStream.m | 2 ++ Monal/Classes/MLVoIPProcessor.m | 16 ++++++++++------ Monal/Classes/MLWebViewController.m | 8 +++++--- Monal/Classes/RegisterAccount.swift | 5 ++++- Monal/Classes/chatViewController.m | 15 ++++++++++----- 9 files changed, 49 insertions(+), 20 deletions(-) diff --git a/Monal/Classes/HelperTools.h b/Monal/Classes/HelperTools.h index fb02f2877d..446b73f549 100644 --- a/Monal/Classes/HelperTools.h +++ b/Monal/Classes/HelperTools.h @@ -177,6 +177,8 @@ void swizzle(Class c, SEL orig, SEL new); +(BOOL) isIP:(NSString*) host; ++(NSURLSession*) createEphemeralURLSession; + @end NS_ASSUME_NONNULL_END diff --git a/Monal/Classes/HelperTools.m b/Monal/Classes/HelperTools.m index aee39aa79e..18a2928b04 100644 --- a/Monal/Classes/HelperTools.m +++ b/Monal/Classes/HelperTools.m @@ -2710,4 +2710,12 @@ +(BOOL) isIP:(NSString*) host return NO; } ++(NSURLSession*) createEphemeralURLSession +{ + NSURLSessionConfiguration* sessionConfig = [NSURLSessionConfiguration ephemeralSessionConfiguration]; + if(@available(iOS 16.1, macCatalyst 16.1, *)) + sessionConfig.requiresDNSSECValidation = YES; + return [NSURLSession sessionWithConfiguration:sessionConfig]; +} + @end diff --git a/Monal/Classes/MLFiletransfer.m b/Monal/Classes/MLFiletransfer.m index 174f6a4234..41b4427f1d 100644 --- a/Monal/Classes/MLFiletransfer.m +++ b/Monal/Classes/MLFiletransfer.m @@ -77,10 +77,12 @@ +(void) checkMimeTypeAndSizeForHistoryID:(NSNumber*) historyId dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{ DDLogInfo(@"Requesting mime-type and size for historyID %@ from http server", historyId); NSMutableURLRequest* request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:url]]; + if(@available(iOS 16.1, macCatalyst 16.1, *)) + request.requiresDNSSECValidation = YES; request.HTTPMethod = @"HEAD"; request.cachePolicy = NSURLRequestReturnCacheDataElseLoad; - NSURLSession* session = [NSURLSession sharedSession]; + NSURLSession* session = [HelperTools createEphemeralURLSession]; [[session dataTaskWithRequest:request completionHandler:^(NSData* _Nullable data __unused, NSURLResponse* _Nullable response, NSError* _Nullable error) { if(error != nil) { @@ -181,7 +183,7 @@ +(void) downloadFileForHistoryID:(NSNumber*) historyId andForceDownload:(BOOL) f return; } - NSURLSession* session = [NSURLSession sharedSession]; + NSURLSession* session = [HelperTools createEphemeralURLSession]; // set app defined description for download size checks [session setSessionDescription:url]; NSURLSessionDownloadTask* task = [session downloadTaskWithURL:[NSURL URLWithString:url] completionHandler:^(NSURL* _Nullable location, NSURLResponse* _Nullable response, NSError* _Nullable error) { diff --git a/Monal/Classes/MLHTTPRequest.m b/Monal/Classes/MLHTTPRequest.m index a12ca5805b..acce093a69 100644 --- a/Monal/Classes/MLHTTPRequest.m +++ b/Monal/Classes/MLHTTPRequest.m @@ -7,7 +7,7 @@ // #import "MLHTTPRequest.h" - +#import "HelperTools.h" @interface MLHTTPRequest () @@ -47,6 +47,8 @@ +(void) sendWithVerb:(NSString*) verb path:(NSString*) path headers:(NSDictiona NSMutableURLRequest* theRequest = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:path] cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:60.0]; + if(@available(iOS 16.1, macCatalyst 16.1, *)) + theRequest.requiresDNSSECValidation = YES; [theRequest setHTTPMethod:verb]; NSData* dataToSubmit = postedData; @@ -70,8 +72,7 @@ +(void) sendWithVerb:(NSString*) verb path:(NSString*) path headers:(NSDictiona DDLogVerbose(@"Calling: %@ %@", verb, path); - NSURLSession* session= [NSURLSession sharedSession]; - + NSURLSession* session = [HelperTools createEphemeralURLSession]; void (^completeBlock)(NSData*,NSURLResponse*,NSError*)= ^(NSData* data,NSURLResponse* response, NSError* connectionError) { diff --git a/Monal/Classes/MLStream.m b/Monal/Classes/MLStream.m index 549fceab4e..3ae299f29a 100644 --- a/Monal/Classes/MLStream.m +++ b/Monal/Classes/MLStream.m @@ -530,6 +530,8 @@ +(void) connectWithSNIDomain:(NSString*) SNIDomain connectHost:(NSString*) host } //needed to activate tcp fast open with apple's internal tls framer nw_parameters_set_fast_open_enabled(parameters, YES); + if(@available(iOS 16.0, macCatalyst 16.0, *)) + nw_parameters_set_requires_dnssec_validation(parameters, YES); //create and configure connection object nw_endpoint_t endpoint = nw_endpoint_create_host([host cStringUsingEncoding:NSUTF8StringEncoding], [[port stringValue] cStringUsingEncoding:NSUTF8StringEncoding]); diff --git a/Monal/Classes/MLVoIPProcessor.m b/Monal/Classes/MLVoIPProcessor.m index 8d66b90af8..a46a939487 100644 --- a/Monal/Classes/MLVoIPProcessor.m +++ b/Monal/Classes/MLVoIPProcessor.m @@ -403,8 +403,11 @@ -(void) initWebRTCForPendingCall:(MLCall*) call // request turn credentials NSMutableURLRequest* urlRequest = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"/api/v1/challenge/new" relativeToURL:[HelperTools getFailoverTurnApiServer]]]; + if(@available(iOS 16.1, macCatalyst 16.1, *)) + urlRequest.requiresDNSSECValidation = YES; [urlRequest setTimeoutInterval:3.0]; - NSURLSessionTask* challengeSession = [[NSURLSession sharedSession] dataTaskWithRequest:urlRequest completionHandler:^(NSData* data, NSURLResponse* response, NSError* error) { + NSURLSession* challengeSession = [HelperTools createEphemeralURLSession]; + [[challengeSession dataTaskWithRequest:urlRequest completionHandler:^(NSData* data, NSURLResponse* response, NSError* error) { if(error != nil || [(NSHTTPURLResponse*)response statusCode] != 200) { DDLogWarn(@"Could not retrieve turn challenge, only using stun: %@", error); @@ -440,6 +443,8 @@ -(void) initWebRTCForPendingCall:(MLCall*) call return; } NSMutableURLRequest* responseRequest = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"/api/v1/challenge/validate" relativeToURL:[HelperTools getFailoverTurnApiServer]]]; + if(@available(iOS 16.1, macCatalyst 16.1, *)) + responseRequest.requiresDNSSECValidation = YES; [responseRequest setHTTPMethod:@"POST"]; [responseRequest setValue:@"application/json" forHTTPHeaderField:@"Accept"]; @@ -448,7 +453,8 @@ -(void) initWebRTCForPendingCall:(MLCall*) call [responseRequest setTimeoutInterval:3.0]; [responseRequest setHTTPBody:challengeResp]; - NSURLSessionTask* responseSession = [[NSURLSession sharedSession] dataTaskWithRequest:responseRequest completionHandler:^(NSData* turnCredentialsData, NSURLResponse* response, NSError* error) { + NSURLSession* responseSession = [HelperTools createEphemeralURLSession]; + [[responseSession dataTaskWithRequest:responseRequest completionHandler:^(NSData* turnCredentialsData, NSURLResponse* response, NSError* error) { if(error != nil || [(NSHTTPURLResponse*)response statusCode] != 200) { DDLogWarn(@"Could not retrieve turn credentials, only using stun: %@", error); @@ -466,10 +472,8 @@ -(void) initWebRTCForPendingCall:(MLCall*) call [iceServers addObject:[[RTCIceServer alloc] initWithURLStrings:[turnCredentials objectForKey:@"uris"] username:[turnCredentials objectForKey:@"username"] credential:[turnCredentials objectForKey:@"password"]]]; [self createWebRTCClientForCall:call usingICEServers:iceServers]; - }]; - [responseSession resume]; - }]; - [challengeSession resume]; + }] resume]; + }] resume]; } //continue without any stun/turn servers if only p2p but no stun/turn servers could be found on local xmpp server //AND no fallback to monal servers was configured diff --git a/Monal/Classes/MLWebViewController.m b/Monal/Classes/MLWebViewController.m index acbf434df5..ea42e4c95d 100644 --- a/Monal/Classes/MLWebViewController.m +++ b/Monal/Classes/MLWebViewController.m @@ -26,10 +26,12 @@ -(void) viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; if(self.urltoLoad.fileURL) - { [self.webview loadFileURL:self.urltoLoad allowingReadAccessToURL:self.urltoLoad]; - } else { - NSURLRequest* nsrequest = [NSURLRequest requestWithURL: self.urltoLoad]; + else + { + NSMutableURLRequest* nsrequest = [NSMutableURLRequest requestWithURL: self.urltoLoad]; + if(@available(iOS 16.1, macCatalyst 16.1, *)) + nsrequest.requiresDNSSECValidation = YES; [self.webview loadRequest:nsrequest]; } self.navigationItem.largeTitleDisplayMode = UINavigationItemLargeTitleDisplayModeNever; diff --git a/Monal/Classes/RegisterAccount.swift b/Monal/Classes/RegisterAccount.swift index f3bc791d4b..35d0c86616 100644 --- a/Monal/Classes/RegisterAccount.swift +++ b/Monal/Classes/RegisterAccount.swift @@ -17,7 +17,10 @@ struct WebView: UIViewRepresentable { } func updateUIView(_ webView: WKWebView, context: Context) { - let request = URLRequest(url: url) + var request = URLRequest(url: url) + if #available(iOS 16.1, macCatalyst 16.1, *) { + request.requiresDNSSECValidation = true; + } webView.load(request) } } diff --git a/Monal/Classes/chatViewController.m b/Monal/Classes/chatViewController.m index 34c78eb104..4474859b5f 100644 --- a/Monal/Classes/chatViewController.m +++ b/Monal/Classes/chatViewController.m @@ -3003,13 +3003,15 @@ -(void) loadPreviewWithUrlForRow:(NSIndexPath *) indexPath withResultHandler:(mo { DDLogVerbose(@"Fetching HTTP HEAD for %@...", row.url); NSMutableURLRequest* headRequest = [[NSMutableURLRequest alloc] initWithURL:row.url]; + if(@available(iOS 16.1, macCatalyst 16.1, *)) + headRequest.requiresDNSSECValidation = YES; headRequest.HTTPMethod = @"HEAD"; headRequest.cachePolicy = NSURLRequestReturnCacheDataElseLoad; - NSURLSession* session = [NSURLSession sharedSession]; + NSURLSession* session = [HelperTools createEphemeralURLSession]; [[session dataTaskWithRequest:headRequest completionHandler:^(NSData* _Nullable data, NSURLResponse* _Nullable response, NSError* _Nullable error) { if(error != nil) { - DDLogWarn(@"Loding preview HEAD for %@ failed: %@", row.url, error); + DDLogWarn(@"Loading preview HEAD for %@ failed: %@", row.url, error); resultHandler(); return; } @@ -3020,7 +3022,7 @@ -(void) loadPreviewWithUrlForRow:(NSIndexPath *) indexPath withResultHandler:(mo if(mimeType.length==0) { - DDLogWarn(@"Loding preview HEAD for %@ failed: mimeType unkown", row.url); + DDLogWarn(@"Loading preview HEAD for %@ failed: mimeType unkown", row.url); resultHandler(); return; } @@ -3035,7 +3037,7 @@ -(void) loadPreviewWithUrlForRow:(NSIndexPath *) indexPath withResultHandler:(mo } if(![mimeType hasPrefix:@"text/"]) { - DDLogWarn(@"Loding HEAD preview for %@ failed: mimeType not supported: %@", row.url, mimeType); + DDLogWarn(@"Loading HEAD preview for %@ failed: mimeType not supported: %@", row.url, mimeType); resultHandler(); return; } @@ -3076,11 +3078,14 @@ -(void) downloadPreviewWithRow:(NSIndexPath*) indexPath usingByterange:(BOOL) us */ DDLogVerbose(@"Fetching HTTP GET for %@...", row.url); NSMutableURLRequest* request = [[NSMutableURLRequest alloc] initWithURL:row.url]; + if(@available(iOS 16.1, macCatalyst 16.1, *)) + request.requiresDNSSECValidation = YES; [request setValue:@"facebookexternalhit/1.1" forHTTPHeaderField:@"User-Agent"]; //required on some sites for og tags e.g. youtube if(useByterange) [request setValue:@"bytes=0-524288" forHTTPHeaderField:@"Range"]; request.timeoutInterval = 10; - [[[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData* _Nullable data, NSURLResponse* _Nullable response, NSError* _Nullable error) { + NSURLSession* session = [HelperTools createEphemeralURLSession]; + [[session dataTaskWithRequest:request completionHandler:^(NSData* _Nullable data, NSURLResponse* _Nullable response, NSError* _Nullable error) { if(error != nil) DDLogVerbose(@"preview fetching error: %@", error); else From 06dbaf8811781621a47c852a75721d1b5f0f4d65 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Fri, 10 May 2024 03:15:48 +0200 Subject: [PATCH 072/143] Don't loop on repeated pubsub precondition fails At least on older ejabberd (~22.05) this can happen because it still returns precondition-not-met errors even after successfully configuring the node. --- Monal/Classes/MLPubSub.m | 35 +++++++++-------------------------- 1 file changed, 9 insertions(+), 26 deletions(-) diff --git a/Monal/Classes/MLPubSub.m b/Monal/Classes/MLPubSub.m index 75b6120384..bb180b2742 100644 --- a/Monal/Classes/MLPubSub.m +++ b/Monal/Classes/MLPubSub.m @@ -288,7 +288,7 @@ -(void) publishItem:(MLXMLNode*) item onNode:(NSString*) node withConfigOptions: //update config options with our own defaults if not already present configOptions = [self copyDefaultNodeOptions:_defaultOptions forConfigForm:nil into:configOptions]; - [self internalPublishItem:item onNode:node withConfigOptions:configOptions andHandler:handler]; + [self internalPublishItem:item onNode:node withConfigOptions:configOptions andHandler:handler andIsRetry:NO]; } -(void) retractItemWithId:(NSString*) itemId onNode:(NSString*) node @@ -499,7 +499,7 @@ -(void) handleHeadlineMessage:(XMPPMessage*) messageNode //*** internal methods below --(void) internalPublishItem:(MLXMLNode*) item onNode:(NSString*) node withConfigOptions:(NSDictionary*) configOptions andHandler:(MLHandler* _Nullable) handler +-(void) internalPublishItem:(MLXMLNode*) item onNode:(NSString*) node withConfigOptions:(NSDictionary*) configOptions andHandler:(MLHandler* _Nullable) handler andIsRetry:(BOOL) is_retry { DDLogDebug(@"Publishing item on node '%@': %@", node, item); XMPPIQ* query = [[XMPPIQ alloc] initWithType:kiqSetType]; @@ -515,7 +515,8 @@ -(void) internalPublishItem:(MLXMLNode*) item onNode:(NSString*) node withConfig $ID(item), $ID(node), $ID(configOptions), - $HANDLER(handler) + $HANDLER(handler), + $BOOL(is_retry) )]; } @@ -880,7 +881,7 @@ -(NSDictionary*) copyDefaultNodeOptions:(NSDictionary*) defaultOptions forConfig } //try again - [self internalPublishItem:item onNode:node withConfigOptions:configOptions andHandler:handler]; + [self internalPublishItem:item onNode:node withConfigOptions:configOptions andHandler:handler andIsRetry:YES]; $$ //this is a user handler for internalPublishItem: called from handlePublishResult @@ -907,31 +908,11 @@ -(NSDictionary*) copyDefaultNodeOptions:(NSDictionary*) defaultOptions forConfig $invalidate(handler, $ID(account), $BOOL(success, NO), $ID(node), $ID(reason)); $$ -$$instance_handler(handlePublishResult, account.pubsub, $$ID(xmpp*, account), $$ID(XMPPIQ*, iqNode), $$ID(MLXMLNode*, item), $$ID(NSString*, node), $$ID(NSDictionary*, configOptions), $_HANDLER(handler)) +$$instance_handler(handlePublishResult, account.pubsub, $$ID(xmpp*, account), $$ID(XMPPIQ*, iqNode), $$ID(MLXMLNode*, item), $$ID(NSString*, node), $$ID(NSDictionary*, configOptions), $_HANDLER(handler), $$BOOL(is_retry)) if([iqNode check:@"/"]) { - //NOTE: workaround for old ejabberd versions <= 21.07 only supporting two special settings as preconditions - if([@"http://www.process-one.net/en/ejabberd/" isEqualToString:account.connectionProperties.serverIdentity] && [configOptions count] > 0 && [iqNode check:@"error/{urn:ietf:params:xml:ns:xmpp-stanzas}resource-constraint"]) - { - DDLogWarn(@"ejabberd (~21.07) workaround for old preconditions handling active for node: %@", node); - - //make sure we don't try all preconditions from configOptions again: only these two listed preconditions are safe to use with ejabberd - NSMutableDictionary* publishPreconditions = [NSMutableDictionary new]; - if(configOptions[@"pubsub#persist_items"]) - publishPreconditions[@"pubsub#persist_items"] = configOptions[@"pubsub#persist_items"]; - if(configOptions[@"pubsub#access_model"]) - publishPreconditions[@"pubsub#access_model"] = configOptions[@"pubsub#access_model"]; - - [self internalPublishItem:item onNode:node withConfigOptions:publishPreconditions andHandler:$newHandlerWithInvalidation(self, handleConfigureAfterPublish, handleConfigureAfterPublishInvalidation, - $ID(node), - $ID(configOptions), - $HANDLER(handler) - )]; - return; - } - //check if this node is already present and configured --> reconfigure it according to our access-model - if([iqNode check:@"error/{http://jabber.org/protocol/pubsub#errors}precondition-not-met"]) + if(!is_retry && [iqNode check:@"error/{http://jabber.org/protocol/pubsub#errors}precondition-not-met"]) { DDLogWarn(@"Node precondition not met, reconfiguring node: %@", node); [self configureNode:node withConfigOptions:configOptions andHandler:$newHandlerWithInvalidation(self, handlePublishAgain, handlePublishAgainInvalidation, @@ -942,6 +923,8 @@ -(NSDictionary*) copyDefaultNodeOptions:(NSDictionary*) defaultOptions forConfig )]; return; } + if(is_retry && [iqNode check:@"error/{http://jabber.org/protocol/pubsub#errors}precondition-not-met"]) + DDLogError(@"Node precondition not met even after reconfiguring node, aborting: %@", node); //all other errors are real errors --> inform user handler $call(handler, $ID(account), $BOOL(success, NO), $ID(node), $ID(errorIq, iqNode)); From 814445124237ce6098dd901715e155ecea2b91fd Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Fri, 10 May 2024 03:48:32 +0200 Subject: [PATCH 073/143] Don't advertise support for urn:xmpp:idle:1 if deactivated by user This makes sure other users won't see a stale time or a constant "currently online". Fixes #1059 --- Monal/Classes/HelperTools.m | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Monal/Classes/HelperTools.m b/Monal/Classes/HelperTools.m index 18a2928b04..9f1d759ada 100644 --- a/Monal/Classes/HelperTools.m +++ b/Monal/Classes/HelperTools.m @@ -1983,7 +1983,6 @@ +(NSSet*) getOwnFeatureSet @"jabber:x:oob", @"urn:xmpp:ping", @"urn:xmpp:receipts", - @"urn:xmpp:idle:1", @"http://jabber.org/protocol/chatstates", @"urn:xmpp:chat-markers:0", @"urn:xmpp:eme:0", @@ -1992,6 +1991,8 @@ +(NSSet*) getOwnFeatureSet ] mutableCopy]; + if([[HelperTools defaultsDB] boolForKey: @"SendLastUserInteraction"]) + [featuresArray addObject:@"urn:xmpp:idle:1"]; if([[HelperTools defaultsDB] boolForKey: @"allowVersionIQ"]) [featuresArray addObject:@"jabber:iq:version"]; //voip stuff From 263addff7e49f9879f60f2ac28049eb6dd580a6d Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Fri, 10 May 2024 07:45:20 +0200 Subject: [PATCH 074/143] Change advertised caps hash for privacy setting changes Changing one of the "Communication" privacy settings triggers a presence containing an updated capabilities hash. --- Monal/Classes/HelperTools.m | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Monal/Classes/HelperTools.m b/Monal/Classes/HelperTools.m index 9f1d759ada..1209e3abba 100644 --- a/Monal/Classes/HelperTools.m +++ b/Monal/Classes/HelperTools.m @@ -1982,9 +1982,6 @@ +(NSSet*) getOwnFeatureSet @"jabber:x:conference", @"jabber:x:oob", @"urn:xmpp:ping", - @"urn:xmpp:receipts", - @"http://jabber.org/protocol/chatstates", - @"urn:xmpp:chat-markers:0", @"urn:xmpp:eme:0", @"urn:xmpp:message-retract:1", @"urn:xmpp:message-correct:0", @@ -1993,6 +1990,12 @@ +(NSSet*) getOwnFeatureSet ] mutableCopy]; if([[HelperTools defaultsDB] boolForKey: @"SendLastUserInteraction"]) [featuresArray addObject:@"urn:xmpp:idle:1"]; + if([[HelperTools defaultsDB] boolForKey: @"SendLastChatState"]) + [featuresArray addObject:@"http://jabber.org/protocol/chatstates"]; + if([[HelperTools defaultsDB] boolForKey: @"SendReceivedMarkers"]) + [featuresArray addObject:@"urn:xmpp:receipts"]; + if([[HelperTools defaultsDB] boolForKey: @"SendDisplayedMarkers"]) + [featuresArray addObject:@"urn:xmpp:chat-markers:0"]; if([[HelperTools defaultsDB] boolForKey: @"allowVersionIQ"]) [featuresArray addObject:@"jabber:iq:version"]; //voip stuff From 73fefcd2b4e7f2956943b538d49c025897949293 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Fri, 10 May 2024 08:46:37 +0200 Subject: [PATCH 075/143] Allo comparison of ObservableKVOWrapper with object of wrapped type --- Monal/Classes/SwiftHelpers.swift | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/Monal/Classes/SwiftHelpers.swift b/Monal/Classes/SwiftHelpers.swift index 82ad5e63d2..4577e4879a 100644 --- a/Monal/Classes/SwiftHelpers.swift +++ b/Monal/Classes/SwiftHelpers.swift @@ -168,11 +168,31 @@ public class ObservableKVOWrapper: ObservableObject, Hashable, return lhs.obj.isEqual(rhs.obj) } + @inlinable + public static func ==(lhs: ObservableKVOWrapper, rhs: ObjType) -> Bool { + return lhs.obj.isEqual(rhs) + } + + @inlinable + public static func ==(lhs: ObjType, rhs: ObservableKVOWrapper) -> Bool { + return lhs.isEqual(rhs.obj) + } + // see https://stackoverflow.com/a/33320737 @inlinable public static func ===(lhs: ObservableKVOWrapper, rhs: ObservableKVOWrapper) -> Bool { return lhs.obj === rhs.obj } + + @inlinable + public static func ===(lhs: ObservableKVOWrapper, rhs: ObjType) -> Bool { + return lhs.obj === rhs + } + + @inlinable + public static func ===(lhs: ObjType, rhs: ObservableKVOWrapper) -> Bool { + return lhs === rhs.obj + } @inlinable public func hash(into hasher: inout Hasher) { From 90f2e01485a9a81775b0f7fc97dd1a5d1879005c Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Fri, 10 May 2024 10:17:45 +0200 Subject: [PATCH 076/143] Make MLContact instances true singletons This also add the previously in MLXMLNode defined WeakContainer to HelperTools. --- Monal/Classes/HelperTools.h | 6 +++ Monal/Classes/HelperTools.m | 9 +++++ Monal/Classes/MLContact.m | 79 ++++++++++++++++++++++++------------- Monal/Classes/MLXMLNode.m | 15 ------- 4 files changed, 66 insertions(+), 43 deletions(-) diff --git a/Monal/Classes/HelperTools.h b/Monal/Classes/HelperTools.h index 446b73f549..dc149d6722 100644 --- a/Monal/Classes/HelperTools.h +++ b/Monal/Classes/HelperTools.h @@ -56,6 +56,12 @@ typedef NS_ENUM(NSUInteger, MLRunLoopIdentifier) { void logException(NSException* exception); void swizzle(Class c, SEL orig, SEL new); +//weak container holding an object as weak pointer (needed to not create retain circles in NSCache +@interface WeakContainer : NSObject +@property (nonatomic, weak) id obj; +-(id) initWithObj:(id) obj; +@end + @interface HelperTools : NSObject @property (class, nonatomic, strong, nullable) DDFileLogger* fileLogger; diff --git a/Monal/Classes/HelperTools.m b/Monal/Classes/HelperTools.m index 1209e3abba..f63eb3b6ef 100644 --- a/Monal/Classes/HelperTools.m +++ b/Monal/Classes/HelperTools.m @@ -278,6 +278,15 @@ void swizzle(Class c, SEL orig, SEL new) method_exchangeImplementations(origMethod, newMethod); } +@implementation WeakContainer +-(id) initWithObj:(id) obj +{ + self = [super init]; + self.obj = obj; + return self; +} +@end + @implementation HelperTools +(void) initialize diff --git a/Monal/Classes/MLContact.m b/Monal/Classes/MLContact.m index 499559b31c..a3314d02c8 100644 --- a/Monal/Classes/MLContact.m +++ b/Monal/Classes/MLContact.m @@ -28,6 +28,8 @@ NSString* const kSubRemove = @"remove"; NSString* const kAskSubscribe = @"subscribe"; +static NSMutableDictionary* _singletonCache; + @interface MLContact () { NSInteger _unreadCount; @@ -69,6 +71,11 @@ @interface MLContact () @implementation MLContact ++(void) initialize +{ + _singletonCache = [NSMutableDictionary new]; +} + +(MLContact*) makeDummyContact:(int) type { if(type == 1) @@ -191,36 +198,52 @@ +(MLContact*) createContactFromJid:(NSString*) jid andAccountNo:(NSNumber*) acco { MLAssert(jid != nil, @"jid must not be nil"); MLAssert(accountNo != nil && accountNo.intValue >= 0, @"accountNo must not be nil and > 0"); - NSDictionary* contactDict = [[DataLayer sharedInstance] contactDictionaryForUsername:jid forAccount:accountNo]; - // check if we know this contact and return a dummy one if not - if(contactDict == nil) - { - DDLogInfo(@"Returning dummy MLContact for %@ on accountNo %@", jid, accountNo); - return [self contactFromDictionary:@{ - @"buddy_name": jid.lowercaseString, - @"nick_name": @"", - @"full_name": @"", - @"subscription": kSubNone, - @"ask": @"", - @"account_id": accountNo, - //@"muc_subject": nil, - //@"muc_nick": nil, - @"Muc": @NO, - @"mentionOnly": @NO, - @"pinned": @NO, - @"blocked": @NO, - @"encrypt": @NO, - @"muted": @NO, - @"status": @"", - @"state": @"offline", - @"count": @0, - @"isActiveChat": @NO, - @"lastInteraction": nilWrapper(nil), - }]; + NSString* cacheKey = [NSString stringWithFormat:@"%@|%@", accountNo, jid]; + @synchronized(_singletonCache) { + if(_singletonCache[cacheKey] != nil) + { + if(((WeakContainer*)_singletonCache[cacheKey]).obj != nil) + return ((WeakContainer*)_singletonCache[cacheKey]).obj; + else + [_singletonCache removeObjectForKey:cacheKey]; + } + + NSDictionary* contactDict = [[DataLayer sharedInstance] contactDictionaryForUsername:jid forAccount:accountNo]; + MLContact* retval = nil; + + // check if we know this contact and return a dummy one if not + if(contactDict == nil) + { + DDLogInfo(@"Returning dummy MLContact for %@ on accountNo %@", jid, accountNo); + retval = [self contactFromDictionary:@{ + @"buddy_name": jid.lowercaseString, + @"nick_name": @"", + @"full_name": @"", + @"subscription": kSubNone, + @"ask": @"", + @"account_id": accountNo, + //@"muc_subject": nil, + //@"muc_nick": nil, + @"Muc": @NO, + @"mentionOnly": @NO, + @"pinned": @NO, + @"blocked": @NO, + @"encrypt": @NO, + @"muted": @NO, + @"status": @"", + @"state": @"offline", + @"count": @0, + @"isActiveChat": @NO, + @"lastInteraction": nilWrapper(nil), + }]; + } + else + retval = [self contactFromDictionary:contactDict]; + + _singletonCache[cacheKey] = [[WeakContainer alloc] initWithObj:retval]; + return retval; } - else - return [self contactFromDictionary:contactDict]; } -(instancetype) init diff --git a/Monal/Classes/MLXMLNode.m b/Monal/Classes/MLXMLNode.m index 3df5af0288..0c31545d97 100644 --- a/Monal/Classes/MLXMLNode.m +++ b/Monal/Classes/MLXMLNode.m @@ -23,21 +23,6 @@ //this is the required prototype from Holger's snprintf.c int rpl_vasprintf(char **, const char *, va_list *); - -//weak container holding an object as weak pointer (needed to not create retain circles in NSCache -@interface WeakContainer : NSObject -@property (nonatomic, weak) id obj; -@end -@implementation WeakContainer --(id) initWithObj:(id) obj -{ - self = [super init]; - self.obj = obj; - return self; -} -@end - - @interface MLXMLNode() { NSMutableArray* _children; From 2a023dbc48a945de8617d70a8dbe7e1271f0bf02 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Fri, 10 May 2024 10:42:20 +0200 Subject: [PATCH 077/143] Fix crash in contact details --- Monal/Classes/ContactDetails.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Monal/Classes/ContactDetails.swift b/Monal/Classes/ContactDetails.swift index 4ecc37c02d..d89dabe8b1 100644 --- a/Monal/Classes/ContactDetails.swift +++ b/Monal/Classes/ContactDetails.swift @@ -120,7 +120,7 @@ struct ContactDetails: View { } } - if !contact.isGroup && (contact.statusMessage as String).count > 0 { + if !contact.isGroup, let statusMessage = contact.statusMessage as String?, statusMessage.count > 0 { VStack { Text("Status message:") Text(contact.statusMessage as String) From b0214fbb21d7f762f2f3e9504a1f6af3d354956d Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Fri, 10 May 2024 10:48:12 +0200 Subject: [PATCH 078/143] Improve appearance of LoadingOverlay --- Monal/Classes/LoadingOverlay.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Monal/Classes/LoadingOverlay.swift b/Monal/Classes/LoadingOverlay.swift index ac713970bc..97387f815a 100644 --- a/Monal/Classes/LoadingOverlay.swift +++ b/Monal/Classes/LoadingOverlay.swift @@ -35,7 +35,8 @@ struct LoadingOverlay: ViewModifier { state.description.font(.footnote) ProgressView() } - .frame(width: 250, height: 100) + .padding(12) + .frame(minWidth: 250, minHeight: 100) .background(Color.secondary.colorInvert()) .cornerRadius(20) .transaction { transaction in transaction.animation = nil} From a4b3a9ce5cb59e5b2930456bc588e95f75ed6cf3 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Fri, 10 May 2024 11:01:07 +0200 Subject: [PATCH 079/143] Make group members editor fully dynamic and handle all errors --- Monal/Classes/ContactPicker.swift | 48 ++++++-- Monal/Classes/MLConstants.h | 1 + Monal/Classes/MLMucProcessor.m | 15 ++- Monal/Classes/MemberList.swift | 189 +++++++++++++++++++++--------- 4 files changed, 183 insertions(+), 70 deletions(-) diff --git a/Monal/Classes/ContactPicker.swift b/Monal/Classes/ContactPicker.swift index 0ca4f833e2..9aff509a5f 100644 --- a/Monal/Classes/ContactPicker.swift +++ b/Monal/Classes/ContactPicker.swift @@ -32,33 +32,56 @@ struct ContactPickerEntry: View { } struct ContactPicker: View { + typealias completionType = (OrderedSet>)->Void + let account: xmpp @Environment(\.presentationMode) private var presentationMode @Binding var returnedContacts: OrderedSet> - @State var allContacts: OrderedSet> @State var selectedContacts: OrderedSet> @State var searchText = "" @State var isEditingSearchInput = false let allowRemoval: Bool + let completion: completionType? + init(_ account: xmpp, initializeFrom contacts: OrderedSet>, allowRemoval: Bool = true, completion:completionType?) { + self.account = account + self.allowRemoval = allowRemoval + self.completion = completion + _selectedContacts = State(wrappedValue:OrderedSet()) + //use a temporary storage because we don't have a binding to the outside world but use the completion handler + var storage = contacts + _returnedContacts = Binding( + get: { storage }, + set: { storage = $0 } + ) + buildPreselectedContacts(contacts) + DDLogError("self.allowRemoval = \(String(describing:self.allowRemoval))") + } + init(_ account: xmpp, binding returnedContacts: Binding>>, allowRemoval: Bool = true) { + self.account = account self.allowRemoval = allowRemoval - var contactsTmp: OrderedSet> = OrderedSet() - + self.completion = nil + _selectedContacts = State(wrappedValue:OrderedSet()) + _returnedContacts = returnedContacts + buildPreselectedContacts(returnedContacts.wrappedValue) + } + + private mutating func buildPreselectedContacts(_ source: OrderedSet>) { //build currently selected list of contacts - contactsTmp.removeAll() - for contact in returnedContacts.wrappedValue { + var contactsTmp: OrderedSet> = OrderedSet() + for contact in source { contactsTmp.append(contact) } - _selectedContacts = State(wrappedValue: contactsTmp) - + _selectedContacts = State(wrappedValue:contactsTmp) + } + + private var allContacts: OrderedSet> { //build list of all possible contacts on this account (excluding selfchat and other mucs) - contactsTmp.removeAll() + var contactsTmp: OrderedSet> = OrderedSet() for contact in DataLayer.sharedInstance().possibleGroupMembers(forAccount: account.accountNo) { contactsTmp.append(ObservableKVOWrapper(contact)) } - _allContacts = State(wrappedValue: contactsTmp) - - _returnedContacts = returnedContacts + return contactsTmp } private var searchResults : OrderedSet> { @@ -110,6 +133,9 @@ struct ContactPicker: View { for contact in selectedContacts { returnedContacts.append(contact) } + if let completion = completion { + completion(returnedContacts) + } } } } diff --git a/Monal/Classes/MLConstants.h b/Monal/Classes/MLConstants.h index f74fd3d8bc..b6c26bd941 100644 --- a/Monal/Classes/MLConstants.h +++ b/Monal/Classes/MLConstants.h @@ -173,6 +173,7 @@ static inline NSString* _Nonnull LocalizationNotNeeded(NSString* _Nonnull s) #define kMonalXmppUserSoftWareVersionRefresh @"kMonalXmppUserSoftWareVersionRefresh" #define kMonalBlockListRefresh @"kMonalBlockListRefresh" #define kMonalContactRemoved @"kMonalContactRemoved" +#define kMonalMucParticipantsAndMembersUpdated @"kMonalMucParticipantsAndMembersUpdated" // max count of char's in a single message (both: sending and receiving) #define kMonalChatMaxAllowedTextLen 2048 diff --git a/Monal/Classes/MLMucProcessor.m b/Monal/Classes/MLMucProcessor.m index 4702b592a8..7a95d311d5 100644 --- a/Monal/Classes/MLMucProcessor.m +++ b/Monal/Classes/MLMucProcessor.m @@ -350,9 +350,17 @@ -(void) processPresence:(XMPPPresence*) presenceNode else [[DataLayer sharedInstance] addParticipant:item toMuc:presenceNode.fromUser forAccountId:_account.accountNo]; - //handle members updates + //handle members updates (publishing the changes in members/participants is already handled by handleMembersListUpdate + //--> only publish if we don't call handleMembersListUpdate if(item[@"jid"] != nil) [self handleMembersListUpdate:[presenceNode find:@"{http://jabber.org/protocol/muc#user}x/item@@"] forMuc:presenceNode.fromUser]; + else + { + DDLogDebug(@"Publishing participants list update..."); + [[MLNotificationQueue currentQueue] postNotificationName:kMonalMucParticipantsAndMembersUpdated object:_account userInfo:@{ + @"contact": [MLContact createContactFromJid:presenceNode.fromUser andAccountNo:_account.accountNo] + }]; + } } else DDLogDebug(@"Ignoring unavailable presences of room being destroyed by us..."); @@ -462,6 +470,11 @@ -(void) handleMembersListUpdate:(NSArray*) items forMuc:(NSString #endif// DISABLE_OMEMO } } + + DDLogDebug(@"Publishing new memberslist..."); + [[MLNotificationQueue currentQueue] postNotificationName:kMonalMucParticipantsAndMembersUpdated object:_account userInfo:@{ + @"contact": [MLContact createContactFromJid:mucJid andAccountNo:_account.accountNo] + }]; } else DDLogWarn(@"Ignoring handleMembersListUpdate for %@, MUC not in buddylist", mucJid); diff --git a/Monal/Classes/MemberList.swift b/Monal/Classes/MemberList.swift index bc126d5618..7a5dc2ad36 100644 --- a/Monal/Classes/MemberList.swift +++ b/Monal/Classes/MemberList.swift @@ -16,7 +16,7 @@ struct ActionSheetPrompt { struct MemberList: View { private let account: xmpp - private let ownAffiliation: String; + @State private var ownAffiliation: String; @StateObject var group: ObservableKVOWrapper @State private var memberList: OrderedSet> @State private var affiliations: Dictionary, String> @@ -25,21 +25,41 @@ struct MemberList: View { @State private var alertPrompt = AlertPrompt(dismissLabel: Text("Close")) @State private var showActionSheet = false @State private var actionSheetPrompt = ActionSheetPrompt() + @StateObject private var overlay = LoadingOverlayState() init(mucContact: ObservableKVOWrapper) { account = MLXMPPManager.sharedInstance().getConnectedAccount(forID: mucContact.accountId)! as xmpp - _group = StateObject(wrappedValue: mucContact) - _memberList = State(wrappedValue: getContactList(viewContact: mucContact)) - ownAffiliation = DataLayer.sharedInstance().getOwnAffiliation(inGroupOrChannel:mucContact.obj) ?? "none" + _group = StateObject(wrappedValue:mucContact) + _ownAffiliation = State(wrappedValue:"none") + _memberList = State(wrappedValue:OrderedSet>()) + _affiliations = State(wrappedValue:[:]) + } + + func updateMemberlist() { + memberList = getContactList(viewContact:group) + ownAffiliation = DataLayer.sharedInstance().getOwnAffiliation(inGroupOrChannel:group.obj) ?? "none" var affiliationTmp = Dictionary, String>() - for memberInfo in Array(DataLayer.sharedInstance().getMembersAndParticipants(ofMuc: mucContact.contactJid, forAccountId: account.accountNo)) { + for memberInfo in Array(DataLayer.sharedInstance().getMembersAndParticipants(ofMuc:group.contactJid, forAccountId:account.accountNo)) { guard let jid = memberInfo["participant_jid"] as? String ?? memberInfo["member_jid"] as? String else { continue } let contact = ObservableKVOWrapper(MLContact.createContact(fromJid:jid, andAccountNo:account.accountNo)) affiliationTmp[contact] = memberInfo["affiliation"] as? String ?? "none" } - _affiliations = State(wrappedValue: affiliationTmp) + affiliations = affiliationTmp + } + + func performAction(_ title: Text, action: @escaping ()->Void) { + action() + self.account.mucProcessor.addUIHandler({_data in let data = _data as! NSDictionary + DispatchQueue.main.async { + hideLoadingOverlay(overlay) + let success : Bool = data["success"] as! Bool; + if !success { + showAlert(title: title, description: Text(data["errorMessage"] as! String)) + } + } + }, forMuc:group.contactJid) } func showAlert(title: Text, description: Text) { @@ -69,98 +89,139 @@ struct MemberList: View { return false } - func affiliationToText(_ affiliation: String?) -> some View { + func actionsAllowed(for contact:ObservableKVOWrapper) -> [String] { + if let contactAffiliation = affiliations[contact] { + if ownAffiliation == "owner" { + return ["profile", "owner", "admin", "member", "outcast"] + } else { //only admin left, because other affiliations don't call actionsAllowed at all + if ["member", "outcast"].contains(contactAffiliation) { + return ["profile", "member", "outcast"] + } else { + //return contact affiliation because that should be displayed as selected in picker + return ["profile", contactAffiliation] + } + } + } + return ["profile"] + } + + func affiliationToString(_ affiliation: String?) -> String { if let affiliation = affiliation { if affiliation == "owner" { - return Text("Owner") + return NSLocalizedString("Owner", comment:"muc affiliation") } else if affiliation == "admin" { - return Text("Admin") + return NSLocalizedString("Admin", comment:"muc affiliation") } else if affiliation == "member" { - return Text("Member") + return NSLocalizedString("Member", comment:"muc affiliation") } else if affiliation == "outcast" { - return Text("Blocked") + return NSLocalizedString("Blocked", comment:"muc affiliation") } else if affiliation == "profile" { - return Text("Open contact details") + return NSLocalizedString("Open contact details", comment:"") } } - return Text("") + return NSLocalizedString("", comment:"muc affiliation") } var body: some View { List { - Section(header: Text(self.group.obj.contactDisplayName)) { + Section(header: Text("\(self.group.contactDisplayName as String) (affiliation: \(affiliationToString(ownAffiliation)))")) { if ownAffiliation == "owner" || ownAffiliation == "admin" { - NavigationLink(destination: LazyClosureView(ContactPicker(account, binding: $memberList, allowRemoval: false))) { + NavigationLink(destination: LazyClosureView(ContactPicker(account, initializeFrom: memberList, allowRemoval: false) { newMemberList in + for member in newMemberList { + if !memberList.contains(member) { + showLoadingOverlay(overlay, headlineView: Text("Adding new member"), descriptionView: Text("Adding \(member.contactJid as String)...")) + performAction(Text("Error adding new member!")) { + DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 1.0) { + account.mucProcessor.setAffiliation("member", ofUser:member.contactJid, inMuc:self.group.contactJid) + account.mucProcessor.inviteUser(member.contactJid, inMuc: self.group.contactJid) + } + } + } + } + })) { Text("Add Group Members") } - } - ForEach(memberList, id:\.self) { contact in - if !contact.isSelfChat { - HStack(alignment: .center) { - ContactEntry(contact:contact) - - Spacer() - - if ownAffiliation == "owner" || ownAffiliation == "admin" { + + ForEach(memberList, id:\.self) { contact in + if !contact.isSelfChat { + HStack(alignment: .center) { + ContactEntry(contact:contact) + Spacer() Picker(selection: Binding( get: { affiliations[contact] ?? "none" }, set: { newAffiliation in + if newAffiliation == affiliations[contact] { + return + } if newAffiliation == "profile" { DDLogVerbose("Activating navigation to \(String(describing:contact))") navigationActive = contact } else if newAffiliation == "outcast" { showActionSheet(title: Text("Block user?"), description: Text("Do you want to block this user from entering this group/channel?")) { DDLogVerbose("Changing affiliation of \(String(describing:contact)) to: \(String(describing:newAffiliation))...") - affiliations[contact] = newAffiliation - account.mucProcessor.setAffiliation(newAffiliation, ofUser:contact.contactJid, inMuc:self.group.contactJid) + showLoadingOverlay(overlay, headlineView: Text("Blocking member"), descriptionView: Text("Blocking \(contact.contactJid as String)...")) + performAction(Text("Error blocking user!")) { + DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 1.0) { + account.mucProcessor.setAffiliation(newAffiliation, ofUser:contact.contactJid, inMuc:self.group.contactJid) + } + } } } else { DDLogVerbose("Changing affiliation of \(String(describing:contact)) to: \(String(describing:newAffiliation))...") - affiliations[contact] = newAffiliation - account.mucProcessor.setAffiliation(newAffiliation, ofUser:contact.contactJid, inMuc:self.group.contactJid) + showLoadingOverlay(overlay, headlineView: Text("Changing affiliation of member"), descriptionView: + Text("Changing \(contact.contactJid as String) to ") + Text(affiliationToString(newAffiliation)) + Text("...")) + performAction(Text("Error changing affiliation!")) { + DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 1.0) { + account.mucProcessor.setAffiliation(newAffiliation, ofUser:contact.contactJid, inMuc:self.group.contactJid) + } + } } } ), label: EmptyView()) { - ForEach(["profile", "owner", "admin", "member", "outcast"], id:\.self) { affiliation in - affiliationToText(affiliation).tag(affiliation) + ForEach(actionsAllowed(for:contact), id:\.self) { affiliation in + Text(affiliationToString(affiliation)).tag(affiliation) } } .pickerStyle(.menu) - } else { - affiliationToText(affiliations[contact]) + //invisible navigation link triggered programmatically + .background( + NavigationLink(destination: LazyClosureView(ContactDetails(delegate:SheetDismisserProtocol(), contact:contact)), tag:contact, selection:$navigationActive) { EmptyView() } + .opacity(0) + ) } + .deleteDisabled( + !ownUserHasAffiliationToRemove(contact: contact) + ) } - .deleteDisabled( - !ownUserHasAffiliationToRemove(contact: contact) - ) - //invisible navigation link triggered programmatically - .background( - NavigationLink(destination: LazyClosureView(ContactDetails(delegate:SheetDismisserProtocol(), contact:contact)), tag:contact, selection:$navigationActive) { EmptyView() } - .opacity(0) - ) } - } - .onDelete(perform: { memberIdx in - let member = memberList[memberIdx.first!] - showActionSheet(title: Text("Remove user?"), description: self.group.mucType == "group" ? Text("Do you want to remove this user from this group? The user won't be able to enter it again until added back to the group.") : Text("Do you want to remove this user from this channel? The user will be able to enter it again.")) { - account.mucProcessor.setAffiliation("none", ofUser: member.contactJid, inMuc: self.group.contactJid) - self.showAlert(title: Text("User removed"), description: Text("\(memberList[memberIdx.first!].obj.contactJid)")) - memberList.remove(at: memberIdx.first!) + .onDelete(perform: { memberIdx in + let member = memberList[memberIdx.first!] + showActionSheet(title: Text("Remove user?"), description: self.group.mucType == "group" ? Text("Do you want to remove this user from this group? The user won't be able to enter it again until added back to the group.") : Text("Do you want to remove this user from this channel? The user will be able to enter it again.")) { + showLoadingOverlay(overlay, headlineView: Text("Removing member"), descriptionView: Text("Removing \(member.contactJid as String)...")) + performAction(Text("Error removing user!")) { + DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 1.0) { + account.mucProcessor.setAffiliation("none", ofUser: member.contactJid, inMuc: self.group.contactJid) + } + } + } + }) + } else { + ForEach(memberList, id:\.self) { contact in + if !contact.isSelfChat { + NavigationLink(destination: LazyClosureView(ContactDetails(delegate:SheetDismisserProtocol(), contact:contact))) { + HStack(alignment: .center) { + ContactEntry(contact:contact) + Spacer() + Text(affiliationToString(affiliations[contact])) + } + } + .deleteDisabled(true) + } } - }) - } - } - .onChange(of: memberList) { [previousMemberList = memberList] newMemberList in - // only handle new members (added via the contact picker) - for member in newMemberList { - if !previousMemberList.contains(member) { - // add selected group member with affiliation member - affiliations[member] = "member" - account.mucProcessor.setAffiliation("member", ofUser:member.contactJid, inMuc:self.group.contactJid) - account.mucProcessor.inviteUser(member.contactJid, inMuc: self.group.contactJid) } } } + .addLoadingOverlay(overlay) .alert(isPresented: $showAlert, content: { Alert(title: alertPrompt.title, message: alertPrompt.message, dismissButton: .default(alertPrompt.dismissLabel)) }) @@ -178,6 +239,18 @@ struct MemberList: View { ) } .navigationBarTitle("Group Members", displayMode: .inline) + .onAppear { + updateMemberlist() + } + .onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("kMonalMucParticipantsAndMembersUpdated")).receive(on: RunLoop.main)) { notification in + if let xmppAccount = notification.object as? xmpp, let contact = notification.userInfo?["contact"] as? MLContact { + DDLogVerbose("Got muc participants/members update from account \(xmppAccount)...") + if contact == group { + updateMemberlist() + hideLoadingOverlay(overlay) + } + } + } } } From 1adabe52b626704d323014c03f015c1733909c55 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Fri, 10 May 2024 12:48:11 +0200 Subject: [PATCH 080/143] Implement channel management This uses the slightly adapted group management gui. --- Monal/Classes/ContactDetails.swift | 11 +++++--- Monal/Classes/MemberList.swift | 42 +++++++++++++++++++++++------- Monal/Classes/SwiftuiHelpers.swift | 2 +- 3 files changed, 41 insertions(+), 14 deletions(-) diff --git a/Monal/Classes/ContactDetails.swift b/Monal/Classes/ContactDetails.swift index d89dabe8b1..90daf3fa15 100644 --- a/Monal/Classes/ContactDetails.swift +++ b/Monal/Classes/ContactDetails.swift @@ -323,8 +323,14 @@ struct ContactDetails: View { Text("Group Members") } } else if contact.obj.isGroup && contact.obj.mucType == "channel" { - NavigationLink(destination: LazyClosureView(ChannelMemberList(mucContact:contact))) { - Text("Channel Members") + if ["owner", "admin"].contains(DataLayer.sharedInstance().getOwnAffiliation(inGroupOrChannel:contact.obj) ?? "none") { + NavigationLink(destination: LazyClosureView(MemberList(mucContact:contact))) { + Text("Channel Members") + } + } else { + NavigationLink(destination: LazyClosureView(ChannelMemberList(mucContact:contact))) { + Text("Channel Members") + } } } } @@ -463,7 +469,6 @@ struct ContactDetails: View { if let callback = data["callback"] { self.successCallback = objcCast(callback) as monal_void_block_t } - DDLogError("callback: \(String(describing:self.successCallback))") successAlert(title: Text("Success"), message: contact.mucType == "group" ? Text("Successfully destroyed group.") : Text("Successfully destroyed channel.")) } else { errorAlert(title: Text("Error destroying group!"), message: Text(data["errorMessage"] as! String)) diff --git a/Monal/Classes/MemberList.swift b/Monal/Classes/MemberList.swift index 7a5dc2ad36..ac273d308c 100644 --- a/Monal/Classes/MemberList.swift +++ b/Monal/Classes/MemberList.swift @@ -76,6 +76,10 @@ struct MemberList: View { } func ownUserHasAffiliationToRemove(contact: ObservableKVOWrapper) -> Bool { + //we don't want to set affiliation=none in channels using deletion swipe (this does not delete the user) + if group.mucType == "channel" { + return false + } if contact.contactJid == account.connectionProperties.identity.jid { return false } @@ -90,19 +94,35 @@ struct MemberList: View { } func actionsAllowed(for contact:ObservableKVOWrapper) -> [String] { - if let contactAffiliation = affiliations[contact] { - if ownAffiliation == "owner" { - return ["profile", "owner", "admin", "member", "outcast"] - } else { //only admin left, because other affiliations don't call actionsAllowed at all - if ["member", "outcast"].contains(contactAffiliation) { - return ["profile", "member", "outcast"] - } else { - //return contact affiliation because that should be displayed as selected in picker - return ["profile", contactAffiliation] + if group.mucType == "group" { + if let contactAffiliation = affiliations[contact] { + if ownAffiliation == "owner" { + return ["profile", "owner", "admin", "member", "outcast"] + } else { //only admin left, because other affiliations don't call actionsAllowed at all + if ["member", "outcast"].contains(contactAffiliation) { + return ["profile", "member", "outcast"] + } else { + //return contact affiliation because that should be displayed as selected in picker + return ["profile", contactAffiliation] + } + } + } + return ["profile"] + } else { + if let contactAffiliation = affiliations[contact] { + if ownAffiliation == "owner" { + return ["profile", "owner", "admin", "member", "none", "outcast"] + } else { //only admin left, because other affiliations don't call actionsAllowed at all + if ["member", "none", "outcast"].contains(contactAffiliation) { + return ["profile", "member", "none", "outcast"] + } else { + //return contact affiliation because that should be displayed as selected in picker + return ["profile", contactAffiliation] + } } } + return ["profile", "none"] } - return ["profile"] } func affiliationToString(_ affiliation: String?) -> String { @@ -113,6 +133,8 @@ struct MemberList: View { return NSLocalizedString("Admin", comment:"muc affiliation") } else if affiliation == "member" { return NSLocalizedString("Member", comment:"muc affiliation") + } else if affiliation == "none" { + return NSLocalizedString("Participant", comment:"muc affiliation") } else if affiliation == "outcast" { return NSLocalizedString("Blocked", comment:"muc affiliation") } else if affiliation == "profile" { diff --git a/Monal/Classes/SwiftuiHelpers.swift b/Monal/Classes/SwiftuiHelpers.swift index e7ff5206ba..f4539a6ad1 100644 --- a/Monal/Classes/SwiftuiHelpers.swift +++ b/Monal/Classes/SwiftuiHelpers.swift @@ -68,7 +68,7 @@ class SheetDismisserProtocol: ObservableObject { func getContactList(viewContact: (ObservableKVOWrapper?)) -> OrderedSet> { if let contact = viewContact { - if(contact.isGroup && contact.mucType == "group") { + if(contact.isGroup) { //this uses the account the muc belongs to and treats every other account to be remote, //even when multiple accounts of the same monal instance are in the same group var contactList : OrderedSet> = OrderedSet() From 54410e89645fe4099f1cab5e9ac5f6efff1b934a Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Fri, 10 May 2024 12:59:40 +0200 Subject: [PATCH 081/143] Dynamically handle affiliation and role changes in contact details --- Monal/Classes/ContactDetails.swift | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/Monal/Classes/ContactDetails.swift b/Monal/Classes/ContactDetails.swift index 90daf3fa15..f44e31aae0 100644 --- a/Monal/Classes/ContactDetails.swift +++ b/Monal/Classes/ContactDetails.swift @@ -9,8 +9,8 @@ struct ContactDetails: View { var delegate: SheetDismisserProtocol private var account: xmpp - private var ownRole: String - private var ownAffiliation: String + @State private var ownRole = "participant" + @State private var ownAffiliation = "none" @StateObject var contact: ObservableKVOWrapper @State private var showingBlockContactConfirmation = false @State private var showingCannotBlockAlert = false @@ -35,16 +35,18 @@ struct ContactDetails: View { self.delegate = delegate _contact = StateObject(wrappedValue: contact) self.account = MLXMPPManager.sharedInstance().getConnectedAccount(forID: contact.accountId)! + } + private func updateRoleAndAffiliation() { if contact.isGroup { self.ownRole = DataLayer.sharedInstance().getOwnRole(inGroupOrChannel: contact.obj) ?? "none" self.ownAffiliation = DataLayer.sharedInstance().getOwnAffiliation(inGroupOrChannel:contact.obj) ?? "none" } else { - self.ownRole = "none" + self.ownRole = "participant" self.ownAffiliation = "none" } } - + private func errorAlert(title: Text, message: Text = Text("")) { alertPrompt.title = title alertPrompt.message = message @@ -323,7 +325,7 @@ struct ContactDetails: View { Text("Group Members") } } else if contact.obj.isGroup && contact.obj.mucType == "channel" { - if ["owner", "admin"].contains(DataLayer.sharedInstance().getOwnAffiliation(inGroupOrChannel:contact.obj) ?? "none") { + if ["owner", "admin"].contains(ownAffiliation) { NavigationLink(destination: LazyClosureView(MemberList(mucContact:contact))) { Text("Channel Members") } @@ -567,6 +569,17 @@ struct ContactDetails: View { .onChange(of:contact.avatar as UIImage) { _ in hideLoadingOverlay(overlay) } + .onAppear { + self.updateRoleAndAffiliation() + } + .onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("kMonalMucParticipantsAndMembersUpdated")).receive(on: RunLoop.main)) { notification in + if let xmppAccount = notification.object as? xmpp, let notificationContact = notification.userInfo?["contact"] as? MLContact { + DDLogVerbose("Got muc participants/members update from account \(xmppAccount)...") + if notificationContact == contact { + self.updateRoleAndAffiliation() + } + } + } } } From 21105a7c96d5226f47ab071a7229a29dfef48985 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Fri, 10 May 2024 15:08:30 +0200 Subject: [PATCH 082/143] Fix group avatar image picker on macos --- Monal/Classes/BackgroundSettings.swift | 4 ++-- Monal/Classes/ContactDetails.swift | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/Monal/Classes/BackgroundSettings.swift b/Monal/Classes/BackgroundSettings.swift index 12b45d6081..890d3a2af0 100644 --- a/Monal/Classes/BackgroundSettings.swift +++ b/Monal/Classes/BackgroundSettings.swift @@ -2,7 +2,7 @@ // BackgroundSettings.swift // Monal // -// Created by admin on 14.11.22. +// Created by Thilo Molitor on 14.11.22. // Copyright © 2022 monal-im.org. All rights reserved. // @@ -83,7 +83,7 @@ struct BackgroundSettings: View { ImagePicker(image:$inputImage) } - //>= ios16 + //>= ios 16 /* PhotosPicker(selection:$selectedItem, matching:.images, photoLibrary:.shared()) { if let inputImage = inputImage { diff --git a/Monal/Classes/ContactDetails.swift b/Monal/Classes/ContactDetails.swift index f44e31aae0..e548fd9f88 100644 --- a/Monal/Classes/ContactDetails.swift +++ b/Monal/Classes/ContactDetails.swift @@ -72,7 +72,24 @@ struct ContactDetails: View { if ownAffiliation == "owner" { view.accessibilityLabel((contact.mucType == "group") ? "Change Group Avatar" : "Change Channel Avatar") .onTapGesture { +#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 } } else { view.accessibilityLabel((contact.mucType == "group") ? "Group Avatar" : "Channel Avatar") From 665ee536b4a56e74d2ed97ce9726924246f23849 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Sun, 19 May 2024 20:58:50 +0200 Subject: [PATCH 083/143] Make channel members list dynamic, too --- Monal/Classes/ChannelMemberList.swift | 56 +++++++++++++++++---------- 1 file changed, 36 insertions(+), 20 deletions(-) diff --git a/Monal/Classes/ChannelMemberList.swift b/Monal/Classes/ChannelMemberList.swift index 3258d56e22..7a5252c7de 100644 --- a/Monal/Classes/ChannelMemberList.swift +++ b/Monal/Classes/ChannelMemberList.swift @@ -9,45 +9,61 @@ import OrderedCollections struct ChannelMemberList: View { - @State private var channelParticipants: OrderedDictionary - @StateObject var channel: ObservableKVOWrapper private let account: xmpp + @State private var ownAffiliation: String; + @StateObject var channel: ObservableKVOWrapper + @State private var participants: OrderedDictionary init(mucContact: ObservableKVOWrapper) { - self.account = MLXMPPManager.sharedInstance().getConnectedAccount(forID: mucContact.accountId)! as xmpp - _channel = StateObject(wrappedValue: mucContact) - - let jidList = Array(DataLayer.sharedInstance().getMembersAndParticipants(ofMuc: mucContact.contactJid, forAccountId: mucContact.accountId)) - var nickSet : OrderedDictionary = OrderedDictionary() - for jidDict in jidList { - if let nick = jidDict["room_nick"] as? String { - nickSet.updateValue((jidDict["affiliation"] as? String) ?? "none", forKey:nick) + account = MLXMPPManager.sharedInstance().getConnectedAccount(forID: mucContact.accountId)! as xmpp + _channel = StateObject(wrappedValue:mucContact) + _ownAffiliation = State(wrappedValue:"none") + _participants = State(wrappedValue:OrderedDictionary()) + } + + func updateParticipantList() { + ownAffiliation = DataLayer.sharedInstance().getOwnAffiliation(inGroupOrChannel:channel.obj) ?? "none" + participants.removeAll(keepingCapacity:true) + for memberInfo in Array(DataLayer.sharedInstance().getMembersAndParticipants(ofMuc:channel.contactJid, forAccountId:account.accountNo)) { + //ignore ourselves + if let jid = memberInfo["participant_jid"] as? String ?? memberInfo["member_jid"] as? String { + if jid == account.connectionProperties.identity.jid { + continue + } + } + if let nick = memberInfo["room_nick"] as? String { + participants[nick] = memberInfo["affiliation"] as? String ?? "none" } } - _channelParticipants = State(wrappedValue: nickSet) } + var body: some View { List { - Section(header: Text(self.channel.obj.contactDisplayName)) { - ForEach(self.channelParticipants.sorted(by: <), id: \.self.key) { participant in + Section(header: Text("\(self.channel.contactDisplayName as String) (affiliation: \(mucAffiliationToString(ownAffiliation)))")) { + ForEach(participants.sorted(by: <), id: \.self.key) { participant in ZStack(alignment: .topLeading) { HStack(alignment: .center) { Text(participant.key) Spacer() - if participant.value == "owner" { - Text(NSLocalizedString("Owner", comment: "")) - } else if participant.value == "admin" { - Text(NSLocalizedString("Admin", comment: "")) - } else { - Text(NSLocalizedString("Participant", comment: "")) - } + Text(mucAffiliationToString(participant.value)) } } } } } .navigationBarTitle(NSLocalizedString("Channel Participants", comment: ""), displayMode: .inline) + .onAppear { + updateParticipantList() + } + .onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("kMonalMucParticipantsAndMembersUpdated")).receive(on: RunLoop.main)) { notification in + if let xmppAccount = notification.object as? xmpp, let contact = notification.userInfo?["contact"] as? MLContact { + DDLogVerbose("Got muc participants/members update from account \(xmppAccount)...") + if contact == channel { + updateParticipantList() + } + } + } } } From 8d01fba882ab8f412cc288624642930346266c82 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Sun, 19 May 2024 20:59:27 +0200 Subject: [PATCH 084/143] Make sure to update muc contact on delete/remove/ban etc. --- Monal/Classes/MLMucProcessor.m | 41 ++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/Monal/Classes/MLMucProcessor.m b/Monal/Classes/MLMucProcessor.m index 7a95d311d5..4d57520872 100644 --- a/Monal/Classes/MLMucProcessor.m +++ b/Monal/Classes/MLMucProcessor.m @@ -346,9 +346,15 @@ -(void) processPresence:(XMPPPresence*) presenceNode //handle participant updates if([presenceNode check:@"/"] || item[@"affiliation"] == nil) + { + DDLogVerbose(@"Removing participant from muc(%@): %@", presenceNode.fromUser, item); [[DataLayer sharedInstance] removeParticipant:item fromMuc:presenceNode.fromUser forAccountId:_account.accountNo]; + } else + { + DDLogVerbose(@"Adding participant from muc(%@): %@", presenceNode.fromUser, item); [[DataLayer sharedInstance] addParticipant:item toMuc:presenceNode.fromUser forAccountId:_account.accountNo]; + } //handle members updates (publishing the changes in members/participants is already handled by handleMembersListUpdate //--> only publish if we don't call handleMembersListUpdate @@ -629,6 +635,11 @@ -(void) handleStatusCodes:(XMPPStanza*) node //update nick in database DDLogInfo(@"Updating muc %@ nick in database to nick provided by server: '%@'...", node.fromUser, node.fromResource); [[DataLayer sharedInstance] updateOwnNickName:node.fromResource forMuc:node.fromUser forAccount:_account.accountNo]; + + DDLogDebug(@"Updating muc contact..."); + [[MLNotificationQueue currentQueue] postNotificationName:kMonalContactRefresh object:_account userInfo:@{ + @"contact": [MLContact createContactFromJid:node.fromUser andAccountNo:_account.accountNo] + }]; } break; } @@ -643,6 +654,11 @@ -(void) handleStatusCodes:(XMPPStanza*) node [self deleteMuc:node.fromUser withBookmarksUpdate:YES keepBuddylistEntry:NO]; selfPrecenceHandled = YES; [self handleError:[NSString stringWithFormat:NSLocalizedString(@"You got banned from group/channel: %@", @""), node.fromUser] forMuc:node.fromUser withNode:node andIsSevere:YES]; + + DDLogDebug(@"Updating muc contact..."); + [[MLNotificationQueue currentQueue] postNotificationName:kMonalContactRefresh object:_account userInfo:@{ + @"contact": [MLContact createContactFromJid:node.fromUser andAccountNo:_account.accountNo] + }]; } break; } @@ -663,6 +679,11 @@ -(void) handleStatusCodes:(XMPPStanza*) node [self deleteMuc:node.fromUser withBookmarksUpdate:YES keepBuddylistEntry:NO]; selfPrecenceHandled = YES; [self handleError:[NSString stringWithFormat:NSLocalizedString(@"You got kicked from group/channel: %@", @""), node.fromUser] forMuc:node.fromUser withNode:node andIsSevere:YES]; + + DDLogDebug(@"Updating muc contact..."); + [[MLNotificationQueue currentQueue] postNotificationName:kMonalContactRefresh object:_account userInfo:@{ + @"contact": [MLContact createContactFromJid:node.fromUser andAccountNo:_account.accountNo] + }]; } } else @@ -687,6 +708,11 @@ -(void) handleStatusCodes:(XMPPStanza*) node [self deleteMuc:node.fromUser withBookmarksUpdate:YES keepBuddylistEntry:YES]; selfPrecenceHandled = YES; [self handleError:[NSString stringWithFormat:NSLocalizedString(@"You got removed from group/channel: %@", @""), node.fromUser] forMuc:node.fromUser withNode:node andIsSevere:YES]; + + DDLogDebug(@"Updating muc contact..."); + [[MLNotificationQueue currentQueue] postNotificationName:kMonalContactRefresh object:_account userInfo:@{ + @"contact": [MLContact createContactFromJid:node.fromUser andAccountNo:_account.accountNo] + }]; } } } @@ -703,6 +729,11 @@ -(void) handleStatusCodes:(XMPPStanza*) node [self handleError:[NSString stringWithFormat:NSLocalizedString(@"Kicked, because group/channel is now members-only: %@", @""), node.fromUser] forMuc:node.fromUser withNode:node andIsSevere:YES]; [self deleteMuc:node.fromUser withBookmarksUpdate:YES keepBuddylistEntry:YES]; selfPrecenceHandled = YES; + + DDLogDebug(@"Updating muc contact..."); + [[MLNotificationQueue currentQueue] postNotificationName:kMonalContactRefresh object:_account userInfo:@{ + @"contact": [MLContact createContactFromJid:node.fromUser andAccountNo:_account.accountNo] + }]; } break; } @@ -716,6 +747,11 @@ -(void) handleStatusCodes:(XMPPStanza*) node [self removeRoomFromJoining:node.fromUser]; selfPrecenceHandled = YES; [self handleError:[NSString stringWithFormat:NSLocalizedString(@"Kicked from group/channel, because of system shutdown: %@", @""), node.fromUser] forMuc:node.fromUser withNode:node andIsSevere:YES]; + + DDLogDebug(@"Updating muc contact..."); + [[MLNotificationQueue currentQueue] postNotificationName:kMonalContactRefresh object:_account userInfo:@{ + @"contact": [MLContact createContactFromJid:node.fromUser andAccountNo:_account.accountNo] + }]; } break; } @@ -786,6 +822,11 @@ -(void) handleStatusCodes:(XMPPStanza*) node { [self handleError:[NSString stringWithFormat:NSLocalizedString(@"Group/Channel got destroyed: %@", @""), node.fromUser] forMuc:node.fromUser withNode:node andIsSevere:YES]; [self deleteMuc:node.fromUser withBookmarksUpdate:YES keepBuddylistEntry:YES]; + + DDLogDebug(@"Updating muc contact..."); + [[MLNotificationQueue currentQueue] postNotificationName:kMonalContactRefresh object:_account userInfo:@{ + @"contact": [MLContact createContactFromJid:node.fromUser andAccountNo:_account.accountNo] + }]; } } } From 93f306d6705ba4b38353d75512e226f17c37772e Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Sat, 11 May 2024 01:57:01 +0200 Subject: [PATCH 085/143] Improve group/channel admin mode even further --- Monal/Classes/ContactDetails.swift | 4 +- Monal/Classes/MemberList.swift | 148 +++++++++++++++++------------ Monal/Classes/SwiftuiHelpers.swift | 21 ++++ 3 files changed, 108 insertions(+), 65 deletions(-) diff --git a/Monal/Classes/ContactDetails.swift b/Monal/Classes/ContactDetails.swift index e548fd9f88..96f7194437 100644 --- a/Monal/Classes/ContactDetails.swift +++ b/Monal/Classes/ContactDetails.swift @@ -344,11 +344,11 @@ struct ContactDetails: View { } else if contact.obj.isGroup && contact.obj.mucType == "channel" { if ["owner", "admin"].contains(ownAffiliation) { NavigationLink(destination: LazyClosureView(MemberList(mucContact:contact))) { - Text("Channel Members") + Text("Channel Participants") } } else { NavigationLink(destination: LazyClosureView(ChannelMemberList(mucContact:contact))) { - Text("Channel Members") + Text("Channel Participants") } } } diff --git a/Monal/Classes/MemberList.swift b/Monal/Classes/MemberList.swift index ac273d308c..28ec53c2f3 100644 --- a/Monal/Classes/MemberList.swift +++ b/Monal/Classes/MemberList.swift @@ -17,9 +17,10 @@ struct ActionSheetPrompt { struct MemberList: View { private let account: xmpp @State private var ownAffiliation: String; - @StateObject var group: ObservableKVOWrapper + @StateObject var muc: ObservableKVOWrapper @State private var memberList: OrderedSet> @State private var affiliations: Dictionary, String> + @State private var online: Dictionary, Bool> @State private var navigationActive: ObservableKVOWrapper? @State private var showAlert = false @State private var alertPrompt = AlertPrompt(dismissLabel: Text("Close")) @@ -29,37 +30,44 @@ struct MemberList: View { init(mucContact: ObservableKVOWrapper) { account = MLXMPPManager.sharedInstance().getConnectedAccount(forID: mucContact.accountId)! as xmpp - _group = StateObject(wrappedValue:mucContact) + _muc = StateObject(wrappedValue:mucContact) _ownAffiliation = State(wrappedValue:"none") _memberList = State(wrappedValue:OrderedSet>()) _affiliations = State(wrappedValue:[:]) + _online = State(wrappedValue:[:]) } func updateMemberlist() { - memberList = getContactList(viewContact:group) - ownAffiliation = DataLayer.sharedInstance().getOwnAffiliation(inGroupOrChannel:group.obj) ?? "none" - var affiliationTmp = Dictionary, String>() - for memberInfo in Array(DataLayer.sharedInstance().getMembersAndParticipants(ofMuc:group.contactJid, forAccountId:account.accountNo)) { + memberList = getContactList(viewContact:self.muc) + ownAffiliation = DataLayer.sharedInstance().getOwnAffiliation(inGroupOrChannel:self.muc.obj) ?? "none" + affiliations.removeAll(keepingCapacity:true) + online.removeAll(keepingCapacity:true) + for memberInfo in Array(DataLayer.sharedInstance().getMembersAndParticipants(ofMuc:self.muc.contactJid, forAccountId:account.accountNo)) { + DDLogVerbose("Got member/participant entry: \(String(describing:memberInfo))") guard let jid = memberInfo["participant_jid"] as? String ?? memberInfo["member_jid"] as? String else { continue } let contact = ObservableKVOWrapper(MLContact.createContact(fromJid:jid, andAccountNo:account.accountNo)) - affiliationTmp[contact] = memberInfo["affiliation"] as? String ?? "none" + affiliations[contact] = memberInfo["affiliation"] as? String ?? "none" + if let num = memberInfo["online"] as? NSNumber { + online[contact] = num.boolValue + } else { + online[contact] = false + } } - affiliations = affiliationTmp } func performAction(_ title: Text, action: @escaping ()->Void) { - action() self.account.mucProcessor.addUIHandler({_data in let data = _data as! NSDictionary DispatchQueue.main.async { hideLoadingOverlay(overlay) let success : Bool = data["success"] as! Bool; if !success { - showAlert(title: title, description: Text(data["errorMessage"] as! String)) + showAlert(title: title, description: Text(data["errorMessage"] as? String ?? "Unknown error!")) } } - }, forMuc:group.contactJid) + }, forMuc:self.muc.contactJid) + action() } func showAlert(title: Text, description: Text) { @@ -77,7 +85,7 @@ struct MemberList: View { func ownUserHasAffiliationToRemove(contact: ObservableKVOWrapper) -> Bool { //we don't want to set affiliation=none in channels using deletion swipe (this does not delete the user) - if group.mucType == "channel" { + if self.muc.mucType == "channel" { return false } if contact.contactJid == account.connectionProperties.identity.jid { @@ -94,74 +102,77 @@ struct MemberList: View { } func actionsAllowed(for contact:ObservableKVOWrapper) -> [String] { - if group.mucType == "group" { - if let contactAffiliation = affiliations[contact] { + if let contactAffiliation = affiliations[contact], let contactOnline = online[contact] { + var reinviteEntry: [String] = [] + if !contactOnline { + reinviteEntry = ["reinvite"] + } + if self.muc.mucType == "group" { if ownAffiliation == "owner" { - return ["profile", "owner", "admin", "member", "outcast"] + return ["profile"] + reinviteEntry + ["owner", "admin", "member", "outcast"] } else { //only admin left, because other affiliations don't call actionsAllowed at all if ["member", "outcast"].contains(contactAffiliation) { - return ["profile", "member", "outcast"] + return ["profile"] + reinviteEntry + ["member", "outcast"] } else { + //if this contact is a co-admin or owner, we aren't allowed to do much to their affiliation //return contact affiliation because that should be displayed as selected in picker - return ["profile", contactAffiliation] + return ["profile"] + reinviteEntry + [contactAffiliation] } } - } - return ["profile"] - } else { - if let contactAffiliation = affiliations[contact] { + } else { if ownAffiliation == "owner" { - return ["profile", "owner", "admin", "member", "none", "outcast"] + return ["profile"] + reinviteEntry + ["owner", "admin", "member", "none", "outcast"] } else { //only admin left, because other affiliations don't call actionsAllowed at all if ["member", "none", "outcast"].contains(contactAffiliation) { - return ["profile", "member", "none", "outcast"] + return ["profile"] + reinviteEntry + ["member", "none", "outcast"] } else { + //if this contact is a co-admin or owner, we aren't allowed to do much to their affiliation //return contact affiliation because that should be displayed as selected in picker - return ["profile", contactAffiliation] + return ["profile"] + reinviteEntry + [contactAffiliation] } } } - return ["profile", "none"] } - } - - func affiliationToString(_ affiliation: String?) -> String { - if let affiliation = affiliation { - if affiliation == "owner" { - return NSLocalizedString("Owner", comment:"muc affiliation") - } else if affiliation == "admin" { - return NSLocalizedString("Admin", comment:"muc affiliation") - } else if affiliation == "member" { - return NSLocalizedString("Member", comment:"muc affiliation") - } else if affiliation == "none" { - return NSLocalizedString("Participant", comment:"muc affiliation") - } else if affiliation == "outcast" { - return NSLocalizedString("Blocked", comment:"muc affiliation") - } else if affiliation == "profile" { - return NSLocalizedString("Open contact details", comment:"") - } + //fallback (should hopefully never be needed) + DDLogWarn("Fallback for group/channel \(String(describing:self.muc.contactJid as String)): affiliation=\(String(describing:affiliations[contact])), online=\(String(describing:online[contact]))") + if self.muc.mucType == "group" { + return ["profile"] + } else { + return ["profile", "reinvite", "none"] } - return NSLocalizedString("", comment:"muc affiliation") } var body: some View { List { - Section(header: Text("\(self.group.contactDisplayName as String) (affiliation: \(affiliationToString(ownAffiliation)))")) { + Section(header: Text("\(self.muc.contactDisplayName as String) (affiliation: \(mucAffiliationToString(ownAffiliation)))")) { if ownAffiliation == "owner" || ownAffiliation == "admin" { NavigationLink(destination: LazyClosureView(ContactPicker(account, initializeFrom: memberList, allowRemoval: false) { newMemberList in for member in newMemberList { if !memberList.contains(member) { - showLoadingOverlay(overlay, headlineView: Text("Adding new member"), descriptionView: Text("Adding \(member.contactJid as String)...")) - performAction(Text("Error adding new member!")) { - DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 1.0) { - account.mucProcessor.setAffiliation("member", ofUser:member.contactJid, inMuc:self.group.contactJid) - account.mucProcessor.inviteUser(member.contactJid, inMuc: self.group.contactJid) + if self.muc.mucType == "group" { + showLoadingOverlay(overlay, headlineView: Text("Adding new member"), descriptionView: Text("Adding \(member.contactJid as String)...")) + performAction(Text("Error adding new member!")) { + DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 1.0) { + account.mucProcessor.setAffiliation("member", ofUser:member.contactJid, inMuc:self.muc.contactJid) + account.mucProcessor.inviteUser(member.contactJid, inMuc: self.muc.contactJid) + } + } + } else { + showLoadingOverlay(overlay, headlineView: Text("Inviting new participant"), descriptionView: Text("Adding \(member.contactJid as String)...")) + performAction(Text("Error adding new participant!")) { + DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 1.0) { + account.mucProcessor.inviteUser(member.contactJid, inMuc: self.muc.contactJid) + } } } } } })) { - Text("Add Group Members") + if self.muc.mucType == "group" { + Text("Add members to group") + } else { + Text("Invite participants to channel") + } } ForEach(memberList, id:\.self) { contact in @@ -178,30 +189,41 @@ struct MemberList: View { if newAffiliation == "profile" { DDLogVerbose("Activating navigation to \(String(describing:contact))") navigationActive = contact + } else if newAffiliation == "reinvite" { + showLoadingOverlay(overlay, headlineView: Text("Inviting user"), descriptionView: Text("Inviting user to this group/channel: \(contact.contactJid as String)")) + performAction(Text("Error inviting user!")) { + DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 1.0) { + //first remove potential ban, then reinvite + if affiliations[contact] == "outcast" { + account.mucProcessor.setAffiliation(self.muc.mucType == "group" ? "member" : "none", ofUser:contact.contactJid, inMuc:self.muc.contactJid) + } + account.mucProcessor.inviteUser(contact.contactJid, inMuc: self.muc.contactJid) + } + } } else if newAffiliation == "outcast" { showActionSheet(title: Text("Block user?"), description: Text("Do you want to block this user from entering this group/channel?")) { DDLogVerbose("Changing affiliation of \(String(describing:contact)) to: \(String(describing:newAffiliation))...") - showLoadingOverlay(overlay, headlineView: Text("Blocking member"), descriptionView: Text("Blocking \(contact.contactJid as String)...")) + showLoadingOverlay(overlay, headlineView: Text("Blocking member"), descriptionView: Text("Blocking \(contact.contactJid as String)")) performAction(Text("Error blocking user!")) { DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 1.0) { - account.mucProcessor.setAffiliation(newAffiliation, ofUser:contact.contactJid, inMuc:self.group.contactJid) + account.mucProcessor.setAffiliation(newAffiliation, ofUser:contact.contactJid, inMuc:self.muc.contactJid) } } } } else { DDLogVerbose("Changing affiliation of \(String(describing:contact)) to: \(String(describing:newAffiliation))...") showLoadingOverlay(overlay, headlineView: Text("Changing affiliation of member"), descriptionView: - Text("Changing \(contact.contactJid as String) to ") + Text(affiliationToString(newAffiliation)) + Text("...")) + Text("Changing \(contact.contactJid as String) to ") + Text(mucAffiliationToString(newAffiliation))) performAction(Text("Error changing affiliation!")) { DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 1.0) { - account.mucProcessor.setAffiliation(newAffiliation, ofUser:contact.contactJid, inMuc:self.group.contactJid) + account.mucProcessor.setAffiliation(newAffiliation, ofUser:contact.contactJid, inMuc:self.muc.contactJid) } } } } ), label: EmptyView()) { ForEach(actionsAllowed(for:contact), id:\.self) { affiliation in - Text(affiliationToString(affiliation)).tag(affiliation) + Text(mucAffiliationToString(affiliation)).tag(affiliation) } } .pickerStyle(.menu) @@ -218,11 +240,11 @@ struct MemberList: View { } .onDelete(perform: { memberIdx in let member = memberList[memberIdx.first!] - showActionSheet(title: Text("Remove user?"), description: self.group.mucType == "group" ? Text("Do you want to remove this user from this group? The user won't be able to enter it again until added back to the group.") : Text("Do you want to remove this user from this channel? The user will be able to enter it again.")) { + showActionSheet(title: Text("Remove user?"), description: self.muc.mucType == "group" ? Text("Do you want to remove this user from this group? The user won't be able to enter it again until added back to the group.") : Text("Do you want to remove this user from this channel? The user will be able to enter it again.")) { showLoadingOverlay(overlay, headlineView: Text("Removing member"), descriptionView: Text("Removing \(member.contactJid as String)...")) performAction(Text("Error removing user!")) { DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 1.0) { - account.mucProcessor.setAffiliation("none", ofUser: member.contactJid, inMuc: self.group.contactJid) + account.mucProcessor.setAffiliation("none", ofUser: member.contactJid, inMuc: self.muc.contactJid) } } } @@ -234,7 +256,7 @@ struct MemberList: View { HStack(alignment: .center) { ContactEntry(contact:contact) Spacer() - Text(affiliationToString(affiliations[contact])) + Text(mucAffiliationToString(affiliations[contact])) } } .deleteDisabled(true) @@ -243,10 +265,6 @@ struct MemberList: View { } } } - .addLoadingOverlay(overlay) - .alert(isPresented: $showAlert, content: { - Alert(title: alertPrompt.title, message: alertPrompt.message, dismissButton: .default(alertPrompt.dismissLabel)) - }) .actionSheet(isPresented: $showActionSheet) { ActionSheet( title: actionSheetPrompt.title, @@ -260,6 +278,10 @@ struct MemberList: View { ] ) } + .alert(isPresented: $showAlert, content: { + Alert(title: alertPrompt.title, message: alertPrompt.message, dismissButton: .default(alertPrompt.dismissLabel)) + }) + .addLoadingOverlay(overlay) .navigationBarTitle("Group Members", displayMode: .inline) .onAppear { updateMemberlist() @@ -267,7 +289,7 @@ struct MemberList: View { .onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("kMonalMucParticipantsAndMembersUpdated")).receive(on: RunLoop.main)) { notification in if let xmppAccount = notification.object as? xmpp, let contact = notification.userInfo?["contact"] as? MLContact { DDLogVerbose("Got muc participants/members update from account \(xmppAccount)...") - if contact == group { + if contact == self.muc { updateMemberlist() hideLoadingOverlay(overlay) } diff --git a/Monal/Classes/SwiftuiHelpers.swift b/Monal/Classes/SwiftuiHelpers.swift index f4539a6ad1..4596d2f35a 100644 --- a/Monal/Classes/SwiftuiHelpers.swift +++ b/Monal/Classes/SwiftuiHelpers.swift @@ -88,6 +88,27 @@ func getContactList(viewContact: (ObservableKVOWrapper?)) -> OrderedS } } +func mucAffiliationToString(_ affiliation: String?) -> String { + if let affiliation = affiliation { + if affiliation == "owner" { + return NSLocalizedString("Owner", comment:"muc affiliation") + } else if affiliation == "admin" { + return NSLocalizedString("Admin", comment:"muc affiliation") + } else if affiliation == "member" { + return NSLocalizedString("Member", comment:"muc affiliation") + } else if affiliation == "none" { + return NSLocalizedString("Participant", comment:"muc affiliation") + } else if affiliation == "outcast" { + return NSLocalizedString("Blocked", comment:"muc affiliation") + } else if affiliation == "profile" { + return NSLocalizedString("Open contact details", comment:"muc members list") + } else if affiliation == "reinvite" { + return NSLocalizedString("Invite again", comment:"muc invite") + } + } + return NSLocalizedString("", comment:"muc affiliation") +} + //see here for some ideas used herein: https://blog.logrocket.com/adding-gifs-ios-app-flanimatedimage-swiftui/#using-flanimatedimage-with-swift struct GIFViewer: UIViewRepresentable { typealias UIViewType = FLAnimatedImageView From bed01d018bd593e616302ef24974cb731f2bd9c9 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Sun, 19 May 2024 23:16:49 +0200 Subject: [PATCH 086/143] Bring back upload hud when sharing through sharesheet --- Monal/Classes/MonalAppDelegate.m | 126 ++++++++++++++++--------------- 1 file changed, 64 insertions(+), 62 deletions(-) diff --git a/Monal/Classes/MonalAppDelegate.m b/Monal/Classes/MonalAppDelegate.m index e07a8c237f..f640f138fd 100644 --- a/Monal/Classes/MonalAppDelegate.m +++ b/Monal/Classes/MonalAppDelegate.m @@ -1949,7 +1949,6 @@ -(void) sendAllOutboxes } //open the destination chat only once - BOOL alreadyOpen = NO; for(NSDictionary* payload in [[DataLayer sharedInstance] getShareSheetPayload]) { DDLogInfo(@"Sending outbox entry: %@", payload); @@ -1965,16 +1964,6 @@ -(void) sendAllOutboxes } MLContact* contact = [MLContact createContactFromJid:payload[@"recipient"] andAccountNo:account.accountNo]; - DDLogVerbose(@"Trying to open chat of outbox receiver: %@", contact); - [[DataLayer sharedInstance] addActiveBuddies:contact.contactJid forAccount:contact.accountId]; - //don't use [self openChatOfContact:withCompletion:] because it's asynchronous and can only handle one contact at a time (e.g. until the asynchronous execution finished) - //we can invoke the activeChats interface directly instead, because we already did the necessary preparations ourselves - if(!alreadyOpen) - { - [(ActiveChatsViewController*)self.activeChats presentChatWithContact:contact]; - alreadyOpen = YES; - } - monal_id_block_t cleanup = ^(NSDictionary* payload) { [[DataLayer sharedInstance] deleteShareSheetPayloadWithId:payload[@"id"]]; [[MLNotificationQueue currentQueue] postNotificationName:kMonalRefresh object:nil userInfo:nil]; @@ -1983,59 +1972,72 @@ -(void) sendAllOutboxes [self.activeChats.currentChatViewController scrollToBottom]; [self.activeChats.currentChatViewController hideUploadHUD]; } + //send next item (if there is one left) + [self sendAllOutboxes]; }; - BOOL encrypted = [[DataLayer sharedInstance] shouldEncryptForJid:contact.contactJid andAccountNo:contact.accountId]; - if([payload[@"type"] isEqualToString:@"text"]) - { - [[MLXMPPManager sharedInstance] sendMessageAndAddToHistory:payload[@"data"] havingType:kMessageTypeText toContact:contact isEncrypted:encrypted uploadInfo:nil withCompletionHandler:^(BOOL successSendObject, NSString* messageIdSentObject) { - DDLogInfo(@"SHARESHEET_SEND_DATA success=%@, account=%@, messageIdSentObject=%@", bool2str(successSendObject), account.accountNo, messageIdSentObject); - cleanup(payload); - }]; - } - else if([payload[@"type"] isEqualToString:@"url"]) - { - [[MLXMPPManager sharedInstance] sendMessageAndAddToHistory:payload[@"data"] havingType:kMessageTypeUrl toContact:contact isEncrypted:encrypted uploadInfo:nil withCompletionHandler:^(BOOL successSendObject, NSString* messageIdSentObject) { - DDLogInfo(@"SHARESHEET_SEND_DATA success=%@, account=%@, messageIdSentObject=%@", bool2str(successSendObject), account.accountNo, messageIdSentObject); - cleanup(payload); - }]; - } - else if([payload[@"type"] isEqualToString:@"geo"]) - { - [[MLXMPPManager sharedInstance] sendMessageAndAddToHistory:payload[@"data"] havingType:kMessageTypeGeo toContact:contact isEncrypted:encrypted uploadInfo:nil withCompletionHandler:^(BOOL successSendObject, NSString* messageIdSentObject) { - DDLogInfo(@"SHARESHEET_SEND_DATA success=%@, account=%@, messageIdSentObject=%@", bool2str(successSendObject), account.accountNo, messageIdSentObject); - cleanup(payload); - }]; - } - else if([payload[@"type"] isEqualToString:@"image"] || [payload[@"type"] isEqualToString:@"file"] || [payload[@"type"] isEqualToString:@"contact"] || [payload[@"type"] isEqualToString:@"audiovisual"]) - { - DDLogInfo(@"Got %@ upload: %@", payload[@"type"], payload[@"data"]); - [self.activeChats.currentChatViewController showUploadHUD]; - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - $call(payload[@"data"], $ID(account), $BOOL(encrypted), $ID(completion, (^(NSString* url, NSString* mimeType, NSNumber* size, NSError* error) { - dispatch_async(dispatch_get_main_queue(), ^{ - if(error != nil) - { - DDLogError(@"Failed to upload outbox file: %@", error); - NSMutableDictionary* payloadCopy = [NSMutableDictionary dictionaryWithDictionary:payload]; - cleanup(payloadCopy); - - UIAlertController* messageAlert = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"Failed to share file", @"") message:[NSString stringWithFormat:NSLocalizedString(@"Error: %@", @""), error] preferredStyle:UIAlertControllerStyleAlert]; - [messageAlert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Close", @"") style:UIAlertActionStyleCancel handler:^(UIAlertAction* action __unused) { - }]]; - [self.activeChats presentViewController:messageAlert animated:YES completion:nil]; - } - else - [[MLXMPPManager sharedInstance] sendMessageAndAddToHistory:url havingType:kMessageTypeFiletransfer toContact:contact isEncrypted:encrypted uploadInfo:@{@"mimeType": mimeType, @"size": size} withCompletionHandler:^(BOOL successSendObject, NSString* messageIdSentObject) { - DDLogInfo(@"SHARESHEET_SEND_DATA success=%@, account=%@, messageIdSentObject=%@", bool2str(successSendObject), account.accountNo, messageIdSentObject); - cleanup(payload); - }]; - }); - }))); - }); - } - else - unreachable(@"Outbox payload type unknown", payload); + monal_id_block_t sendItem = ^(id dummy __unused){ + BOOL encrypted = [[DataLayer sharedInstance] shouldEncryptForJid:contact.contactJid andAccountNo:contact.accountId]; + if([payload[@"type"] isEqualToString:@"text"]) + { + [[MLXMPPManager sharedInstance] sendMessageAndAddToHistory:payload[@"data"] havingType:kMessageTypeText toContact:contact isEncrypted:encrypted uploadInfo:nil withCompletionHandler:^(BOOL successSendObject, NSString* messageIdSentObject) { + DDLogInfo(@"SHARESHEET_SEND_DATA success=%@, account=%@, messageIdSentObject=%@", bool2str(successSendObject), account.accountNo, messageIdSentObject); + cleanup(payload); + }]; + } + else if([payload[@"type"] isEqualToString:@"url"]) + { + [[MLXMPPManager sharedInstance] sendMessageAndAddToHistory:payload[@"data"] havingType:kMessageTypeUrl toContact:contact isEncrypted:encrypted uploadInfo:nil withCompletionHandler:^(BOOL successSendObject, NSString* messageIdSentObject) { + DDLogInfo(@"SHARESHEET_SEND_DATA success=%@, account=%@, messageIdSentObject=%@", bool2str(successSendObject), account.accountNo, messageIdSentObject); + cleanup(payload); + }]; + } + else if([payload[@"type"] isEqualToString:@"geo"]) + { + [[MLXMPPManager sharedInstance] sendMessageAndAddToHistory:payload[@"data"] havingType:kMessageTypeGeo toContact:contact isEncrypted:encrypted uploadInfo:nil withCompletionHandler:^(BOOL successSendObject, NSString* messageIdSentObject) { + DDLogInfo(@"SHARESHEET_SEND_DATA success=%@, account=%@, messageIdSentObject=%@", bool2str(successSendObject), account.accountNo, messageIdSentObject); + cleanup(payload); + }]; + } + else if([payload[@"type"] isEqualToString:@"image"] || [payload[@"type"] isEqualToString:@"file"] || [payload[@"type"] isEqualToString:@"contact"] || [payload[@"type"] isEqualToString:@"audiovisual"]) + { + DDLogInfo(@"Got %@ upload: %@", payload[@"type"], payload[@"data"]); + [self.activeChats.currentChatViewController showUploadHUD]; + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + $call(payload[@"data"], $ID(account), $BOOL(encrypted), $ID(completion, (^(NSString* url, NSString* mimeType, NSNumber* size, NSError* error) { + dispatch_async(dispatch_get_main_queue(), ^{ + if(error != nil) + { + DDLogError(@"Failed to upload outbox file: %@", error); + NSMutableDictionary* payloadCopy = [NSMutableDictionary dictionaryWithDictionary:payload]; + cleanup(payloadCopy); + + UIAlertController* messageAlert = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"Failed to share file", @"") message:[NSString stringWithFormat:NSLocalizedString(@"Error: %@", @""), error] preferredStyle:UIAlertControllerStyleAlert]; + [messageAlert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Close", @"") style:UIAlertActionStyleCancel handler:^(UIAlertAction* action __unused) { + }]]; + [self.activeChats presentViewController:messageAlert animated:YES completion:nil]; + } + else + [[MLXMPPManager sharedInstance] sendMessageAndAddToHistory:url havingType:kMessageTypeFiletransfer toContact:contact isEncrypted:encrypted uploadInfo:@{@"mimeType": mimeType, @"size": size} withCompletionHandler:^(BOOL successSendObject, NSString* messageIdSentObject) { + DDLogInfo(@"SHARESHEET_SEND_DATA success=%@, account=%@, messageIdSentObject=%@", bool2str(successSendObject), account.accountNo, messageIdSentObject); + cleanup(payload); + }]; + }); + }))); + }); + } + else + unreachable(@"Outbox payload type unknown", payload); + }; + + DDLogVerbose(@"Trying to open chat of outbox receiver: %@", contact); + [[DataLayer sharedInstance] addActiveBuddies:contact.contactJid forAccount:contact.accountId]; + //don't use [self openChatOfContact:withCompletion:] because it's asynchronous and can only handle one contact at a time (e.g. until the asynchronous execution finished) + //we can invoke the activeChats interface directly instead, because we already did the necessary preparations ourselves + [(ActiveChatsViewController*)self.activeChats presentChatWithContact:contact andCompletion:sendItem]; + + //only send one item at a time (this method will be invoked again when sending completed) + break; } } From 18ef38323468fc660baefa709b37a9afd44468cb Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Mon, 20 May 2024 03:33:41 +0200 Subject: [PATCH 087/143] Fix all types of scrolling (initial, scroll down, keyboard open etc.) --- Monal/Classes/MLChatInputContainer.m | 6 ++- Monal/Classes/MonalAppDelegate.m | 2 +- Monal/Classes/chatViewController.h | 2 +- Monal/Classes/chatViewController.m | 78 +++++++++++++++------------- 4 files changed, 48 insertions(+), 40 deletions(-) diff --git a/Monal/Classes/MLChatInputContainer.m b/Monal/Classes/MLChatInputContainer.m index e28c329576..cb57367083 100644 --- a/Monal/Classes/MLChatInputContainer.m +++ b/Monal/Classes/MLChatInputContainer.m @@ -37,7 +37,11 @@ -(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event NSArray *subViews = self.subviews; for(UIView *subView in subViews) { if (CGRectContainsPoint(subView.frame, point) && subView.frame.origin.y < 0) { - [self.chatInputActionDelegate doScrollDownAction]; + DDLogDebug(@"ScrollDown button tapped..."); + //without async dispatch this would do nothing + dispatch_async(dispatch_get_main_queue(), ^{ + [self.chatInputActionDelegate doScrollDownAction]; + }); } } return [super pointInside:point withEvent:event]; diff --git a/Monal/Classes/MonalAppDelegate.m b/Monal/Classes/MonalAppDelegate.m index f640f138fd..2b6300eac1 100644 --- a/Monal/Classes/MonalAppDelegate.m +++ b/Monal/Classes/MonalAppDelegate.m @@ -1969,7 +1969,7 @@ -(void) sendAllOutboxes [[MLNotificationQueue currentQueue] postNotificationName:kMonalRefresh object:nil userInfo:nil]; if(self.activeChats.currentChatViewController != nil) { - [self.activeChats.currentChatViewController scrollToBottom]; + [self.activeChats.currentChatViewController scrollToBottomAnimated:NO]; [self.activeChats.currentChatViewController hideUploadHUD]; } //send next item (if there is one left) diff --git a/Monal/Classes/chatViewController.h b/Monal/Classes/chatViewController.h index 1fcf7b497e..f38ec1b1a6 100644 --- a/Monal/Classes/chatViewController.h +++ b/Monal/Classes/chatViewController.h @@ -84,6 +84,6 @@ -(void) showUploadHUD; -(void) hideUploadHUD; --(void) scrollToBottom; +-(void) scrollToBottomAnimated:(BOOL) animated; @end diff --git a/Monal/Classes/chatViewController.m b/Monal/Classes/chatViewController.m index 4474859b5f..fbde09d694 100644 --- a/Monal/Classes/chatViewController.m +++ b/Monal/Classes/chatViewController.m @@ -54,6 +54,7 @@ @interface chatViewController()* _localMLContactCache; BOOL _isRecording; + BOOL _isAtBottom; } @property (nonatomic, strong) NSDateFormatter* destinationDateFormat; @@ -83,9 +84,7 @@ @interface chatViewController()_localMLContactCache) { [self->_localMLContactCache removeAllObjects]; } [self refreshData]; [self reloadTable]; - }); + }]; } -(void) openCallScreen:(id) sender @@ -814,11 +813,12 @@ -(void) viewWillAppear:(BOOL)animated // Set correct chatInput height constraints [self setChatInputHeightConstraints:self.hardwareKeyboardPresent]; - [self scrollToBottom]; [self tempfreezeAutoloading]; [self.contact addObserver:self forKeyPath:@"isEncrypted" options:NSKeyValueObservingOptionNew context:nil]; + + [self scrollToBottomAnimated:NO]; } @@ -1697,7 +1697,7 @@ -(MLMessage* _Nullable) addMessageto:(NSString *)to withMessage:(nonnull NSStrin withRowAnimation:UITableViewRowAnimationNone]; } } completion:^(BOOL finished) { - [self scrollToBottom]; + [self scrollToBottomIfNeeded]; }]; }); @@ -1766,7 +1766,7 @@ -(void) handleNewMessage:(NSNotification *)notification } [self->_messageTable endUpdates]; - [self scrollToBottom]; + [self scrollToBottomIfNeeded]; [CATransaction commit]; if (self.searchController.isActive) @@ -1913,20 +1913,25 @@ -(void) handleFiletransferMessageUpdate:(NSNotification*) notification } } --(void) scrollToBottom +-(void) scrollToBottomIfNeeded { - if(self.messageList.count == 0) return; - dispatch_async(dispatch_get_main_queue(), ^{ + if(_isAtBottom) + [self scrollToBottomAnimated:YES]; +} + +-(void) scrollToBottomAnimated:(BOOL) animated +{ + if(self.messageList.count == 0) + return; + [HelperTools dispatchAsync:YES reentrantOnQueue:dispatch_get_main_queue() withBlock:^{ NSInteger bottom = [self.messageTable numberOfRowsInSection:messagesSection]; if(bottom > 0) { NSIndexPath* path1 = [NSIndexPath indexPathForRow:bottom-1 inSection:messagesSection]; - // if(![self.messageTable.indexPathsForVisibleRows containsObject:path1]) - { - [self.messageTable scrollToRowAtIndexPath:path1 atScrollPosition:UITableViewScrollPositionTop animated:YES]; - } + [self.messageTable scrollToRowAtIndexPath:path1 atScrollPosition:UITableViewScrollPositionTop animated:animated]; + self->_isAtBottom = YES; } - }); + }]; } #pragma mark - date time @@ -2686,23 +2691,18 @@ -(void) scrollViewDidScroll:(UIScrollView *)scrollView // Only load old msgs if the view appeared if(!self.viewDidAppear) return; - + // get current scroll position (y-axis) CGFloat curOffset = scrollView.contentOffset.y; - - if (self.lastOffset > curOffset) - { - [self.lastMsgButton setHidden:NO]; - } - CGFloat bottomLength = scrollView.frame.size.height + curOffset; - - if (scrollView.contentSize.height <= bottomLength) - { + _isAtBottom = scrollView.contentSize.height <= bottomLength; + + if(_isAtBottom) [self.lastMsgButton setHidden:YES]; - } - - self.lastOffset = curOffset; + else + [self.lastMsgButton setHidden:NO]; + + } -(void) loadOldMsgHistory @@ -2923,7 +2923,7 @@ -(void) commandFPressed:(UIKeyCommand*)keyCommand - (void)textViewDidBeginEditing:(UITextView *)textView { - [self scrollToBottom]; + //[self scrollToBottomAnimated:YES]; } - (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text @@ -3129,7 +3129,8 @@ - (void)keyboardWillDisappear:(NSNotification*) aNotification - (void)keyboardDidShow:(NSNotification*)aNotification { - //TODO grab animation info + static BOOL firstTime = YES; + //TODO grab animation info NSDictionary* info = [aNotification userInfo]; CGSize kbSize = [[info objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue].size; if(kbSize.height > 100) { //my inputbar +any other @@ -3139,10 +3140,12 @@ - (void)keyboardDidShow:(NSNotification*)aNotification self.messageTable.contentInset = contentInsets; self.messageTable.scrollIndicatorInsets = contentInsets; - // Only scroll to bottom of the message table if a chat is opened - // don't scroll down on other events like closing a image preview - if(self.viewDidAppear == NO) - [self scrollToBottom]; + //this will be automatically called once the whole chat view is loaded (even if not showing a keyboard) + //--> filter that first call to not scroll a few pixels on view open + //(the few pixels come from some margin/padding only applied after viewDidAppear was called) + if(!firstTime) + [self scrollToBottomIfNeeded]; + firstTime = NO; } - (void)keyboardDidHide:(NSNotification*)aNotification @@ -3157,6 +3160,7 @@ - (void)keyboardDidHide:(NSNotification*)aNotification - (void)keyboardWillShow:(NSNotification*)aNotification { + [self setChatInputHeightConstraints:NO]; //TODO grab animation info // UIEdgeInsets contentInsets = UIEdgeInsetsZero; From e3a3591f69ea17334105084f2313477355a46c5b Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Mon, 20 May 2024 16:50:48 +0200 Subject: [PATCH 088/143] Set thread name in getExtraRunloopWithIdentifier, too Before we only set the runloop name --- Monal/Classes/HelperTools.m | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Monal/Classes/HelperTools.m b/Monal/Classes/HelperTools.m index f63eb3b6ef..7ba6b4df0d 100644 --- a/Monal/Classes/HelperTools.m +++ b/Monal/Classes/HelperTools.m @@ -587,6 +587,8 @@ +(NSRunLoop*) getExtraRunloopWithIdentifier:(MLRunLoopIdentifier) identifier NSCondition* condition = [NSCondition new]; [condition lock]; dispatch_async(dispatch_queue_create_with_target(name, DISPATCH_QUEUE_SERIAL, dispatch_get_global_queue(priority, 0)), ^{ + //set thread name, too (not only runloop name) + [NSThread.currentThread setName:[NSString stringWithFormat:@"%s", name]]; //we don't need an @synchronized block around this because the @synchronized block of the outer thread //waits until we signal our condition (e.g. no other thread can race with us) NSRunLoop* localLoop = runloops[@(identifier)] = [NSRunLoop currentRunLoop]; From 6a3abdfec083b112022de8f2bc2baf3317463477 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Mon, 20 May 2024 21:41:28 +0200 Subject: [PATCH 089/143] Fix scrolling for catalyst, too --- Monal/Classes/chatViewController.m | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Monal/Classes/chatViewController.m b/Monal/Classes/chatViewController.m index fbde09d694..8a2d94a618 100644 --- a/Monal/Classes/chatViewController.m +++ b/Monal/Classes/chatViewController.m @@ -1916,7 +1916,11 @@ -(void) handleFiletransferMessageUpdate:(NSNotification*) notification -(void) scrollToBottomIfNeeded { if(_isAtBottom) - [self scrollToBottomAnimated:YES]; + { + dispatch_async(dispatch_get_main_queue(), ^{ + [self scrollToBottomAnimated:YES]; + }); + } } -(void) scrollToBottomAnimated:(BOOL) animated @@ -3143,8 +3147,12 @@ - (void)keyboardDidShow:(NSNotification*)aNotification //this will be automatically called once the whole chat view is loaded (even if not showing a keyboard) //--> filter that first call to not scroll a few pixels on view open //(the few pixels come from some margin/padding only applied after viewDidAppear was called) +#if TARGET_OS_MACCATALYST + [self scrollToBottomIfNeeded]; +#else if(!firstTime) [self scrollToBottomIfNeeded]; +#endif firstTime = NO; } From 0c72aa2c5d426a7a9e3323d4e9502525f720b2da Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Mon, 20 May 2024 21:51:01 +0200 Subject: [PATCH 090/143] Make DNSSEC validation configurable (default: off) In alpha builds it will be default: on --- Monal/Classes/HelperTools.m | 3 ++- Monal/Classes/MLFiletransfer.m | 3 ++- Monal/Classes/MLHTTPRequest.m | 3 ++- Monal/Classes/MLStream.m | 4 +++- Monal/Classes/MLVoIPProcessor.m | 6 ++++-- Monal/Classes/MLWebViewController.m | 4 +++- Monal/Classes/MLXMPPManager.m | 6 ++++++ Monal/Classes/PrivacySettings.swift | 9 +++++++++ Monal/Classes/RegisterAccount.swift | 2 +- Monal/Classes/chatViewController.m | 6 ++++-- 10 files changed, 36 insertions(+), 10 deletions(-) diff --git a/Monal/Classes/HelperTools.m b/Monal/Classes/HelperTools.m index 7ba6b4df0d..abfa82d971 100644 --- a/Monal/Classes/HelperTools.m +++ b/Monal/Classes/HelperTools.m @@ -2729,7 +2729,8 @@ +(NSURLSession*) createEphemeralURLSession { NSURLSessionConfiguration* sessionConfig = [NSURLSessionConfiguration ephemeralSessionConfiguration]; if(@available(iOS 16.1, macCatalyst 16.1, *)) - sessionConfig.requiresDNSSECValidation = YES; + if([[HelperTools defaultsDB] boolForKey: @"useDnssecForAllConnections"]) + sessionConfig.requiresDNSSECValidation = YES; return [NSURLSession sessionWithConfiguration:sessionConfig]; } diff --git a/Monal/Classes/MLFiletransfer.m b/Monal/Classes/MLFiletransfer.m index 41b4427f1d..804d565f94 100644 --- a/Monal/Classes/MLFiletransfer.m +++ b/Monal/Classes/MLFiletransfer.m @@ -78,7 +78,8 @@ +(void) checkMimeTypeAndSizeForHistoryID:(NSNumber*) historyId DDLogInfo(@"Requesting mime-type and size for historyID %@ from http server", historyId); NSMutableURLRequest* request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:url]]; if(@available(iOS 16.1, macCatalyst 16.1, *)) - request.requiresDNSSECValidation = YES; + if([[HelperTools defaultsDB] boolForKey: @"useDnssecForAllConnections"]) + request.requiresDNSSECValidation = YES; request.HTTPMethod = @"HEAD"; request.cachePolicy = NSURLRequestReturnCacheDataElseLoad; diff --git a/Monal/Classes/MLHTTPRequest.m b/Monal/Classes/MLHTTPRequest.m index acce093a69..de50109ffb 100644 --- a/Monal/Classes/MLHTTPRequest.m +++ b/Monal/Classes/MLHTTPRequest.m @@ -48,7 +48,8 @@ +(void) sendWithVerb:(NSString*) verb path:(NSString*) path headers:(NSDictiona cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:60.0]; if(@available(iOS 16.1, macCatalyst 16.1, *)) - theRequest.requiresDNSSECValidation = YES; + if([[HelperTools defaultsDB] boolForKey: @"useDnssecForAllConnections"]) + theRequest.requiresDNSSECValidation = YES; [theRequest setHTTPMethod:verb]; NSData* dataToSubmit = postedData; diff --git a/Monal/Classes/MLStream.m b/Monal/Classes/MLStream.m index 3ae299f29a..4a2c6562b1 100644 --- a/Monal/Classes/MLStream.m +++ b/Monal/Classes/MLStream.m @@ -530,8 +530,10 @@ +(void) connectWithSNIDomain:(NSString*) SNIDomain connectHost:(NSString*) host } //needed to activate tcp fast open with apple's internal tls framer nw_parameters_set_fast_open_enabled(parameters, YES); + //use dnssec if configured if(@available(iOS 16.0, macCatalyst 16.0, *)) - nw_parameters_set_requires_dnssec_validation(parameters, YES); + if([[HelperTools defaultsDB] boolForKey: @"useDnssecForAllConnections"]) + nw_parameters_set_requires_dnssec_validation(parameters, YES); //create and configure connection object nw_endpoint_t endpoint = nw_endpoint_create_host([host cStringUsingEncoding:NSUTF8StringEncoding], [[port stringValue] cStringUsingEncoding:NSUTF8StringEncoding]); diff --git a/Monal/Classes/MLVoIPProcessor.m b/Monal/Classes/MLVoIPProcessor.m index a46a939487..36021c6131 100644 --- a/Monal/Classes/MLVoIPProcessor.m +++ b/Monal/Classes/MLVoIPProcessor.m @@ -404,7 +404,8 @@ -(void) initWebRTCForPendingCall:(MLCall*) call // request turn credentials NSMutableURLRequest* urlRequest = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"/api/v1/challenge/new" relativeToURL:[HelperTools getFailoverTurnApiServer]]]; if(@available(iOS 16.1, macCatalyst 16.1, *)) - urlRequest.requiresDNSSECValidation = YES; + if([[HelperTools defaultsDB] boolForKey: @"useDnssecForAllConnections"]) + urlRequest.requiresDNSSECValidation = YES; [urlRequest setTimeoutInterval:3.0]; NSURLSession* challengeSession = [HelperTools createEphemeralURLSession]; [[challengeSession dataTaskWithRequest:urlRequest completionHandler:^(NSData* data, NSURLResponse* response, NSError* error) { @@ -444,7 +445,8 @@ -(void) initWebRTCForPendingCall:(MLCall*) call } NSMutableURLRequest* responseRequest = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"/api/v1/challenge/validate" relativeToURL:[HelperTools getFailoverTurnApiServer]]]; if(@available(iOS 16.1, macCatalyst 16.1, *)) - responseRequest.requiresDNSSECValidation = YES; + if([[HelperTools defaultsDB] boolForKey: @"useDnssecForAllConnections"]) + responseRequest.requiresDNSSECValidation = YES; [responseRequest setHTTPMethod:@"POST"]; [responseRequest setValue:@"application/json" forHTTPHeaderField:@"Accept"]; diff --git a/Monal/Classes/MLWebViewController.m b/Monal/Classes/MLWebViewController.m index ea42e4c95d..33ae3a260f 100644 --- a/Monal/Classes/MLWebViewController.m +++ b/Monal/Classes/MLWebViewController.m @@ -7,6 +7,7 @@ // #import "MLWebViewController.h" +#import "HelperTools.h" @interface MLWebViewController () @property (weak, nonatomic) IBOutlet WKWebView* webview; @@ -31,7 +32,8 @@ -(void) viewWillAppear:(BOOL)animated { NSMutableURLRequest* nsrequest = [NSMutableURLRequest requestWithURL: self.urltoLoad]; if(@available(iOS 16.1, macCatalyst 16.1, *)) - nsrequest.requiresDNSSECValidation = YES; + if([[HelperTools defaultsDB] boolForKey: @"useDnssecForAllConnections"]) + nsrequest.requiresDNSSECValidation = YES; [self.webview loadRequest:nsrequest]; } self.navigationItem.largeTitleDisplayMode = UINavigationItemLargeTitleDisplayModeNever; diff --git a/Monal/Classes/MLXMPPManager.m b/Monal/Classes/MLXMPPManager.m index 01247a8ead..35857c5bac 100644 --- a/Monal/Classes/MLXMPPManager.m +++ b/Monal/Classes/MLXMPPManager.m @@ -149,6 +149,12 @@ -(void) defaultSettings #else [self upgradeBoolUserSettingsIfUnset:@"showKeyboardOnChatOpen" toDefault:NO]; #endif + +#ifdef IS_ALPHA + [self upgradeBoolUserSettingsIfUnset:@"useDnssecForAllConnections" toDefault:YES]; +#else + [self upgradeBoolUserSettingsIfUnset:@"useDnssecForAllConnections" toDefault:NO]; +#endif } -(void) upgradeFloatUserSettingsToInteger:(NSString*) settingsName diff --git a/Monal/Classes/PrivacySettings.swift b/Monal/Classes/PrivacySettings.swift index b86f41c07a..6e087a3ab9 100644 --- a/Monal/Classes/PrivacySettings.swift +++ b/Monal/Classes/PrivacySettings.swift @@ -78,6 +78,9 @@ class PrivacyDefaultDB: ObservableObject { @defaultsDB("showKeyboardOnChatOpen") var showKeyboardOnChatOpen: Bool + + @defaultsDB("useDnssecForAllConnections") + var useDnssecForAllConnections: Bool } @@ -159,6 +162,12 @@ struct PrivacyScreen: View { Toggle(isOn: $privacyDefaultDB.autodeleteAllMessagesAfter3Days) { Text("Autodelete all messages after 3 days") } + if #available(iOS 16.0, macCatalyst 16.0, *) { + Text("Use DNSSEC to validate all DNS query responses before connecting to the IP designated in the DNS response. While being more secure, this can lead to connection problems in certain networks like Hotel WiFi etc.") + Toggle(isOn: $privacyDefaultDB.useDnssecForAllConnections) { + Text("Use DNSSEC validation for all connections") + } + } } .navigationBarTitle("Privacy & security", displayMode: .inline) } diff --git a/Monal/Classes/RegisterAccount.swift b/Monal/Classes/RegisterAccount.swift index 35d0c86616..7df568471e 100644 --- a/Monal/Classes/RegisterAccount.swift +++ b/Monal/Classes/RegisterAccount.swift @@ -18,7 +18,7 @@ struct WebView: UIViewRepresentable { func updateUIView(_ webView: WKWebView, context: Context) { var request = URLRequest(url: url) - if #available(iOS 16.1, macCatalyst 16.1, *) { + if #available(iOS 16.1, macCatalyst 16.1, *), HelperTools.defaultsDB().bool(forKey:"useDnssecForAllConnections") { request.requiresDNSSECValidation = true; } webView.load(request) diff --git a/Monal/Classes/chatViewController.m b/Monal/Classes/chatViewController.m index 8a2d94a618..b5b18f830a 100644 --- a/Monal/Classes/chatViewController.m +++ b/Monal/Classes/chatViewController.m @@ -3008,7 +3008,8 @@ -(void) loadPreviewWithUrlForRow:(NSIndexPath *) indexPath withResultHandler:(mo DDLogVerbose(@"Fetching HTTP HEAD for %@...", row.url); NSMutableURLRequest* headRequest = [[NSMutableURLRequest alloc] initWithURL:row.url]; if(@available(iOS 16.1, macCatalyst 16.1, *)) - headRequest.requiresDNSSECValidation = YES; + if([[HelperTools defaultsDB] boolForKey: @"useDnssecForAllConnections"]) + headRequest.requiresDNSSECValidation = YES; headRequest.HTTPMethod = @"HEAD"; headRequest.cachePolicy = NSURLRequestReturnCacheDataElseLoad; NSURLSession* session = [HelperTools createEphemeralURLSession]; @@ -3083,7 +3084,8 @@ -(void) downloadPreviewWithRow:(NSIndexPath*) indexPath usingByterange:(BOOL) us DDLogVerbose(@"Fetching HTTP GET for %@...", row.url); NSMutableURLRequest* request = [[NSMutableURLRequest alloc] initWithURL:row.url]; if(@available(iOS 16.1, macCatalyst 16.1, *)) - request.requiresDNSSECValidation = YES; + if([[HelperTools defaultsDB] boolForKey: @"useDnssecForAllConnections"]) + request.requiresDNSSECValidation = YES; [request setValue:@"facebookexternalhit/1.1" forHTTPHeaderField:@"User-Agent"]; //required on some sites for og tags e.g. youtube if(useByterange) [request setValue:@"bytes=0-524288" forHTTPHeaderField:@"Range"]; From aafd6072813db8a2a74567005cc72855731d7bff Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Mon, 20 May 2024 22:58:17 +0200 Subject: [PATCH 091/143] Fix swiftui warning in privacy settings --- Monal/Classes/PrivacySettings.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Monal/Classes/PrivacySettings.swift b/Monal/Classes/PrivacySettings.swift index 6e087a3ab9..4d601c1d5b 100644 --- a/Monal/Classes/PrivacySettings.swift +++ b/Monal/Classes/PrivacySettings.swift @@ -155,7 +155,7 @@ struct PrivacyScreen: View { Text(getNotificationPrivacyOption(option)).tag(option.rawValue) } } - .frame(width: .infinity, height: 56, alignment: .trailing) + .frame(height: 56, alignment: .trailing) Toggle(isOn: $privacyDefaultDB.omemoDefaultOn) { Text("Enable encryption by default for new chats") } From 957f47f1094483004d807b1d22ac7bbdc6bfb611 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Mon, 20 May 2024 22:58:32 +0200 Subject: [PATCH 092/143] Rework scroll to bottom again --- Monal/Classes/chatViewController.m | 48 ++++++++++++++++++------------ 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/Monal/Classes/chatViewController.m b/Monal/Classes/chatViewController.m index b5b18f830a..0a45fdb52c 100644 --- a/Monal/Classes/chatViewController.m +++ b/Monal/Classes/chatViewController.m @@ -55,6 +55,7 @@ @interface chatViewController()* _localMLContactCache; BOOL _isRecording; BOOL _isAtBottom; + monal_void_block_t _scrollToBottomTimer; } @property (nonatomic, strong) NSDateFormatter* destinationDateFormat; @@ -1685,6 +1686,7 @@ -(MLMessage* _Nullable) addMessageto:(NSString *)to withMessage:(nonnull NSStrin //update message list in ui dispatch_async(dispatch_get_main_queue(), ^{ + BOOL wasAtBottom = self->_isAtBottom; [self.messageTable performBatchUpdates:^{ if(!self.messageList) self.messageList = [NSMutableArray new]; @@ -1697,7 +1699,8 @@ -(MLMessage* _Nullable) addMessageto:(NSString *)to withMessage:(nonnull NSStrin withRowAnimation:UITableViewRowAnimationNone]; } } completion:^(BOOL finished) { - [self scrollToBottomIfNeeded]; + if(wasAtBottom) + [self scrollToBottomAnimated:NO]; }]; }); @@ -1730,6 +1733,8 @@ -(void) handleNewMessage:(NSNotification *)notification if([message isEqualToContact:self.contact]) { dispatch_async(dispatch_get_main_queue(), ^{ + BOOL wasAtBottom = self->_isAtBottom; + if(!self.messageList) self.messageList = [NSMutableArray new]; @@ -1766,7 +1771,7 @@ -(void) handleNewMessage:(NSNotification *)notification } [self->_messageTable endUpdates]; - [self scrollToBottomIfNeeded]; + [CATransaction commit]; if (self.searchController.isActive) @@ -1777,6 +1782,9 @@ -(void) handleNewMessage:(NSNotification *)notification } [self refreshCounter]; + + if(wasAtBottom) + [self scrollToBottomAnimated:YES]; }); } } @@ -1917,9 +1925,8 @@ -(void) scrollToBottomIfNeeded { if(_isAtBottom) { - dispatch_async(dispatch_get_main_queue(), ^{ - [self scrollToBottomAnimated:YES]; - }); + //DDLogVerbose(@"Scrolling to bottom because needed: %@", [NSThread callStackSymbols]); + [self scrollToBottomAnimated:NO]; } } @@ -1927,15 +1934,27 @@ -(void) scrollToBottomAnimated:(BOOL) animated { if(self.messageList.count == 0) return; - [HelperTools dispatchAsync:YES reentrantOnQueue:dispatch_get_main_queue() withBlock:^{ + monal_void_block_t scrollBlock = ^{ NSInteger bottom = [self.messageTable numberOfRowsInSection:messagesSection]; if(bottom > 0) { + DDLogVerbose(@"Scrolling to bottom(%@): %@", bool2str(animated), [NSThread callStackSymbols]); NSIndexPath* path1 = [NSIndexPath indexPathForRow:bottom-1 inSection:messagesSection]; - [self.messageTable scrollToRowAtIndexPath:path1 atScrollPosition:UITableViewScrollPositionTop animated:animated]; + [self.messageTable scrollToRowAtIndexPath:path1 atScrollPosition:UITableViewScrollPositionBottom animated:animated]; self->_isAtBottom = YES; } - }]; + }; + if(animated) + { + DDLogVerbose(@"Registering timer for scrolling to bottom(%@): %@", bool2str(animated), [NSThread callStackSymbols]); + if(_scrollToBottomTimer) + _scrollToBottomTimer(); + _scrollToBottomTimer = createQueuedTimer(0.1, dispatch_get_main_queue(), (^{ + scrollBlock(); + })); + } + else + [HelperTools dispatchAsync:NO reentrantOnQueue:dispatch_get_main_queue() withBlock:scrollBlock]; } #pragma mark - date time @@ -2925,9 +2944,9 @@ -(void) commandFPressed:(UIKeyCommand*)keyCommand # pragma mark - Textview delegate functions -- (void)textViewDidBeginEditing:(UITextView *)textView +-(void) textViewDidBeginEditing:(UITextView*) textView { - //[self scrollToBottomAnimated:YES]; + [self scrollToBottomIfNeeded]; } - (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text @@ -3135,7 +3154,6 @@ - (void)keyboardWillDisappear:(NSNotification*) aNotification - (void)keyboardDidShow:(NSNotification*)aNotification { - static BOOL firstTime = YES; //TODO grab animation info NSDictionary* info = [aNotification userInfo]; CGSize kbSize = [[info objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue].size; @@ -3147,15 +3165,7 @@ - (void)keyboardDidShow:(NSNotification*)aNotification self.messageTable.scrollIndicatorInsets = contentInsets; //this will be automatically called once the whole chat view is loaded (even if not showing a keyboard) - //--> filter that first call to not scroll a few pixels on view open - //(the few pixels come from some margin/padding only applied after viewDidAppear was called) -#if TARGET_OS_MACCATALYST [self scrollToBottomIfNeeded]; -#else - if(!firstTime) - [self scrollToBottomIfNeeded]; -#endif - firstTime = NO; } - (void)keyboardDidHide:(NSNotification*)aNotification From 180d93685afadc85979d1236366f72dc22f94c12 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Tue, 21 May 2024 01:13:40 +0200 Subject: [PATCH 093/143] Prepare SRV resolver for DNSSEC (but commented out) This can only be activated once the ios bug has been fixed --- Monal/Classes/MLDNSLookup.m | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/Monal/Classes/MLDNSLookup.m b/Monal/Classes/MLDNSLookup.m index 1055138fb1..8116f7a02c 100644 --- a/Monal/Classes/MLDNSLookup.m +++ b/Monal/Classes/MLDNSLookup.m @@ -45,7 +45,7 @@ -(void) doDiscoveryWithSecure:(BOOL) secure andDomain:(NSString*) domain withTim NSString* serviceDiscoveryString = [NSString stringWithFormat:@"_xmpp%@-client._tcp.%@", secure ? @"s" : @"", domain]; res = DNSServiceQueryRecord( &sdRef, - kDNSServiceFlagsReturnIntermediates, + kDNSServiceFlagsReturnIntermediates, // | kDNSServiceFlagsValidate, 0, [serviceDiscoveryString UTF8String], kDNSServiceType_SRV, @@ -125,6 +125,8 @@ -(NSArray*) doRealDnsDiscoverOnDomain:(NSString*) domain withTimeout:(NSTimeInte //wait for both dns queries to complete dispatch_barrier_sync(queue, ^{ DDLogVerbose(@"SRV DNS queries completed (xmpps AND xmpp)..."); +// [HelperTools flushLogsWithTimeout:0.100]; +// exit(0); }); @synchronized(self.discoveredServers) { @@ -240,16 +242,13 @@ void query_cb(const DNSServiceRef DNSServiceRef, const DNSServiceFlags flags, co { //make sure the compiler doesn't cry because of unused arguments (void)DNSServiceRef; - (void)flags; (void)interfaceIndex; (void)rrclass; - (void)ttl; - (void)_context; //just ignore errors (don't fill anything into the discoveredServers array) if(errorCode) { - // DDLogVerbose(@"query callback: error==%d\n", errorCode); + DDLogVerbose(@"query callback: error==%d\n", errorCode); return; } @@ -265,7 +264,7 @@ void query_cb(const DNSServiceRef DNSServiceRef, const DNSServiceFlags flags, co if(srvDomainLen > MAX_DOMAIN_NAME) return; ConvertDomainNameToCString_withescape(&srv->target, srvDomainLen, targetStr, 0); - DDLogVerbose(@"pri=%d, w=%d, port=%d, target=%s, ttl=%u\n", ntohs(srv->priority), ntohs(srv->weight), ntohs(srv->port), targetStr, ttl); + DDLogVerbose(@"pri=%d, w=%d, port=%d, target=%s, ttl=%u, flags=%u\n", ntohs(srv->priority), ntohs(srv->weight), ntohs(srv->port), targetStr, ttl, (u_int32_t)flags); NSString* theServer = [NSString stringWithUTF8String:targetStr]; NSNumber* prio = [NSNumber numberWithUnsignedInt:(ntohs(srv->priority) + (isSecure == YES ? 0 : UINT16_MAX))]; // prefer TLS over STARTTLS From abfc7a63767fad5e3c3941b82d43a3f52cc5c529 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Mon, 20 May 2024 23:34:21 +0200 Subject: [PATCH 094/143] Only send outgoing displayed markers if request by contact (XEP-0333) --- Monal/Classes/MonalAppDelegate.m | 13 ++------ Monal/Classes/chatViewController.m | 9 ++---- Monal/Classes/xmpp.h | 2 +- Monal/Classes/xmpp.m | 52 +++++++++++++++++++++--------- 4 files changed, 43 insertions(+), 33 deletions(-) diff --git a/Monal/Classes/MonalAppDelegate.m b/Monal/Classes/MonalAppDelegate.m index 2b6300eac1..18e8c81a3e 100644 --- a/Monal/Classes/MonalAppDelegate.m +++ b/Monal/Classes/MonalAppDelegate.m @@ -951,16 +951,9 @@ -(void) userNotificationCenter:(UNUserNotificationCenter*) center didReceiveNoti NSArray* unread = [[DataLayer sharedInstance] markMessagesAsReadForBuddy:fromContact.contactJid andAccount:fromContact.accountId tillStanzaId:messageId wasOutgoing:NO]; DDLogDebug(@"Marked as read: %@", unread); - //send displayed marker for last unread message *marked as wanting chat markers* (XEP-0333) -// for(MLMessage* msg in unread) -// ; //TODO: implement this!! - - MLMessage* lastUnreadMessage = [unread lastObject]; - if(lastUnreadMessage) - { - DDLogDebug(@"Sending XEP-0333 displayed marker for message '%@'", lastUnreadMessage.messageId); - [account sendDisplayMarkerForMessage:lastUnreadMessage]; - } + //publish MDS display marker and optionally send displayed marker for last unread message (XEP-0333) + DDLogDebug(@"Sending MDS (and possibly XEP-0333 displayed marker) for messages: %@", unread); + [account sendDisplayMarkerForMessages:unread]; //remove notifications of all read messages (this will cause the MLNotificationManager to update the app badge, too) [[MLNotificationQueue currentQueue] postNotificationName:kMonalDisplayedMessagesNotice object:account userInfo:@{@"messagesArray":unread}]; diff --git a/Monal/Classes/chatViewController.m b/Monal/Classes/chatViewController.m index 0a45fdb52c..d404b74115 100644 --- a/Monal/Classes/chatViewController.m +++ b/Monal/Classes/chatViewController.m @@ -1005,12 +1005,8 @@ -(void) refreshCounter NSArray* unread = [[DataLayer sharedInstance] markMessagesAsReadForBuddy:self.contact.contactJid andAccount:self.contact.accountId tillStanzaId:nil wasOutgoing:NO]; //publish MDS display marker and optionally send displayed marker for last unread message (XEP-0333) - MLMessage* lastUnreadMessage = [unread lastObject]; - if(lastUnreadMessage) - { - DDLogDebug(@"Sending XEP-0333 displayed marker for message '%@'", lastUnreadMessage.messageId); - [self.xmppAccount sendDisplayMarkerForMessage:lastUnreadMessage]; - } + DDLogDebug(@"Sending MDS (and possibly XEP-0333 displayed marker) for messages: %@", unread); + [self.xmppAccount sendDisplayMarkerForMessages:unread]; //now switch back to the main thread, we are reading only (and self.contact should only be accessed from the main thread) dispatch_async(dispatch_get_main_queue(), ^{ @@ -1943,6 +1939,7 @@ -(void) scrollToBottomAnimated:(BOOL) animated [self.messageTable scrollToRowAtIndexPath:path1 atScrollPosition:UITableViewScrollPositionBottom animated:animated]; self->_isAtBottom = YES; } + [self refreshCounter]; }; if(animated) { diff --git a/Monal/Classes/xmpp.h b/Monal/Classes/xmpp.h index a34be781da..49302cd42f 100644 --- a/Monal/Classes/xmpp.h +++ b/Monal/Classes/xmpp.h @@ -228,7 +228,7 @@ typedef void (^monal_iq_handler_t)(XMPPIQ* _Nullable); -(NSMutableArray*) getOrderedMamPageFor:(NSString*) mamQueryId; -(void) bindResource:(NSString*) resource; -(void) initSession; --(void) sendDisplayMarkerForMessage:(MLMessage*) msg; +-(void) sendDisplayMarkerForMessages:(NSArray*) unread; -(void) publishAvatar:(UIImage*) image; -(void) publishStatusMessage:(NSString*) message; -(void) delayIncomingMessageStanzasForArchiveJid:(NSString*) archiveJid; diff --git a/Monal/Classes/xmpp.m b/Monal/Classes/xmpp.m index ae9cdc2531..c567e94e6b 100644 --- a/Monal/Classes/xmpp.m +++ b/Monal/Classes/xmpp.m @@ -5451,42 +5451,62 @@ -(void) publishMDSMarkerForMessage:(MLMessage*) msg }]; } --(void) sendDisplayMarkerForMessage:(MLMessage*) msg +-(void) sendDisplayMarkerForMessages:(NSArray*) unread { + //ignore empty arrays + if(unread.count == 0) + return; + + //send displayed marker for last unread message *marked as wanting chat markers* (XEP-0333) + MLMessage* lastMarkableMessage = nil; + for(MLMessage* msg in unread) + if(msg.displayMarkerWanted) + lastMarkableMessage = msg; + + //last unread message used for mds + MLMessage* lastUnreadMessage = [unread lastObject]; + if(![[HelperTools defaultsDB] boolForKey:@"SendDisplayedMarkers"]) { DDLogVerbose(@"Not sending chat marker, configured to not do so..."); - [self publishMDSMarkerForMessage:msg]; //always publish mds marker + [self publishMDSMarkerForMessage:lastUnreadMessage]; //always publish mds marker return; } - //don't send chatmarkers in channels - if(msg.isMuc && [@"channel" isEqualToString:msg.mucType]) + //don't send chatmarkers in channels (all messages have the same muc attributes, randomly pick the last one) + if(lastUnreadMessage.isMuc && [@"channel" isEqualToString:lastUnreadMessage.mucType]) { DDLogVerbose(@"Not sending XEP-0333 chat marker in channel..."); - [self publishMDSMarkerForMessage:msg]; //always publish mds marker + [self publishMDSMarkerForMessage:lastUnreadMessage]; //always publish mds marker return; } - MLContact* contact = [MLContact createContactFromJid:msg.buddyName andAccountNo:msg.accountId]; + //all messages have the same contact, randomly pick the last one + MLContact* contact = [MLContact createContactFromJid:lastUnreadMessage.buddyName andAccountNo:lastUnreadMessage.accountId]; //don't send chatmarkers to 1:1 chats with users in our contact list that did not subscribe us (e.g. are not allowed to see us) if(!contact.isGroup && !contact.isSubscribedFrom) { DDLogVerbose(@"Not sending chat marker, we are not subscribed from this contact..."); - [self publishMDSMarkerForMessage:msg]; //always publish mds marker + [self publishMDSMarkerForMessage:lastUnreadMessage]; //always publish mds marker return; } - XMPPMessage* displayedNode = [[XMPPMessage alloc] initToContact:contact]; - [displayedNode setDisplayed:msg.isMuc && msg.stanzaId != nil ? msg.stanzaId : msg.messageId]; - if([self.connectionProperties.accountFeatures containsObject:@"urn:xmpp:mds:server-assist:0"]) - [displayedNode setMDSDisplayed:msg.stanzaId withStanzaIdBy:(msg.isMuc ? msg.buddyName : self.connectionProperties.identity.jid)]; - [displayedNode setStoreHint]; - DDLogVerbose(@"Sending display marker: %@", displayedNode); - [self send:displayedNode]; + //only send chatmarkers if requested by contact + BOOL assistedMDS = [self.connectionProperties.accountFeatures containsObject:@"urn:xmpp:mds:server-assist:0"] && lastMarkableMessage == lastUnreadMessage; + if(lastMarkableMessage != nil) + { + XMPPMessage* displayedNode = [[XMPPMessage alloc] initToContact:contact]; + [displayedNode setDisplayed:lastMarkableMessage.isMuc && lastMarkableMessage.stanzaId != nil ? lastMarkableMessage.stanzaId : lastMarkableMessage.messageId]; + if(assistedMDS) + [displayedNode setMDSDisplayed:lastMarkableMessage.stanzaId withStanzaIdBy:(lastMarkableMessage.isMuc ? lastMarkableMessage.buddyName : self.connectionProperties.identity.jid)]; + [displayedNode setStoreHint]; + DDLogVerbose(@"Sending display marker: %@", displayedNode); + [self send:displayedNode]; + } - if(![self.connectionProperties.accountFeatures containsObject:@"urn:xmpp:mds:server-assist:0"]) - [self publishMDSMarkerForMessage:msg]; //always publish mds marker + //send mds if not already done by server using mds-assist + if(!assistedMDS) + [self publishMDSMarkerForMessage:lastUnreadMessage]; //always publish mds marker } -(void) removeFromServerWithCompletion:(void (^)(NSString* _Nullable error)) completion From cc0c6169a94e3e28b4f0a2fe9e95dabe3514745a Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Tue, 21 May 2024 01:12:12 +0200 Subject: [PATCH 095/143] Fix unread badge that got broken by MLContact singleton implementation --- Monal/Classes/MLContact.m | 75 ++++++++++++++++++++++----------------- 1 file changed, 42 insertions(+), 33 deletions(-) diff --git a/Monal/Classes/MLContact.m b/Monal/Classes/MLContact.m index a3314d02c8..e84cb20804 100644 --- a/Monal/Classes/MLContact.m +++ b/Monal/Classes/MLContact.m @@ -194,6 +194,40 @@ +(NSString*) ownDisplayNameForAccount:(xmpp*) account return nilDefault(displayName, @""); } ++(MLContact*) createContactFromDatabaseWithJid:(NSString*) jid andAccountNo:(NSNumber*) accountNo +{ + NSDictionary* contactDict = [[DataLayer sharedInstance] contactDictionaryForUsername:jid forAccount:accountNo]; + + // check if we know this contact and return a dummy one if not + if(contactDict == nil) + { + DDLogInfo(@"Returning dummy MLContact for %@ on accountNo %@", jid, accountNo); + return [self contactFromDictionary:@{ + @"buddy_name": jid.lowercaseString, + @"nick_name": @"", + @"full_name": @"", + @"subscription": kSubNone, + @"ask": @"", + @"account_id": accountNo, + //@"muc_subject": nil, + //@"muc_nick": nil, + @"Muc": @NO, + @"mentionOnly": @NO, + @"pinned": @NO, + @"blocked": @NO, + @"encrypt": @NO, + @"muted": @NO, + @"status": @"", + @"state": @"offline", + @"count": @0, + @"isActiveChat": @NO, + @"lastInteraction": nilWrapper(nil), + }]; + } + else + return [self contactFromDictionary:contactDict]; +} + +(MLContact*) createContactFromJid:(NSString*) jid andAccountNo:(NSNumber*) accountNo { MLAssert(jid != nil, @"jid must not be nil"); @@ -209,37 +243,7 @@ +(MLContact*) createContactFromJid:(NSString*) jid andAccountNo:(NSNumber*) acco [_singletonCache removeObjectForKey:cacheKey]; } - NSDictionary* contactDict = [[DataLayer sharedInstance] contactDictionaryForUsername:jid forAccount:accountNo]; - MLContact* retval = nil; - - // check if we know this contact and return a dummy one if not - if(contactDict == nil) - { - DDLogInfo(@"Returning dummy MLContact for %@ on accountNo %@", jid, accountNo); - retval = [self contactFromDictionary:@{ - @"buddy_name": jid.lowercaseString, - @"nick_name": @"", - @"full_name": @"", - @"subscription": kSubNone, - @"ask": @"", - @"account_id": accountNo, - //@"muc_subject": nil, - //@"muc_nick": nil, - @"Muc": @NO, - @"mentionOnly": @NO, - @"pinned": @NO, - @"blocked": @NO, - @"encrypt": @NO, - @"muted": @NO, - @"status": @"", - @"state": @"offline", - @"count": @0, - @"isActiveChat": @NO, - @"lastInteraction": nilWrapper(nil), - }]; - } - else - retval = [self contactFromDictionary:contactDict]; + MLContact* retval = [self createContactFromDatabaseWithJid:jid andAccountNo:accountNo]; _singletonCache[cacheKey] = [[WeakContainer alloc] initWithObj:retval]; return retval; @@ -253,7 +257,11 @@ -(instancetype) init [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleLastInteractionTimeUpdate:) name:kMonalLastInteractionUpdatedNotice object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleBlockListRefresh:) name:kMonalBlockListRefresh object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleContactRefresh:) name:kMonalContactRefresh object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleContactRefresh:) name:kMonalContactRemoved object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleMucSubjectChange:) name:kMonalMucSubjectChanged object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateUnreadCount) name:kMonalNewMessageNotice object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateUnreadCount) name:kMonalDeletedMessageNotice object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateUnreadCount) name:kMLMessageSentToContact object:nil]; return self; } @@ -294,7 +302,8 @@ -(void) handleContactRefresh:(NSNotification*) notification MLContact* contact = data[@"contact"]; if(![self.contactJid isEqualToString:contact.contactJid] || self.accountId.intValue != contact.accountId.intValue) return; // ignore other accounts or contacts - [self updateWithContact:contact]; + [self refresh]; + [self updateUnreadCount]; //only handle avatar updates if the property was already used and the old avatar is cached in this contact if(_avatar != nil) { @@ -319,7 +328,7 @@ -(void) handleMucSubjectChange:(NSNotification*) notification -(void) refresh { - [self updateWithContact:[MLContact createContactFromJid:self.contactJid andAccountNo:self.accountId]]; + [self updateWithContact:[[self class] createContactFromDatabaseWithJid:self.contactJid andAccountNo:self.accountId]]; } -(void) updateUnreadCount From d3f12fb44cd8399f6875bf6491ac2383c1e9e95c Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Tue, 21 May 2024 03:48:24 +0200 Subject: [PATCH 096/143] Rework settings to have more natural categories This resembles more or less what Conversations uses for its new categorized settings view --- Monal/Classes/ActiveChatsViewController.h | 1 + Monal/Classes/ActiveChatsViewController.m | 6 + Monal/Classes/BackgroundSettings.swift | 7 +- Monal/Classes/ContactDetails.swift | 2 +- Monal/Classes/GeneralSettings.swift | 381 ++++++++++++++++++ Monal/Classes/MLSettingsTableViewController.m | 28 +- Monal/Classes/MonalAppDelegate.m | 2 +- ...ings.swift => NotificationDebugging.swift} | 37 +- Monal/Classes/PrivacySettings.swift | 315 --------------- Monal/Classes/SwiftuiHelpers.swift | 40 +- Monal/Monal.xcodeproj/project.pbxproj | 16 +- 11 files changed, 435 insertions(+), 400 deletions(-) create mode 100644 Monal/Classes/GeneralSettings.swift rename Monal/Classes/{NotificationSettings.swift => NotificationDebugging.swift} (81%) delete mode 100644 Monal/Classes/PrivacySettings.swift diff --git a/Monal/Classes/ActiveChatsViewController.h b/Monal/Classes/ActiveChatsViewController.h index 0b3eb189e3..ef345377a9 100644 --- a/Monal/Classes/ActiveChatsViewController.h +++ b/Monal/Classes/ActiveChatsViewController.h @@ -39,6 +39,7 @@ NS_ASSUME_NONNULL_BEGIN -(void) deleteConversation; -(void) showSettings; -(void) showPrivacySettings; +-(void) showNotificationSettings; -(void) showDetails; -(void) showRegisterWithUsername:(NSString*) username onHost:(NSString*) host withToken:(NSString* _Nullable) token usingCompletion:(monal_id_block_t _Nullable) callback; -(void) showAddContactWithJid:(NSString*) jid preauthToken:(NSString* _Nullable) preauthToken prefillAccount:(xmpp* _Nullable) account andOmemoFingerprints:(NSDictionary* _Nullable) fingerprints; diff --git a/Monal/Classes/ActiveChatsViewController.m b/Monal/Classes/ActiveChatsViewController.m index 039a0ccb6f..38bcf37b88 100755 --- a/Monal/Classes/ActiveChatsViewController.m +++ b/Monal/Classes/ActiveChatsViewController.m @@ -508,6 +508,12 @@ -(void) openConversationPlaceholder:(MLContact*) contact } } +-(void) showNotificationSettings +{ + UIViewController* view = [[SwiftuiInterface new] makeViewWithName:@"ActiveChatsNotificatioSettings"]; + [self presentViewController:view animated:YES completion:^{}]; +} + -(void) showPrivacySettings { UIViewController* view = [[SwiftuiInterface new] makeViewWithName:@"ActiveChatsPrivacySettings"]; diff --git a/Monal/Classes/BackgroundSettings.swift b/Monal/Classes/BackgroundSettings.swift index 890d3a2af0..87d100ae9a 100644 --- a/Monal/Classes/BackgroundSettings.swift +++ b/Monal/Classes/BackgroundSettings.swift @@ -23,11 +23,9 @@ struct BackgroundSettings: View { @State private var showingImagePicker = false @State private var inputImage: UIImage? let contact: ObservableKVOWrapper? - let delegate: SheetDismisserProtocol - init(contact: ObservableKVOWrapper?, delegate: SheetDismisserProtocol) { + init(contact: ObservableKVOWrapper?) { self.contact = contact - self.delegate = delegate _inputImage = State(initialValue:MLImageManager.sharedInstance().getBackgroundFor(self.contact?.obj)) } @@ -129,8 +127,7 @@ struct BackgroundSettings: View { } struct BackgroundSettings_Previews: PreviewProvider { - static var delegate = SheetDismisserProtocol() static var previews: some View { - BackgroundSettings(contact:nil, delegate:delegate) + BackgroundSettings(contact:nil) } } diff --git a/Monal/Classes/ContactDetails.swift b/Monal/Classes/ContactDetails.swift index 96f7194437..ca42b7d17e 100644 --- a/Monal/Classes/ContactDetails.swift +++ b/Monal/Classes/ContactDetails.swift @@ -333,7 +333,7 @@ struct ContactDetails: View { } } - NavigationLink(destination: LazyClosureView(BackgroundSettings(contact:contact, delegate:delegate))) { + NavigationLink(destination: LazyClosureView(BackgroundSettings(contact:contact))) { Text("Change Chat Background") } diff --git a/Monal/Classes/GeneralSettings.swift b/Monal/Classes/GeneralSettings.swift new file mode 100644 index 0000000000..361b3e4ff4 --- /dev/null +++ b/Monal/Classes/GeneralSettings.swift @@ -0,0 +1,381 @@ +// +// GeneralSettings.swift +// Monal +// +// Created by Vaidik Dubey on 22/03/24. +// Copyright © 2024 monal-im.org. All rights reserved. +// + + +func getNotificationPrivacyOption(_ option: NotificationPrivacySettingOption) -> String { + switch option{ + case .DisplayNameAndMessage: + return NSLocalizedString("Display Name And Message", comment: "") + case .DisplayOnlyName: + return NSLocalizedString("Display Only Name", comment: "") + case .DisplayOnlyPlaceholder: + return NSLocalizedString("Display Only Placeholder", comment: "") + } +} + +class GeneralSettingsDefaultsDB: ObservableObject { + @defaultsDB("NotificationPrivacySetting") + var notificationPrivacySetting: Int + + @defaultsDB("OMEMODefaultOn") + var omemoDefaultOn:Bool + + @defaultsDB("AutodeleteAllMessagesAfter3Days") + var autodeleteAllMessagesAfter3Days: Bool + + @defaultsDB("SendLastUserInteraction") + var sendLastUserInteraction: Bool + + @defaultsDB("SendLastChatState") + var sendLastChatState: Bool + + @defaultsDB("SendReceivedMarkers") + var sendReceivedMarkers: Bool + + @defaultsDB("SendDisplayedMarkers") + var sendDisplayedMarkers: Bool + + @defaultsDB("ShowGeoLocation") + var showGeoLocation: Bool + + @defaultsDB("ShowURLPreview") + var showURLPreview: Bool + + @defaultsDB("webrtcAllowP2P") + var webrtcAllowP2P: Bool + + @defaultsDB("webrtcUseFallbackTurn") + var webrtcUseFallbackTurn: Bool + + @defaultsDB("allowVersionIQ") + var allowVersionIQ: Bool + + @defaultsDB("allowNonRosterContacts") + var allowNonRosterContacts: Bool + + @defaultsDB("allowCallsFromNonRosterContacts") + var allowCallsFromNonRosterContacts: Bool + + @defaultsDB("HasSeenPrivacySettings") + var hasSeenPrivacySettings: Bool + + @defaultsDB("AutodownloadFiletransfers") + var autodownloadFiletransfers : Bool + + @defaultsDB("AutodownloadFiletransfersWifiMaxSize") + var autodownloadFiletransfersWifiMaxSize : UInt + + @defaultsDB("AutodownloadFiletransfersMobileMaxSize") + var autodownloadFiletransfersMobileMaxSize : UInt + + @defaultsDB("ImageUploadQuality") + var imageUploadQuality : Float + + @defaultsDB("showKeyboardOnChatOpen") + var showKeyboardOnChatOpen: Bool + + @defaultsDB("useDnssecForAllConnections") + var useDnssecForAllConnections: Bool +} + + +struct GeneralSettings: View { + @ObservedObject var generalSettingsDefaultsDB = GeneralSettingsDefaultsDB() + + var body: some View { + Form { + Section(header:Text("General Settings")) { + NavigationLink(destination: LazyClosureView(UserInterfaceSettings())) { + HStack{ + Image(systemName: "hand.tap.fill") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 20, height: 20) + Text("User Interface") + } + } + NavigationLink(destination: LazyClosureView(SecuritySettings())) { + HStack{ + Image(systemName: "shield.checkerboard") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 20, height: 20) + Text("Security") + } + } + NavigationLink(destination: LazyClosureView(PrivacySettings())) { + HStack{ + Image(systemName: "eye") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 20, height: 20) + Text("Privacy") + } + } + NavigationLink(destination: LazyClosureView(NotificationSettings())) { + HStack{ + Image(systemName: "text.bubble") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 20, height: 20) + Text("Notifications") + } + } + NavigationLink(destination: LazyClosureView(AttachmentSettings())) { + HStack{ + Image(systemName: "paperclip") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 20, height: 20) + Text("Attachments") + } + } + } + } + .navigationBarTitle("General Settings") + .onAppear { + generalSettingsDefaultsDB.hasSeenPrivacySettings = true + } + } +} + +struct UserInterfaceSettings: View { + @ObservedObject var generalSettingsDefaultsDB = GeneralSettingsDefaultsDB() + + var body: some View { + Form { + Section(header: Text("Previews")) { + Toggle(isOn: $generalSettingsDefaultsDB.showGeoLocation) { + Text("Show inline geo location").font(.body) + Text("Received geo locations are shared with Apple's Maps App.").font(.footnote) + } + Toggle(isOn: $generalSettingsDefaultsDB.showURLPreview) { + Text("Show URL previews").font(.body) + Text("The operator of the webserver providing that URL may see your IP address.").font(.footnote) + } + } + + Section(header: Text("Input")) { + Toggle(isOn: $generalSettingsDefaultsDB.showKeyboardOnChatOpen) { + Text("Autofocus text input on chat open").font(.body) + Text("Will focus the textfield on macOS or iOS with hardware keyboard attached, will open the software keyboard otherwise.").font(.footnote) + } + } + + Section(header: Text("Appearance")) { + NavigationLink(destination: LazyClosureView(BackgroundSettings(contact:nil))) { + Text("Chat background image").font(.body) + Text("Configure the background image displayed in open chats.").font(.footnote) + } + } + } + .navigationBarTitle("User Interface", displayMode: .inline) + } +} + +struct SecuritySettings: View { + @ObservedObject var generalSettingsDefaultsDB = GeneralSettingsDefaultsDB() + + var body: some View { + Form { + Section(header: Text("Encryption")) { + Toggle(isOn: $generalSettingsDefaultsDB.omemoDefaultOn) { + Text("Enable encryption by default for new chats").font(.body) + Text("Every new contact will have encryption enabled, but already known contacts will preserve their encryption settings.").font(.footnote) + } + + if #available(iOS 16.0, macCatalyst 16.0, *) { + Toggle(isOn: $generalSettingsDefaultsDB.useDnssecForAllConnections) { + Text("Use DNSSEC validation for all connections").font(.body) + Text( +""" +Use DNSSEC to validate all DNS query responses before connecting to the IP address designated \ +in the DNS response.\n\ +While being more secure, this can lead to connection problems in certain networks \ +like hotel wifi, ugly mobile carriers etc. +""" + ).font(.footnote) + } + } + + Toggle(isOn: $generalSettingsDefaultsDB.webrtcAllowP2P) { + Text("Calls: Allow P2P sessions").font(.body) + Text("Allow your phone to establish a direct network connection to the remote party. This might leak your IP address to the caller/callee.").font(.footnote) + } + } + + Section(header: Text("On this device")) { + Toggle(isOn: $generalSettingsDefaultsDB.autodeleteAllMessagesAfter3Days) { + Text("Autodelete all messages after 3 days") + } + } + } + .navigationBarTitle("Security", displayMode: .inline) + } +} + +struct PrivacySettings: View { + @ObservedObject var generalSettingsDefaultsDB = GeneralSettingsDefaultsDB() + + var body: some View { + Form { + Section(header: Text("Activity indications")) { + Toggle(isOn: $generalSettingsDefaultsDB.sendReceivedMarkers) { + Text("Send message received").font(.body) + Text("Let your contacts know if you received a message.").font(.footnote) + } + Toggle(isOn: $generalSettingsDefaultsDB.sendDisplayedMarkers) { + Text("Send message displayed state").font(.body) + Text("Let your contacts know if you read a message.").font(.footnote) + } + Toggle(isOn: $generalSettingsDefaultsDB.sendLastChatState) { + Text("Send typing notifications").font(.body) + Text("Let your contacts know if you are typing a message.").font(.footnote) + } + Toggle(isOn: $generalSettingsDefaultsDB.sendLastUserInteraction) { + Text("Send last interaction time").font(.body) + Text("Let your contacts know when you last opened the app.").font(.footnote) + } + } + + Section(header: Text("Interactions")) { + Toggle(isOn: $generalSettingsDefaultsDB.allowNonRosterContacts) { + Text("Accept incoming messages from strangers").font(.body) + Text("Allow contacts not in your contact list to contact you.").font(.footnote) + } + Toggle(isOn: $generalSettingsDefaultsDB.allowCallsFromNonRosterContacts) { + Text("Accept incoming calls from strangers").font(.body) + Text("Allow contacts not in your contact list to call you.").font(.footnote) + }.disabled(!generalSettingsDefaultsDB.allowNonRosterContacts) + } + + Section(header: Text("Misc")) { + Toggle(isOn: $generalSettingsDefaultsDB.allowVersionIQ) { + Text("Publish version").font(.body) + Text("Allow contacts in your contact list to query your Monal and iOS versions.").font(.footnote) + } + Toggle(isOn: $generalSettingsDefaultsDB.webrtcUseFallbackTurn) { + Text("Calls: Allow TURN fallback to Monal-Servers").font(.body) + Text("This will make calls possible even if your XMPP server does not provide a TURN server.").font(.footnote) + } + } + } + .navigationBarTitle("Privacy", displayMode: .inline) + } +} + +struct NotificationSettings: View { + @ObservedObject var generalSettingsDefaultsDB = GeneralSettingsDefaultsDB() + @State private var pushPermissionEnabled = false + + private var pushNotEnabled: Bool { + let xmppAccountInfo = MLXMPPManager.sharedInstance().connectedXMPP as! [xmpp] + var pushNotEnabled = false + for account in xmppAccountInfo { + pushNotEnabled = pushNotEnabled || !account.connectionProperties.pushEnabled + } + return pushNotEnabled + } + + var body: some View { + Form { + Section(header: Text("Settings")) { + Picker(selection: $generalSettingsDefaultsDB.notificationPrivacySetting, label: Text("Notification privacy")) { + ForEach(NotificationPrivacySettingOption.allCases, id: \.self) { option in + Text(getNotificationPrivacyOption(option)).tag(option.rawValue) + } + } + .frame(height: 56, alignment: .trailing) + } + + Section(header: Text("Debugging")) { + NavigationLink(destination: LazyClosureView(NotificationDebugging())) { + buildNotificationStateLabel(Text("Debug Notification Problems"), isWorking: !self.pushNotEnabled && self.pushPermissionEnabled) + } + } + } + .onAppear { + UNUserNotificationCenter.current().getNotificationSettings { (settings) -> Void in + self.pushPermissionEnabled = (settings.authorizationStatus == .authorized || settings.authorizationStatus == .provisional); + } + } + .navigationBarTitle("Notifications", displayMode: .inline) + } +} + +struct AttachmentSettings: View { + @ObservedObject var generalSettingsDefaultsDB = GeneralSettingsDefaultsDB() + + var body: some View { + Form { + Section(header: Text("General File Transfer Settings")) { + Toggle(isOn: $generalSettingsDefaultsDB.autodownloadFiletransfers) { + Text("Auto-Download Media") + } + } + + Section(header: Text("Download Settings")) { + Text("Adjust the maximum file size for auto-downloads over WiFi") + .foregroundColor(.secondary) + .font(.footnote) + Slider( + value: $generalSettingsDefaultsDB.autodownloadFiletransfersWifiMaxSize.bytecount(mappedTo: 1024*1024), + in: 1.0...100.0, + step: 1.0, + minimumValueLabel: Text("1 MiB"), + maximumValueLabel: Text("100 MiB"), + label: { + Text("Load over wifi") + } + ) + Text("Load over WiFi up to: \(UInt(generalSettingsDefaultsDB.autodownloadFiletransfersWifiMaxSize/(1024*1024))) MiB") + } + + Section { + Text("Adjust the maximum file size for auto-downloads over cellular network") + .foregroundColor(.secondary) + .font(.footnote) + Slider( + value: $generalSettingsDefaultsDB.autodownloadFiletransfersMobileMaxSize.bytecount(mappedTo: 1024*1024), + in: 0.0...100.0, + step: 1.0, + minimumValueLabel: Text("1 MiB"), + maximumValueLabel: Text("100 MiB"), + label: { + Text("Load over Cellular") + } + ) + Text("Load over cellular up to: \(Int(generalSettingsDefaultsDB.autodownloadFiletransfersMobileMaxSize/(1024*1024))) MiB") + } + + Section(header: Text("Upload Settings")) { + Text("Adjust the quality of images uploaded") + .foregroundColor(.secondary) + .font(.footnote) + Slider( + value: $generalSettingsDefaultsDB.imageUploadQuality, + in: 0.33...1.0, + step: 0.01, + minimumValueLabel: Text("33%"), + maximumValueLabel: Text("100%"), + label: { + Text("Upload Settings") + } + ) + Text("Image Upload Quality: \(String(format: "%.0f%%", generalSettingsDefaultsDB.imageUploadQuality*100))") + } + } + } +} + + +struct ContentView_Previews: PreviewProvider { + static var previews: some View { + PrivacySettings() + } +} diff --git a/Monal/Classes/MLSettingsTableViewController.m b/Monal/Classes/MLSettingsTableViewController.m index f67b0e4b80..64d5b11c67 100644 --- a/Monal/Classes/MLSettingsTableViewController.m +++ b/Monal/Classes/MLSettingsTableViewController.m @@ -32,9 +32,7 @@ }; enum SettingsAppRows { - PrivacySettingsRow, - NotificationsRow, - BackgroundsRow, + GeneralSettingsRow, SoundsRow, SettingsAppRowsCnt }; @@ -204,14 +202,8 @@ -(UITableViewCell*) tableView:(UITableView*) tableView cellForRowAtIndexPath:(NS } case kSettingSectionApp: { switch(indexPath.row) { - case PrivacySettingsRow: - [cell initTapCell:NSLocalizedString(@"Privacy Settings", @"")]; - break; - case NotificationsRow: - [cell initTapCell:NSLocalizedString(@"Notifications", @"")]; - break; - case BackgroundsRow: - [cell initTapCell:NSLocalizedString(@"Backgrounds", @"")]; + case GeneralSettingsRow: + [cell initTapCell:NSLocalizedString(@"General Settings", @"")]; break; case SoundsRow: [cell initTapCell:NSLocalizedString(@"Sounds", @"")]; @@ -322,21 +314,11 @@ -(void)tableView:(UITableView*) tableView didSelectRowAtIndexPath:(NSIndexPath*) case kSettingSectionApp: { switch(indexPath.row) { - case PrivacySettingsRow: { - UIViewController* privacyViewController = [[SwiftuiInterface new] makeViewWithName:@"PrivacySettings"]; + case GeneralSettingsRow: { + UIViewController* privacyViewController = [[SwiftuiInterface new] makeViewWithName:@"GeneralSettings"]; [self showDetailViewController:privacyViewController sender:self]; break; } - case NotificationsRow: { - UIViewController* notificationSettingsController = [[SwiftuiInterface new] makeViewWithName:@"NotificationSettings"]; - [self showDetailViewController:notificationSettingsController sender:self]; - break; - } - case BackgroundsRow: { - UIViewController* backgroundSettingsController = [[SwiftuiInterface new] makeBackgroundSettings:nil]; - [self showDetailViewController:backgroundSettingsController sender:self]; - break; - } case SoundsRow: [self performSegueWithIdentifier:@"showSounds" sender:self]; break; diff --git a/Monal/Classes/MonalAppDelegate.m b/Monal/Classes/MonalAppDelegate.m index 18e8c81a3e..81f3f6fbee 100644 --- a/Monal/Classes/MonalAppDelegate.m +++ b/Monal/Classes/MonalAppDelegate.m @@ -1016,7 +1016,7 @@ -(void) userNotificationCenter:(UNUserNotificationCenter*) center openSettingsFo while(self.activeChats == nil) usleep(100000); dispatch_async(dispatch_get_main_queue(), ^{ - [(ActiveChatsViewController*)self.activeChats showPrivacySettings]; + [(ActiveChatsViewController*)self.activeChats showNotificationSettings]; }); }); } diff --git a/Monal/Classes/NotificationSettings.swift b/Monal/Classes/NotificationDebugging.swift similarity index 81% rename from Monal/Classes/NotificationSettings.swift rename to Monal/Classes/NotificationDebugging.swift index 8e37883ebb..859a7506ba 100644 --- a/Monal/Classes/NotificationSettings.swift +++ b/Monal/Classes/NotificationDebugging.swift @@ -1,5 +1,5 @@ // -// NotificationSettings.swift +// NotificationDebugging.swift // Monal // // Created by Jan on 02.05.22. @@ -8,28 +8,7 @@ import OrderedCollections -struct NotificationSettings: View { - @ViewBuilder - func buildLabel(_ description: Text, isWorking: Bool) -> some View { - if(isWorking == true) { - Label(title: { - description - }, icon: { - Image(systemName: "checkmark.seal") - .foregroundColor(.green) - }) - } else { - Label(title: { - description - }, icon: { - Image(systemName: "xmark.seal") - .foregroundColor(.red) - }) - } - } - - var delegate: SheetDismisserProtocol - +struct NotificationDebugging: View { private let applePushEnabled: Bool private let applePushToken: String private let xmppAccountInfo: [xmpp] @@ -46,7 +25,7 @@ struct NotificationSettings: View { Group { Section(header: Text("Status").font(.title3)) { VStack(alignment: .leading) { - buildLabel(Text("Apple Push Service"), isWorking: self.applePushEnabled); + buildNotificationStateLabel(Text("Apple Push Service"), isWorking: self.applePushEnabled); Divider() Text("Apple push service should always be on. If it is off, your device can not talk to Apple's server.").font(.footnote) if !self.applePushEnabled, let apnsError = MLXMPPManager.sharedInstance().apnsError { @@ -70,7 +49,7 @@ struct NotificationSettings: View { } Section { VStack(alignment: .leading) { - buildLabel(Text("Can Show Notifications"), isWorking: self.pushPermissionEnabled); + buildNotificationStateLabel(Text("Can Show Notifications"), isWorking: self.pushPermissionEnabled); Divider() Text("If Monal can't show notifications, you will not see alerts when a message arrives. This happens if you tapped 'Decline' when Monal first asked permission. Fix it by going to iOS Settings -> Monal -> Notifications and select 'Allow Notifications'.").font(.footnote) } @@ -79,7 +58,7 @@ struct NotificationSettings: View { Section { VStack(alignment: .leading) { ForEach(self.xmppAccountInfo, id: \.self) { account in - buildLabel(Text(account.connectionProperties.identity.jid), isWorking: account.connectionProperties.pushEnabled) + buildNotificationStateLabel(Text(account.connectionProperties.identity.jid), isWorking: account.connectionProperties.pushEnabled) Divider() } Text("If this is off your device could not activate push on your xmpp server, make sure to have configured it to support XEP-0357.").font(.footnote) @@ -123,11 +102,10 @@ struct NotificationSettings: View { }); } - init(delegate: SheetDismisserProtocol) { + init() { self.applePushEnabled = MLXMPPManager.sharedInstance().hasAPNSToken; self.applePushToken = MLXMPPManager.sharedInstance().pushToken; self.xmppAccountInfo = MLXMPPManager.sharedInstance().connectedXMPP as! [xmpp] - self.delegate = delegate // push server selector self.availablePushServers = HelperTools.getAvailablePushServers() @@ -136,8 +114,7 @@ struct NotificationSettings: View { } struct PushSettings_Previews: PreviewProvider { - static var delegate = SheetDismisserProtocol() static var previews: some View { - NotificationSettings(delegate:delegate) + NotificationSettings() } } diff --git a/Monal/Classes/PrivacySettings.swift b/Monal/Classes/PrivacySettings.swift deleted file mode 100644 index 4d601c1d5b..0000000000 --- a/Monal/Classes/PrivacySettings.swift +++ /dev/null @@ -1,315 +0,0 @@ -// -// PrivacySettings.swift -// Monal -// -// Created by Vaidik Dubey on 22/03/24. -// Copyright © 2024 monal-im.org. All rights reserved. -// - - -func getNotificationPrivacyOption(_ option: NotificationPrivacySettingOption) -> String { - switch option{ - case .DisplayNameAndMessage: - return NSLocalizedString("Display Name And Message", comment: "") - case .DisplayOnlyName: - return NSLocalizedString("Display Only Name", comment: "") - case .DisplayOnlyPlaceholder: - return NSLocalizedString("Display Only Placeholder", comment: "") - } -} - -class PrivacyDefaultDB: ObservableObject { - @defaultsDB("NotificationPrivacySetting") - var notificationPrivacySetting: Int - - @defaultsDB("OMEMODefaultOn") - var omemoDefaultOn:Bool - - @defaultsDB("AutodeleteAllMessagesAfter3Days") - var autodeleteAllMessagesAfter3Days: Bool - - @defaultsDB("SendLastUserInteraction") - var sendLastUserInteraction: Bool - - @defaultsDB("SendLastChatState") - var sendLastChatState: Bool - - @defaultsDB("SendReceivedMarkers") - var sendReceivedMarkers: Bool - - @defaultsDB("SendDisplayedMarkers") - var sendDisplayedMarkers: Bool - - @defaultsDB("ShowGeoLocation") - var showGeoLocation: Bool - - @defaultsDB("ShowURLPreview") - var showURLPreview: Bool - - @defaultsDB("webrtcAllowP2P") - var webrtcAllowP2P: Bool - - @defaultsDB("webrtcUseFallbackTurn") - var webrtcUseFallbackTurn: Bool - - @defaultsDB("allowVersionIQ") - var allowVersionIQ: Bool - - @defaultsDB("allowNonRosterContacts") - var allowNonRosterContacts: Bool - - @defaultsDB("allowCallsFromNonRosterContacts") - var allowCallsFromNonRosterContacts: Bool - - @defaultsDB("HasSeenPrivacySettings") - var hasSeenPrivacySettings: Bool - - @defaultsDB("AutodownloadFiletransfers") - var autodownloadFiletransfers : Bool - - @defaultsDB("AutodownloadFiletransfersWifiMaxSize") - var autodownloadFiletransfersWifiMaxSize : UInt - - @defaultsDB("AutodownloadFiletransfersMobileMaxSize") - var autodownloadFiletransfersMobileMaxSize : UInt - - @defaultsDB("ImageUploadQuality") - var imageUploadQuality : Float - - @defaultsDB("showKeyboardOnChatOpen") - var showKeyboardOnChatOpen: Bool - - @defaultsDB("useDnssecForAllConnections") - var useDnssecForAllConnections: Bool -} - - -struct PrivacySettings: View { - @ObservedObject var privacyDefaultDB = PrivacyDefaultDB() - - var body: some View { - Form { - Section(header:Text("Privacy and security settings")) { - NavigationLink(destination: PrivacyScreen()) { - HStack{ - Image(systemName: "lock.shield") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 20, height: 20) - Text("Privacy & Security") - } - } - NavigationLink(destination: PublishingScreen()) { - HStack{ - Image(systemName: "eye") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 20, height: 20) - Text("Publishing & Appearance") - } - } - NavigationLink(destination: PreviewsScreen()) { - HStack{ - Image(systemName: "doc.text.magnifyingglass") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 20, height: 20) - Text("Previews") - } - } - NavigationLink(destination: CommunicationScreen()) { - HStack{ - Image(systemName: "bubble.left.and.bubble.right") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 20, height: 20) - Text("Communication") - } - } - - NavigationLink(destination: MLAutoDownloadFiletransferSettingView()) { - HStack{ - Image(systemName: "square.and.arrow.down") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 20, height: 20) - Text("Media Upload & Download") - } - } - } - } - .navigationBarTitle("Privacy Settings") - .onAppear { - privacyDefaultDB.hasSeenPrivacySettings = true - } - } -} - -struct PrivacyScreen: View { - @ObservedObject var privacyDefaultDB = PrivacyDefaultDB() - - var body: some View { - Form { - Picker(selection: $privacyDefaultDB.notificationPrivacySetting, label: Text("Notification privacy")) { - ForEach(NotificationPrivacySettingOption.allCases, id: \.self) { option in - Text(getNotificationPrivacyOption(option)).tag(option.rawValue) - } - } - .frame(height: 56, alignment: .trailing) - Toggle(isOn: $privacyDefaultDB.omemoDefaultOn) { - Text("Enable encryption by default for new chats") - } - Toggle(isOn: $privacyDefaultDB.autodeleteAllMessagesAfter3Days) { - Text("Autodelete all messages after 3 days") - } - if #available(iOS 16.0, macCatalyst 16.0, *) { - Text("Use DNSSEC to validate all DNS query responses before connecting to the IP designated in the DNS response. While being more secure, this can lead to connection problems in certain networks like Hotel WiFi etc.") - Toggle(isOn: $privacyDefaultDB.useDnssecForAllConnections) { - Text("Use DNSSEC validation for all connections") - } - } - } - .navigationBarTitle("Privacy & security", displayMode: .inline) - } -} - -struct PublishingScreen: View { - @ObservedObject var privacyDefaultDB = PrivacyDefaultDB() - - var body: some View { - Form { - Section(header: Text("Publishing")) { - Toggle(isOn: $privacyDefaultDB.sendLastUserInteraction) { - Text("Send last interaction time") - } - Toggle(isOn: $privacyDefaultDB.sendLastChatState) { - Text("Send typing notifications") - } - Toggle(isOn: $privacyDefaultDB.sendReceivedMarkers) { - Text("Send message received state") - } - Toggle(isOn: $privacyDefaultDB.sendDisplayedMarkers) { - Text("Send message displayed state") - } - } - Section(header: Text("Appearance")) { - Toggle(isOn: $privacyDefaultDB.showKeyboardOnChatOpen) { - Text("Autofocus text input on chat open") - } - } - } - .navigationBarTitle("Publishing & Appearance", displayMode: .inline) - } -} - -struct PreviewsScreen: View { - @ObservedObject var privacyDefaultDB = PrivacyDefaultDB() - - var body: some View { - Form { - Toggle(isOn: $privacyDefaultDB.showGeoLocation) { - Text("Show inline geo location") - } - Toggle(isOn: $privacyDefaultDB.showURLPreview) { - Text("Show URL previews") - } - } - .navigationBarTitle("Previews", displayMode: .inline) - } -} - -struct CommunicationScreen: View { - @ObservedObject var privacyDefaultDB = PrivacyDefaultDB() - - var body: some View { - Form { - Toggle(isOn: $privacyDefaultDB.allowNonRosterContacts) { - Text("Allow contacts not in my contact list to contact me") - } - Toggle(isOn: $privacyDefaultDB.allowVersionIQ) { - Text("Allow approved contacts to query my Monal and iOS version") - } - Toggle(isOn: $privacyDefaultDB.allowCallsFromNonRosterContacts) { - Text("Calls: Allow contacts not in my contact list to call me") - } - Toggle(isOn: $privacyDefaultDB.webrtcAllowP2P) { - Text("Calls: Allow P2P sessions") - } - Toggle(isOn: $privacyDefaultDB.webrtcUseFallbackTurn) { - Text("Calls: Allow TURN fallback to Monal-Servers") - } - } - .navigationBarTitle("Communication", displayMode: .inline) - } -} - -struct MLAutoDownloadFiletransferSettingView: View { - @ObservedObject var privacyDefaultDB = PrivacyDefaultDB() - - var body: some View { - Form { - Section(header: Text("General File Transfer Settings")) { - Toggle(isOn: $privacyDefaultDB.autodownloadFiletransfers) { - Text("Auto-Download Media") - } - } - - Section(header: Text("Download Settings")) { - - Text("Adjust the maximum file size for auto-downloads over WiFi") - .foregroundColor(.secondary) - .font(.footnote) - Slider( - value: $privacyDefaultDB.autodownloadFiletransfersWifiMaxSize.bytecount(mappedTo: 1024*1024), - in: 1.0...100.0, - step: 1.0, - minimumValueLabel: Text("1 MiB"), - maximumValueLabel: Text("100 MiB"), - label: { - Text("Load over wifi") - } - ) - Text("Load over WiFi up to: \(UInt(privacyDefaultDB.autodownloadFiletransfersWifiMaxSize/(1024*1024))) MiB") - } - - Text("Adjust the maximum file size for auto-downloads over cellular network") - .foregroundColor(.secondary) - .font(.footnote) - Slider( - value: $privacyDefaultDB.autodownloadFiletransfersMobileMaxSize.bytecount(mappedTo: 1024*1024), - in: 0.0...100.0, - step: 1.0, - minimumValueLabel: Text("1 MiB"), - maximumValueLabel: Text("100 MiB"), - label: { - Text("Load over Cellular") - } - ) - Text("Load over cellular up to: \(Int(privacyDefaultDB.autodownloadFiletransfersMobileMaxSize/(1024*1024))) MiB") - - Section(header: Text("Upload Settings")) { - Text("Adjust the quality of images uploaded") - .foregroundColor(.secondary) - .font(.footnote) - Slider( - value: $privacyDefaultDB.imageUploadQuality, - in: 0.33...1.0, - step: 0.01, - minimumValueLabel: Text("33%"), - maximumValueLabel: Text("100%"), - label: { - Text("Upload Settings") - } - ) - Text("Image Upload Quality: \(String(format: "%.0f%%", privacyDefaultDB.imageUploadQuality*100))") - } - } - } -} - - -struct ContentView_Previews: PreviewProvider { - static var previews: some View { - PrivacySettings() - } -} diff --git a/Monal/Classes/SwiftuiHelpers.swift b/Monal/Classes/SwiftuiHelpers.swift index 4596d2f35a..afec677650 100644 --- a/Monal/Classes/SwiftuiHelpers.swift +++ b/Monal/Classes/SwiftuiHelpers.swift @@ -109,6 +109,25 @@ func mucAffiliationToString(_ affiliation: String?) -> String { return NSLocalizedString("", comment:"muc affiliation") } +@ViewBuilder +func buildNotificationStateLabel(_ description: Text, isWorking: Bool) -> some View { + if(isWorking == true) { + Label(title: { + description + }, icon: { + Image(systemName: "checkmark.seal") + .foregroundColor(.green) + }) + } else { + Label(title: { + description + }, icon: { + Image(systemName: "xmark.seal") + .foregroundColor(.red) + }) + } +} + //see here for some ideas used herein: https://blog.logrocket.com/adding-gifs-ios-app-flanimatedimage-swiftui/#using-flanimatedimage-with-swift struct GIFViewer: UIViewRepresentable { typealias UIViewType = FLAnimatedImageView @@ -483,19 +502,6 @@ class SwiftuiInterface : NSObject { return host } - @objc - func makeBackgroundSettings(_ contact: MLContact?) -> UIViewController { - let delegate = SheetDismisserProtocol() - let host = UIHostingController(rootView:AnyView(EmptyView())) - delegate.host = host - var contactArg:ObservableKVOWrapper? = nil; - if let contact = contact { - contactArg = ObservableKVOWrapper(contact) - } - host.rootView = AnyView(UIKitWorkaround(BackgroundSettings(contact:contactArg, delegate:delegate))) - return host - } - @objc func makeAddContactView(dismisser: @escaping (MLContact) -> ()) -> UIViewController { let delegate = SheetDismisserProtocol() @@ -520,8 +526,6 @@ class SwiftuiInterface : NSObject { let host = UIHostingController(rootView:AnyView(EmptyView())) delegate.host = host switch(name) { // TODO names are currently taken from the segue identifier, an enum would be nice once everything is ported to SwiftUI - case "NotificationSettings": - host.rootView = AnyView(UIKitWorkaround(NotificationSettings(delegate:delegate))) case "DebugView": host.rootView = AnyView(UIKitWorkaround(DebugView())) case "WelcomeLogIn": @@ -534,10 +538,12 @@ class SwiftuiInterface : NSObject { host.rootView = AnyView(AddTopLevelNavigation(withDelegate: delegate, to: CreateGroupMenu(delegate: delegate))) case "ChatPlaceholder": host.rootView = AnyView(ChatPlaceholder()) - case "PrivacySettings" : - host.rootView = AnyView(UIKitWorkaround(PrivacySettings())) + case "GeneralSettings" : + host.rootView = AnyView(UIKitWorkaround(GeneralSettings())) case "ActiveChatsPrivacySettings": host.rootView = AnyView(AddTopLevelNavigation(withDelegate: delegate, to: PrivacySettings())) + case "ActiveChatsNotificatioSettings": + host.rootView = AnyView(AddTopLevelNavigation(withDelegate: delegate, to: NotificationSettings())) default: unreachable() } diff --git a/Monal/Monal.xcodeproj/project.pbxproj b/Monal/Monal.xcodeproj/project.pbxproj index de395b13ad..50f3c39688 100644 --- a/Monal/Monal.xcodeproj/project.pbxproj +++ b/Monal/Monal.xcodeproj/project.pbxproj @@ -10,7 +10,7 @@ 08CAF17FA202CF3CB760D93C /* Pods_NotificationService.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2B7A5555D807EE78C95217FD /* Pods_NotificationService.framework */; }; 1D3623260D0F684500981E51 /* MonalAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 1D3623250D0F684500981E51 /* MonalAppDelegate.m */; }; 1D60589B0D05DD56006BFB54 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 29B97316FDCFA39411CA2CEA /* main.m */; }; - 20ED55852BADDA5C0005783E /* PrivacySettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20ED55842BADDA5C0005783E /* PrivacySettings.swift */; }; + 20ED55852BADDA5C0005783E /* GeneralSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20ED55842BADDA5C0005783E /* GeneralSettings.swift */; }; 2601D9CB0FBF25EF004DB939 /* sworim.sqlite in Resources */ = {isa = PBXBuildFile; fileRef = 2601D9CA0FBF25EF004DB939 /* sworim.sqlite */; }; 260773C4232FC4E800BFD50F /* NotificationService.m in Sources */ = {isa = PBXBuildFile; fileRef = 260773C3232FC4E800BFD50F /* NotificationService.m */; }; 2609B5291FD5B26800F09FA1 /* MLSplitViewDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 2609B5281FD5B26800F09FA1 /* MLSplitViewDelegate.m */; }; @@ -83,7 +83,7 @@ 38720923251EDE07001837EB /* MLXEPSlashMeHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 38720921251EDE07001837EB /* MLXEPSlashMeHandler.m */; }; 389E298C25E901CA009A5268 /* MLAudioRecoderManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 389E298925E901CA009A5268 /* MLAudioRecoderManager.m */; }; 389E298D25E901CA009A5268 /* MLAudioRecoderManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 389E298B25E901CA009A5268 /* MLAudioRecoderManager.h */; }; - 3D06A515281FFCC000DDAE90 /* NotificationSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D06A514281FFCC000DDAE90 /* NotificationSettings.swift */; }; + 3D06A515281FFCC000DDAE90 /* NotificationDebugging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D06A514281FFCC000DDAE90 /* NotificationDebugging.swift */; }; 3D27D956290B0BB60014748B /* AddContactMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D27D955290B0BB60014748B /* AddContactMenu.swift */; }; 3D27D958290B0BC80014748B /* ContactRequestsMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D27D957290B0BC80014748B /* ContactRequestsMenu.swift */; }; 3D5A91422842B4AE008CE57E /* MemberList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D5A91412842B4AE008CE57E /* MemberList.swift */; }; @@ -303,7 +303,7 @@ 1D3623240D0F684500981E51 /* MonalAppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = MonalAppDelegate.h; path = Classes/MonalAppDelegate.h; sourceTree = ""; }; 1D3623250D0F684500981E51 /* MonalAppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = MonalAppDelegate.m; path = Classes/MonalAppDelegate.m; sourceTree = ""; }; 1D46F251C198E3D8FA55692F /* Pods-Monal.appstore.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Monal.appstore.xcconfig"; path = "Target Support Files/Pods-Monal/Pods-Monal.appstore.xcconfig"; sourceTree = ""; }; - 20ED55842BADDA5C0005783E /* PrivacySettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacySettings.swift; sourceTree = ""; }; + 20ED55842BADDA5C0005783E /* GeneralSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralSettings.swift; sourceTree = ""; }; 213F5BFD4599EC9317B99E97 /* Pods-Monal.appstore-quicksy.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Monal.appstore-quicksy.xcconfig"; path = "Target Support Files/Pods-Monal/Pods-Monal.appstore-quicksy.xcconfig"; sourceTree = ""; }; 21E99538324C14220843F325 /* Pods-shareSheet.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-shareSheet.debug.xcconfig"; path = "Target Support Files/Pods-shareSheet/Pods-shareSheet.debug.xcconfig"; sourceTree = ""; }; 222F09C97CFF93A2CF1007F3 /* Pods-MonalUITests.alpha-ios.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MonalUITests.alpha-ios.xcconfig"; path = "Target Support Files/Pods-MonalUITests/Pods-MonalUITests.alpha-ios.xcconfig"; sourceTree = ""; }; @@ -499,7 +499,7 @@ 389E298B25E901CA009A5268 /* MLAudioRecoderManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MLAudioRecoderManager.h; sourceTree = ""; }; 39B989B9775C0725A810D271 /* Pods-MonalUITests.adhoc.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MonalUITests.adhoc.xcconfig"; path = "Target Support Files/Pods-MonalUITests/Pods-MonalUITests.adhoc.xcconfig"; sourceTree = ""; }; 39DB4C9159DA578D1A34990D /* Pods-monalxmpp.alpha.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-monalxmpp.alpha.xcconfig"; path = "Target Support Files/Pods-monalxmpp/Pods-monalxmpp.alpha.xcconfig"; sourceTree = ""; }; - 3D06A514281FFCC000DDAE90 /* NotificationSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettings.swift; sourceTree = ""; }; + 3D06A514281FFCC000DDAE90 /* NotificationDebugging.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationDebugging.swift; sourceTree = ""; }; 3D27D955290B0BB60014748B /* AddContactMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContactMenu.swift; sourceTree = ""; }; 3D27D957290B0BC80014748B /* ContactRequestsMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactRequestsMenu.swift; sourceTree = ""; }; 3D5A91412842B4AE008CE57E /* MemberList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberList.swift; sourceTree = ""; }; @@ -996,11 +996,11 @@ 2644D4981FF29E5600F46AB5 /* MLSettingsTableViewController.m */, 26B0CA8721AE2E3C0080B133 /* MLSoundsTableViewController.h */, 26B0CA8821AE2E3C0080B133 /* MLSoundsTableViewController.m */, - 3D06A514281FFCC000DDAE90 /* NotificationSettings.swift */, + 3D06A514281FFCC000DDAE90 /* NotificationDebugging.swift */, E8CF9CBF26249640001A1952 /* MLSettingsAboutViewController.h */, E8CF9CC026249640001A1952 /* MLSettingsAboutViewController.m */, 8441EFF82921B53500E851E9 /* BackgroundSettings.swift */, - 20ED55842BADDA5C0005783E /* PrivacySettings.swift */, + 20ED55842BADDA5C0005783E /* GeneralSettings.swift */, ); name = Settings; sourceTree = ""; @@ -2036,7 +2036,7 @@ 26158AF21FFA6E4500E53BDC /* MLWebViewController.m in Sources */, C1943A4C25309A9D0036172F /* MLReloadCell.m in Sources */, 262E51971AD8CB7200788351 /* MLTextInputCell.m in Sources */, - 20ED55852BADDA5C0005783E /* PrivacySettings.swift in Sources */, + 20ED55852BADDA5C0005783E /* GeneralSettings.swift in Sources */, 84E55E7D2964424E003E191A /* ActiveChatsViewController.m in Sources */, C1E8A7F72B8E47C300760220 /* EditGroupSubject.swift in Sources */, 263DFAC32187D0E00038E716 /* MLLinkCell.m in Sources */, @@ -2056,7 +2056,7 @@ 54F0B81928231691003664BD /* WelcomeLogIn.swift in Sources */, E8CF9CC726249640001A1952 /* MLSettingsAboutViewController.m in Sources */, C10490492612ED2F0054AC9E /* MLEmoji.swift in Sources */, - 3D06A515281FFCC000DDAE90 /* NotificationSettings.swift in Sources */, + 3D06A515281FFCC000DDAE90 /* NotificationDebugging.swift in Sources */, 845D636B2AD4AEDA0066EFFB /* ImageViewer.swift in Sources */, 2636C43F177BD58C001CA71F /* XMPPEdit.m in Sources */, 84FC375928981A5600634E3E /* PasswordMigration.swift in Sources */, From 9ca93523108b4bfc4adfa5fa908eb2ed946196e1 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Tue, 21 May 2024 04:08:51 +0200 Subject: [PATCH 097/143] Clean up some older code --- Monal/Classes/RegisterAccount.swift | 3 +-- Monal/Classes/SwiftHelpers.swift | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Monal/Classes/RegisterAccount.swift b/Monal/Classes/RegisterAccount.swift index 7df568471e..3d729949dc 100644 --- a/Monal/Classes/RegisterAccount.swift +++ b/Monal/Classes/RegisterAccount.swift @@ -74,8 +74,7 @@ struct RegisterAccount: View { self._username = State(wrappedValue:(registerData["username"] as? String) ?? "") self._registerToken = State(wrappedValue:registerData["token"] as? String) if let completion = registerData["completion"] { - //see https://stackoverflow.com/a/40592109/3528174 - self._completionHandler = State(wrappedValue:unsafeBitCast(completion, to:monal_id_block_t.self)) + self._completionHandler = State(wrappedValue:objcCast(completion) as monal_id_block_t) } DDLogVerbose("registerToken is now: \(String(describing:self.registerToken))") DDLogVerbose("Completion handler is now: \(String(describing:self.completionHandler))") diff --git a/Monal/Classes/SwiftHelpers.swift b/Monal/Classes/SwiftHelpers.swift index 4577e4879a..a2b0d39adb 100644 --- a/Monal/Classes/SwiftHelpers.swift +++ b/Monal/Classes/SwiftHelpers.swift @@ -28,6 +28,7 @@ let BGFETCH_DEFAULT_INTERVAL = HelperTools.getObjcDefinedValue(.BGFETCH_DEFAULT_ public typealias monal_void_block_t = @convention(block) () -> Void; public typealias monal_id_block_t = @convention(block) (AnyObject?) -> Void; +//see https://stackoverflow.com/a/40592109/3528174 public func objcCast(_ obj: Any) -> T { return unsafeBitCast(obj as AnyObject, to:T.self) } From 95908d16deb21e6013a89ae90178d1fb025ceabb Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Tue, 21 May 2024 04:09:02 +0200 Subject: [PATCH 098/143] Make sure privacy settings get displayed after first login/register --- Monal/Classes/RegisterAccount.swift | 8 ++++++++ Monal/Classes/WelcomeLogIn.swift | 14 ++++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/Monal/Classes/RegisterAccount.swift b/Monal/Classes/RegisterAccount.swift index 3d729949dc..4d3e449ece 100644 --- a/Monal/Classes/RegisterAccount.swift +++ b/Monal/Classes/RegisterAccount.swift @@ -387,6 +387,14 @@ struct RegisterAccount: View { Alert(title: alertPrompt.title, message: alertPrompt.message, dismissButton: .default(alertPrompt.dismissLabel, action: { if(self.registerComplete == true) { self.delegate.dismiss() + + if !HelperTools.defaultsDB().bool(forKey:"HasSeenPrivacySettings") { + let appDelegate = UIApplication.shared.delegate as! MonalAppDelegate + if let activeChats = appDelegate.activeChats { + activeChats.showPrivacySettings() + } + } + if let completion = self.completionHandler { DDLogVerbose("Calling reg completion handler...") completion(self.registeredAccountNo as NSNumber) diff --git a/Monal/Classes/WelcomeLogIn.swift b/Monal/Classes/WelcomeLogIn.swift index 0379ee5d73..d1c277a944 100644 --- a/Monal/Classes/WelcomeLogIn.swift +++ b/Monal/Classes/WelcomeLogIn.swift @@ -110,6 +110,16 @@ struct WelcomeLogIn: View { } } } + + private func dismissAndShowPrivacySettings() { + self.delegate.dismiss() + if !HelperTools.defaultsDB().bool(forKey:"HasSeenPrivacySettings") { + let appDelegate = UIApplication.shared.delegate as! MonalAppDelegate + if let activeChats = appDelegate.activeChats { + activeChats.showPrivacySettings() + } + } + } var body: some View { ScrollView { @@ -176,7 +186,7 @@ struct WelcomeLogIn: View { .alert(isPresented: $showAlert) { Alert(title: alertPrompt.title, message: alertPrompt.message, dismissButton: .default(alertPrompt.dismissLabel, action: { if(self.loginComplete == true) { - self.delegate.dismiss() + dismissAndShowPrivacySettings() } })) } @@ -215,7 +225,7 @@ struct WelcomeLogIn: View { if(DataLayer.sharedInstance().enabledAccountCnts() == 0) { Button(action: { - self.delegate.dismiss() + dismissAndShowPrivacySettings() }){ Text("Set up account later") .frame(maxWidth: .infinity) From 4b3bd91549512f1b60559119994143462f0727f5 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Tue, 21 May 2024 17:10:48 +0200 Subject: [PATCH 099/143] Fix some very rare race conditions on incoming calls --- Monal/Classes/MLVoIPProcessor.m | 8 +++++--- Monal/Classes/MonalAppDelegate.m | 6 +++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/Monal/Classes/MLVoIPProcessor.m b/Monal/Classes/MLVoIPProcessor.m index 36021c6131..4dc2d490ad 100644 --- a/Monal/Classes/MLVoIPProcessor.m +++ b/Monal/Classes/MLVoIPProcessor.m @@ -201,7 +201,7 @@ -(void) voipRegistration -(void) pushRegistry:(PKPushRegistry*) registry didUpdatePushCredentials:(PKPushCredentials*) credentials forType:(NSString*) type { NSString* token = [HelperTools stringFromToken:credentials.token]; - DDLogDebug(@"Ignoring APNS voip token string: %@", token); + DDLogDebug(@"Ignoring APNS voip token string for type %@: %@", type, token); } -(void) pushRegistry:(PKPushRegistry*) registry didInvalidatePushTokenForType:(NSString*) type @@ -332,14 +332,16 @@ -(void) processIncomingCall:(NSDictionary* _Nonnull) userInfo withCompletion:(vo //this will be done once the app delegate started to connect our xmpp accounts above //do this in an extra thread to not block this callback thread (could be main thread or otherwise restricted by apple) dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + DDLogDebug(@"Sending jmi ringing message..."); + [call sendJmiRinging]; + //wait for our account to connect before initializing webrtc using XEP-0215 iq stanzas //if the user proceeds the call before we are bound, the outgoing proceed message stanza will be queued and sent once we are bound //outgoing iq messages are not queued in all cases (e.g. non-smacks reconnect), hence this waiting loop while(call.account.accountState < kStateBound) [NSThread sleepForTimeInterval:0.250]; - DDLogDebug(@"Account is connected, now send jmi ringing message and really initialize WebRTC..."); - [call sendJmiRinging]; + DDLogDebug(@"Account is connected, now really initialize WebRTC..."); [self initWebRTCForPendingCall:call]; }); } diff --git a/Monal/Classes/MonalAppDelegate.m b/Monal/Classes/MonalAppDelegate.m index 81f3f6fbee..3a189b2412 100644 --- a/Monal/Classes/MonalAppDelegate.m +++ b/Monal/Classes/MonalAppDelegate.m @@ -387,9 +387,6 @@ -(BOOL) application:(UIApplication*) application willFinishLaunchingWithOptions: [[MLImageManager sharedInstance] cleanupHashes]; }); - //initialize callkit - _voipProcessor = [MLVoIPProcessor new]; - //only proceed with launching if the NotificationServiceExtension is *not* running if([MLProcessLock checkRemoteRunning:@"NotificationServiceExtension"]) { @@ -587,6 +584,9 @@ -(BOOL) application:(UIApplication*) application didFinishLaunchingWithOptions:( [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(windowHandling:) name:@"NSWindowDidResignKeyNotification" object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(windowHandling:) name:@"NSWindowDidBecomeKeyNotification" object:nil]; #endif + + //initialize callkit (mus be done after connectIfNecessary to make sure the list of accounts is already populated when a voip push comes in) + _voipProcessor = [MLVoIPProcessor new]; /* NSDictionary* options = launchOptions[UIApplicationLaunchOptionsUserActivityDictionaryKey]; From e3ea2cfcb0a6e297038626fba797d49cd26470ec Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Sat, 25 May 2024 00:26:45 +0200 Subject: [PATCH 100/143] Add new addTopRight view modifier to add a top-right overlay --- Monal/Classes/BackgroundSettings.swift | 37 +++++++++++++++++--------- Monal/Classes/SwiftuiHelpers.swift | 24 +++++++++++++++++ 2 files changed, 49 insertions(+), 12 deletions(-) diff --git a/Monal/Classes/BackgroundSettings.swift b/Monal/Classes/BackgroundSettings.swift index 87d100ae9a..81ce6d6900 100644 --- a/Monal/Classes/BackgroundSettings.swift +++ b/Monal/Classes/BackgroundSettings.swift @@ -33,8 +33,9 @@ struct BackgroundSettings: View { var body: some View { VStack { Form { - Group { - Section(header:title(contact:contact)) { + Section(header:title(contact:contact)) { + VStack(spacing: 20) { + Spacer().frame(height: 0) Button(action: { #if targetEnvironment(macCatalyst) let picker = DocumentPickerViewController( @@ -56,27 +57,39 @@ struct BackgroundSettings: View { #endif }) { if let inputImage = inputImage { - ZStack(alignment: .topLeading) { - HStack(alignment: .center) { - Image(uiImage:inputImage) - .resizable() - .aspectRatio(contentMode: .fit) - .frame(maxWidth: .infinity, alignment: .center) - } + HStack(alignment: .center) { + Image(uiImage:inputImage) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(maxWidth: .infinity, alignment: .center) + } + .addTopRight { Button(action: { self.inputImage = nil }, label: { - Image(systemName: "xmark.circle.fill").foregroundColor(.red) + Image(systemName: "xmark.circle.fill") + .resizable() + .frame(width: 32.0, height: 32.0) + .accessibilityLabel("Remove Background Image") + .applyClosure { view in + if #available(iOS 15, *) { + view + .symbolRenderingMode(.palette) + .foregroundStyle(.white, .red) + } else { + view.foregroundColor(.red) + } + } }) .buttonStyle(.borderless) - .offset(x: -7, y: -7) + .offset(x: 12, y: -12) } - .frame(maxWidth: .infinity, alignment: .center) } else { Text("Select background image") .frame(maxWidth: .infinity, alignment: .center) } } + .accessibilityLabel("Change Background Image") .sheet(isPresented:$showingImagePicker) { ImagePicker(image:$inputImage) } diff --git a/Monal/Classes/SwiftuiHelpers.swift b/Monal/Classes/SwiftuiHelpers.swift index afec677650..0403f62d86 100644 --- a/Monal/Classes/SwiftuiHelpers.swift +++ b/Monal/Classes/SwiftuiHelpers.swift @@ -109,6 +109,30 @@ func mucAffiliationToString(_ affiliation: String?) -> String { return NSLocalizedString("", comment:"muc affiliation") } +struct TopRight: ViewModifier { + let overlay: T + public func body(content: Content) -> some View { + ZStack(alignment: .topLeading) { + content + VStack { + HStack { + Spacer() + overlay + } + Spacer() + } + } + } +} +extension View { + func addTopRight(view overlayClosure: @autoclosure @escaping () -> T) -> some View { + modifier(TopRight(overlay:overlayClosure())) + } + func addTopRight(@ViewBuilder _ overlayClosure: @escaping () -> some View) -> some View { + modifier(TopRight(overlay:overlayClosure())) + } +} + @ViewBuilder func buildNotificationStateLabel(_ description: Text, isWorking: Bool) -> some View { if(isWorking == true) { From c0eaa51b70d43f14bfa7a0b64a1f97b1c9c684d0 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Sat, 25 May 2024 00:28:26 +0200 Subject: [PATCH 101/143] Update muc contact details to allow removal of avatars This adds new addTopRight-powered overlay for add and delete, too. --- Monal/Classes/ContactDetails.swift | 122 ++++++++++++++++++++++++----- Monal/Classes/MLContact.h | 3 +- Monal/Classes/MLContact.m | 5 ++ Monal/Classes/MLImageManager.h | 1 + Monal/Classes/MLImageManager.m | 12 +++ 5 files changed, 121 insertions(+), 22 deletions(-) diff --git a/Monal/Classes/ContactDetails.swift b/Monal/Classes/ContactDetails.swift index ca42b7d17e..1e68cddf5c 100644 --- a/Monal/Classes/ContactDetails.swift +++ b/Monal/Classes/ContactDetails.swift @@ -12,6 +12,7 @@ struct ContactDetails: View { @State private var ownRole = "participant" @State private var ownAffiliation = "none" @StateObject var contact: ObservableKVOWrapper + @State private var showingRemoveAvatarConfirmation = false @State private var showingBlockContactConfirmation = false @State private var showingCannotBlockAlert = false @State private var showingRemoveContactConfirmation = false @@ -57,7 +58,41 @@ struct ContactDetails: View { alertPrompt.title = title alertPrompt.message = message showAlert = true - self.success = true // < dismiss entire view on close + success = true // < dismiss entire view on close + } + + private func performAction(_ title: Text, action: @escaping ()->Void) { + self.account.mucProcessor.addUIHandler({_data in let data = _data as! NSDictionary + DispatchQueue.main.async { + hideLoadingOverlay(self.overlay) + let success : Bool = data["success"] as! Bool; + if !success { + errorAlert(title: title, message: Text(data["errorMessage"] as? String ?? "Unknown error!")) + } + } + }, forMuc:self.contact.contactJid) + action() + } + + 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 { @@ -72,24 +107,50 @@ struct ContactDetails: View { if ownAffiliation == "owner" { view.accessibilityLabel((contact.mucType == "group") ? "Change Group Avatar" : "Change Channel Avatar") .onTapGesture { -#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 + showImagePicker() + } + .addTopRight { + if contact.hasAvatar { + Button(action: { + showingRemoveAvatarConfirmation = true + }, label: { + Image(systemName: "xmark.circle.fill") + .resizable() + .frame(width: 24.0, height: 24.0) + .accessibilityLabel((contact.mucType == "group") ? "Remove Group Avatar" : "Remove Channel Avatar") + .applyClosure { view in + if #available(iOS 15, *) { + view + .symbolRenderingMode(.palette) + .foregroundStyle(.white, .red) + } else { + view.foregroundColor(.red) + } } - } - }, - onDismiss: { - //do nothing on dismiss - } - ) - UIApplication.shared.windows.first?.rootViewController?.present(picker, animated: true) -#else - showingImagePicker = true -#endif + }) + .buttonStyle(.borderless) + .offset(x: 8, y: -8) + } else { + Button(action: { + showImagePicker() + }, label: { + Image(systemName: "pencil.circle.fill") + .resizable() + .frame(width: 24.0, height: 24.0) + .accessibilityLabel((contact.mucType == "group") ? "Change Group Avatar" : "Change Channel Avatar") +// .applyClosure { view in +// if #available(iOS 15, *) { +// view +// .symbolRenderingMode(.palette) +// .foregroundStyle(.primary, .secondary) +// } else { +// view.foregroundColor(.primary) +// } +// } + }) + .buttonStyle(.borderless) + .offset(x: 8, y: -8) + } } } else { view.accessibilityLabel((contact.mucType == "group") ? "Group Avatar" : "Channel Avatar") @@ -103,7 +164,24 @@ struct ContactDetails: View { .sheet(isPresented:$showingImagePicker) { ImagePicker(image:$inputImage) } - + .actionSheet(isPresented: $showingRemoveAvatarConfirmation) { + ActionSheet( + title: Text("Really remove avatar?"), + message: Text("This will remove the current avatar image and revert this group/channel to the default one."), + buttons: [ + .cancel(), + .destructive( + Text("Yes"), + action: { + showLoadingOverlay(overlay, headline: NSLocalizedString("Removing avatar...", comment: "")) + performAction(Text("Error removing avatar!")) { + self.account.mucProcessor.publishAvatar(nil, forMuc: contact.contactJid) + } + } + ) + ] + ) + } Button { UIPasteboard.general.setValue(contact.contactJid as String, forPasteboardType:UTType.utf8PlainText.identifier as String) @@ -580,8 +658,10 @@ struct ContactDetails: View { })) } .onChange(of:inputImage) { _ in - showLoadingOverlay(overlay, headline: NSLocalizedString("Uploading image...", comment: "")) - self.account.mucProcessor.publishAvatar(inputImage, forMuc: contact.contactJid) + showLoadingOverlay(overlay, headline: NSLocalizedString("Uploading avatar...", comment: "")) + performAction(Text("Error changing avatar!")) { + self.account.mucProcessor.publishAvatar(inputImage, forMuc: contact.contactJid) + } } .onChange(of:contact.avatar as UIImage) { _ in hideLoadingOverlay(overlay) diff --git a/Monal/Classes/MLContact.h b/Monal/Classes/MLContact.h index 6741dfb9f1..80a2b90b11 100644 --- a/Monal/Classes/MLContact.h +++ b/Monal/Classes/MLContact.h @@ -50,7 +50,8 @@ FOUNDATION_EXPORT NSString* const kAskSubscribe; */ @property (nonatomic, readonly) NSNumber* accountId; @property (nonatomic, readonly) NSString* contactJid; -@property (nonatomic, copy) UIImage* avatar; +@property (nonatomic, readonly, copy) UIImage* avatar; +@property (nonatomic, readonly) BOOL hasAvatar; @property (nonatomic, readonly) NSString* fullName; /** usually user assigned nick name diff --git a/Monal/Classes/MLContact.m b/Monal/Classes/MLContact.m index e84cb20804..fffaaea2e1 100644 --- a/Monal/Classes/MLContact.m +++ b/Monal/Classes/MLContact.m @@ -488,6 +488,11 @@ -(void) setAvatar:(UIImage*) avatar _avatar = [UIImage new]; //empty dummy image, to not save nil (should never happen, MLImageManager has default images) } +-(BOOL) hasAvatar +{ + return [[MLImageManager sharedInstance] hasIconForContact:self]; +} + -(BOOL) isSelfChat { xmpp* account = [[MLXMPPManager sharedInstance] getConnectedAccountForID:self.accountId]; diff --git a/Monal/Classes/MLImageManager.h b/Monal/Classes/MLImageManager.h index d4016ad77b..a41b060436 100644 --- a/Monal/Classes/MLImageManager.h +++ b/Monal/Classes/MLImageManager.h @@ -35,6 +35,7 @@ /** retrieves a uiimage for the icon. returns noicon.png if nothing is found. never returns nil. */ +-(BOOL) hasIconForContact:(MLContact* _Nonnull) contact; -(UIImage* _Nullable) getIconForContact:(MLContact* _Nonnull) contact withCompletion:(void (^_Nullable)(UIImage *_Nullable))completion; -(UIImage* _Nullable) getIconForContact:(MLContact* _Nonnull) contact; +(UIImage* _Nonnull) circularImage:(UIImage* _Nonnull) image; diff --git a/Monal/Classes/MLImageManager.m b/Monal/Classes/MLImageManager.m index 97cc9543d4..07215a3288 100644 --- a/Monal/Classes/MLImageManager.m +++ b/Monal/Classes/MLImageManager.m @@ -262,6 +262,18 @@ -(void) setIconForContact:(MLContact*) contact WithData:(NSData* _Nullable) data } +-(BOOL) hasIconForContact:(MLContact*) contact +{ + NSString* filename = [self fileNameforContact:contact]; + + NSString* writablePath = [self.documentsDirectory stringByAppendingPathComponent:@"buddyicons"]; + writablePath = [writablePath stringByAppendingPathComponent:contact.accountId.stringValue]; + writablePath = [writablePath stringByAppendingPathComponent:filename]; + + DDLogVerbose(@"Checking avatar image at: %@", writablePath); + return [UIImage imageWithContentsOfFile:writablePath] != nil; +} + -(UIImage*) getIconForContact:(MLContact*) contact { return [self getIconForContact:contact withCompletion:nil]; From bdede99a1deb6c2d9fbbb7ad2cc3d9e26aadf184 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Sat, 25 May 2024 01:45:34 +0200 Subject: [PATCH 102/143] Bump version to 6.4.0 --- Monal/Monal.xcodeproj/project.pbxproj | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Monal/Monal.xcodeproj/project.pbxproj b/Monal/Monal.xcodeproj/project.pbxproj index 50f3c39688..a6721e1d73 100644 --- a/Monal/Monal.xcodeproj/project.pbxproj +++ b/Monal/Monal.xcodeproj/project.pbxproj @@ -2645,7 +2645,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 14.0; LLVM_LTO = YES; MACOSX_DEPLOYMENT_TARGET = 11.0; - MARKETING_VERSION = 6.3.1; + MARKETING_VERSION = 6.4.0; PRESERVE_DEAD_CODE_INITS_AND_TERMS = NO; SDKROOT = iphoneos; STRIP_INSTALLED_PRODUCT = NO; @@ -3012,7 +3012,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 14.0; LLVM_LTO = YES; MACOSX_DEPLOYMENT_TARGET = 11.0; - MARKETING_VERSION = 6.3.1; + MARKETING_VERSION = 6.4.0; PRESERVE_DEAD_CODE_INITS_AND_TERMS = NO; SDKROOT = iphoneos; STRIP_INSTALLED_PRODUCT = NO; @@ -3174,7 +3174,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 14.0; LLVM_LTO = YES; MACOSX_DEPLOYMENT_TARGET = 11.0; - MARKETING_VERSION = 6.3.1; + MARKETING_VERSION = 6.4.0; ONLY_ACTIVE_ARCH = YES; PRESERVE_DEAD_CODE_INITS_AND_TERMS = NO; RUN_CLANG_STATIC_ANALYZER = YES; @@ -3450,7 +3450,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 14.0; LLVM_LTO = YES; MACOSX_DEPLOYMENT_TARGET = 11.0; - MARKETING_VERSION = 6.3.1; + MARKETING_VERSION = 6.4.0; PRESERVE_DEAD_CODE_INITS_AND_TERMS = NO; RUN_CLANG_STATIC_ANALYZER = YES; SDKROOT = iphoneos; @@ -3803,7 +3803,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 14.0; LLVM_LTO = YES; MACOSX_DEPLOYMENT_TARGET = 11.0; - MARKETING_VERSION = 6.3.1; + MARKETING_VERSION = 6.4.0; PRESERVE_DEAD_CODE_INITS_AND_TERMS = NO; SDKROOT = iphoneos; STRIP_INSTALLED_PRODUCT = NO; @@ -4217,7 +4217,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 14.0; LLVM_LTO = NO; MACOSX_DEPLOYMENT_TARGET = 11.0; - MARKETING_VERSION = 6.3.1; + MARKETING_VERSION = 6.4.0; PRESERVE_DEAD_CODE_INITS_AND_TERMS = NO; RUN_CLANG_STATIC_ANALYZER = YES; SDKROOT = iphoneos; From 376c207377cf3a65fb621cef9032298d89df3825 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Sat, 25 May 2024 01:45:34 +0200 Subject: [PATCH 103/143] Bump version to 6.4.0 --- Monal/Monal.xcodeproj/project.pbxproj | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Monal/Monal.xcodeproj/project.pbxproj b/Monal/Monal.xcodeproj/project.pbxproj index 50f3c39688..a6721e1d73 100644 --- a/Monal/Monal.xcodeproj/project.pbxproj +++ b/Monal/Monal.xcodeproj/project.pbxproj @@ -2645,7 +2645,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 14.0; LLVM_LTO = YES; MACOSX_DEPLOYMENT_TARGET = 11.0; - MARKETING_VERSION = 6.3.1; + MARKETING_VERSION = 6.4.0; PRESERVE_DEAD_CODE_INITS_AND_TERMS = NO; SDKROOT = iphoneos; STRIP_INSTALLED_PRODUCT = NO; @@ -3012,7 +3012,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 14.0; LLVM_LTO = YES; MACOSX_DEPLOYMENT_TARGET = 11.0; - MARKETING_VERSION = 6.3.1; + MARKETING_VERSION = 6.4.0; PRESERVE_DEAD_CODE_INITS_AND_TERMS = NO; SDKROOT = iphoneos; STRIP_INSTALLED_PRODUCT = NO; @@ -3174,7 +3174,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 14.0; LLVM_LTO = YES; MACOSX_DEPLOYMENT_TARGET = 11.0; - MARKETING_VERSION = 6.3.1; + MARKETING_VERSION = 6.4.0; ONLY_ACTIVE_ARCH = YES; PRESERVE_DEAD_CODE_INITS_AND_TERMS = NO; RUN_CLANG_STATIC_ANALYZER = YES; @@ -3450,7 +3450,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 14.0; LLVM_LTO = YES; MACOSX_DEPLOYMENT_TARGET = 11.0; - MARKETING_VERSION = 6.3.1; + MARKETING_VERSION = 6.4.0; PRESERVE_DEAD_CODE_INITS_AND_TERMS = NO; RUN_CLANG_STATIC_ANALYZER = YES; SDKROOT = iphoneos; @@ -3803,7 +3803,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 14.0; LLVM_LTO = YES; MACOSX_DEPLOYMENT_TARGET = 11.0; - MARKETING_VERSION = 6.3.1; + MARKETING_VERSION = 6.4.0; PRESERVE_DEAD_CODE_INITS_AND_TERMS = NO; SDKROOT = iphoneos; STRIP_INSTALLED_PRODUCT = NO; @@ -4217,7 +4217,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 14.0; LLVM_LTO = NO; MACOSX_DEPLOYMENT_TARGET = 11.0; - MARKETING_VERSION = 6.3.1; + MARKETING_VERSION = 6.4.0; PRESERVE_DEAD_CODE_INITS_AND_TERMS = NO; RUN_CLANG_STATIC_ANALYZER = YES; SDKROOT = iphoneos; From 48f386f5276c78246e6add9fd408bdec8d1f03ef Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Sat, 25 May 2024 02:23:28 +0200 Subject: [PATCH 104/143] --- 905 --- 6.4.0b1 From 958fd6511b5c2085391218f519684bd975c1726f Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Sat, 25 May 2024 23:48:25 +0200 Subject: [PATCH 105/143] Harmonize navigation bar translatability --- Monal/Classes/AddContactMenu.swift | 2 +- Monal/Classes/ChannelMemberList.swift | 2 +- Monal/Classes/ContactPicker.swift | 2 +- Monal/Classes/ContactRequestsMenu.swift | 2 +- Monal/Classes/ContactResources.swift | 2 +- Monal/Classes/CreateGroupMenu.swift | 2 +- Monal/Classes/GeneralSettings.swift | 10 +++++----- Monal/Classes/MemberList.swift | 2 +- Monal/Classes/OmemoKeys.swift | 2 +- Monal/Classes/OmemoQrCodeView.swift | 2 +- Monal/Classes/RegisterAccount.swift | 2 +- 11 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Monal/Classes/AddContactMenu.swift b/Monal/Classes/AddContactMenu.swift index 108b8a0368..8ad73be92b 100644 --- a/Monal/Classes/AddContactMenu.swift +++ b/Monal/Classes/AddContactMenu.swift @@ -287,7 +287,7 @@ struct AddContactMenu: View { } } .addLoadingOverlay(overlay) - .navigationBarTitle("Add Contact or Channel", displayMode: .inline) + .navigationBarTitle(Text("Add Contact or Channel"), displayMode: .inline) .navigationViewStyle(.stack) .toolbar(content: { ToolbarItemGroup(placement: .navigationBarTrailing) { diff --git a/Monal/Classes/ChannelMemberList.swift b/Monal/Classes/ChannelMemberList.swift index 7a5252c7de..42ddf06f1e 100644 --- a/Monal/Classes/ChannelMemberList.swift +++ b/Monal/Classes/ChannelMemberList.swift @@ -52,7 +52,7 @@ struct ChannelMemberList: View { } } } - .navigationBarTitle(NSLocalizedString("Channel Participants", comment: ""), displayMode: .inline) + .navigationBarTitle(Text("Channel Participants"), displayMode: .inline) .onAppear { updateParticipantList() } diff --git a/Monal/Classes/ContactPicker.swift b/Monal/Classes/ContactPicker.swift index 9aff509a5f..d180d0dd4b 100644 --- a/Monal/Classes/ContactPicker.swift +++ b/Monal/Classes/ContactPicker.swift @@ -127,7 +127,7 @@ struct ContactPicker: View { } } .listStyle(.inset) - .navigationBarTitle(NSLocalizedString("Contact Selection", comment: ""), displayMode: .inline) + .navigationBarTitle(Text("Contact Selection"), displayMode: .inline) .onDisappear { returnedContacts.removeAll() for contact in selectedContacts { diff --git a/Monal/Classes/ContactRequestsMenu.swift b/Monal/Classes/ContactRequestsMenu.swift index 80c873ff1a..040d3283af 100644 --- a/Monal/Classes/ContactRequestsMenu.swift +++ b/Monal/Classes/ContactRequestsMenu.swift @@ -85,7 +85,7 @@ struct ContactRequestsMenu: View { } } } - .navigationBarTitle("Contact Requests", displayMode: .inline) + .navigationBarTitle(Text("Contact Requests"), displayMode: .inline) .navigationViewStyle(.stack) .onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("kMonalContactRefresh")).receive(on: RunLoop.main)) { notification in self.pendingRequests = DataLayer.sharedInstance().allContactRequests() as! [MLContact] diff --git a/Monal/Classes/ContactResources.swift b/Monal/Classes/ContactResources.swift index cc691e7c8f..0a17df3205 100644 --- a/Monal/Classes/ContactResources.swift +++ b/Monal/Classes/ContactResources.swift @@ -144,7 +144,7 @@ struct ContactResources: View { } } } - .navigationBarTitle("Devices of \(contact.contactDisplayName as String)", displayMode: .inline) + .navigationBarTitle(Text("Devices of \(contact.contactDisplayName as String)"), displayMode: .inline) } } diff --git a/Monal/Classes/CreateGroupMenu.swift b/Monal/Classes/CreateGroupMenu.swift index f0f41f7d0e..12e9938a72 100644 --- a/Monal/Classes/CreateGroupMenu.swift +++ b/Monal/Classes/CreateGroupMenu.swift @@ -117,7 +117,7 @@ struct CreateGroupMenu: View { })) } .addLoadingOverlay(overlay) - .navigationBarTitle(NSLocalizedString("Create new group", comment:""), displayMode: .inline) + .navigationBarTitle(Text("Create new group"), displayMode: .inline) .navigationViewStyle(.stack) } } diff --git a/Monal/Classes/GeneralSettings.swift b/Monal/Classes/GeneralSettings.swift index 361b3e4ff4..aa432f4969 100644 --- a/Monal/Classes/GeneralSettings.swift +++ b/Monal/Classes/GeneralSettings.swift @@ -137,7 +137,7 @@ struct GeneralSettings: View { } } } - .navigationBarTitle("General Settings") + .navigationBarTitle(Text("General Settings")) .onAppear { generalSettingsDefaultsDB.hasSeenPrivacySettings = true } @@ -174,7 +174,7 @@ struct UserInterfaceSettings: View { } } } - .navigationBarTitle("User Interface", displayMode: .inline) + .navigationBarTitle(Text("User Interface"), displayMode: .inline) } } @@ -215,7 +215,7 @@ like hotel wifi, ugly mobile carriers etc. } } } - .navigationBarTitle("Security", displayMode: .inline) + .navigationBarTitle(Text("Security"), displayMode: .inline) } } @@ -265,7 +265,7 @@ struct PrivacySettings: View { } } } - .navigationBarTitle("Privacy", displayMode: .inline) + .navigationBarTitle(Text("Privacy"), displayMode: .inline) } } @@ -304,7 +304,7 @@ struct NotificationSettings: View { self.pushPermissionEnabled = (settings.authorizationStatus == .authorized || settings.authorizationStatus == .provisional); } } - .navigationBarTitle("Notifications", displayMode: .inline) + .navigationBarTitle(Text("Notifications"), displayMode: .inline) } } diff --git a/Monal/Classes/MemberList.swift b/Monal/Classes/MemberList.swift index 28ec53c2f3..03cf491b6c 100644 --- a/Monal/Classes/MemberList.swift +++ b/Monal/Classes/MemberList.swift @@ -282,7 +282,7 @@ struct MemberList: View { Alert(title: alertPrompt.title, message: alertPrompt.message, dismissButton: .default(alertPrompt.dismissLabel)) }) .addLoadingOverlay(overlay) - .navigationBarTitle("Group Members", displayMode: .inline) + .navigationBarTitle(Text("Group Members"), displayMode: .inline) .onAppear { updateMemberlist() } diff --git a/Monal/Classes/OmemoKeys.swift b/Monal/Classes/OmemoKeys.swift index 19aaf5bc28..826bd8d6e5 100644 --- a/Monal/Classes/OmemoKeys.swift +++ b/Monal/Classes/OmemoKeys.swift @@ -380,7 +380,7 @@ struct OmemoKeys: View { } } .accentColor(monalGreen) - .navigationBarTitle((self.ownKeys == true) ? "My Encryption Keys" : "Encryption Keys", displayMode: .inline) + .navigationBarTitle((self.ownKeys == true) ? Text("My Encryption Keys") : Text("Encryption Keys"), displayMode: .inline) .onAppear(perform: { self.selectedContact = self.contacts.first // needs to be done here as first is nil in init }) diff --git a/Monal/Classes/OmemoQrCodeView.swift b/Monal/Classes/OmemoQrCodeView.swift index e3b59b09f4..0ce34146bd 100644 --- a/Monal/Classes/OmemoQrCodeView.swift +++ b/Monal/Classes/OmemoQrCodeView.swift @@ -57,7 +57,7 @@ struct OmemoQrCodeView: View { .resizable() .scaledToFit() .aspectRatio(1, contentMode: .fit) - .navigationBarTitle("Keys of \(self.jid)", displayMode: .inline) + .navigationBarTitle(Text("Keys of \(self.jid)"), displayMode: .inline) } } diff --git a/Monal/Classes/RegisterAccount.swift b/Monal/Classes/RegisterAccount.swift index 4d3e449ece..90a7b7c9a8 100644 --- a/Monal/Classes/RegisterAccount.swift +++ b/Monal/Classes/RegisterAccount.swift @@ -417,7 +417,7 @@ struct RegisterAccount: View { .sheet(isPresented: $showWebView) { NavigationView { WebView(url: URL(string: (RegisterAccount.XMPPServer[$selectedServerIndex.wrappedValue]["TermsSite_\(Locale.current.languageCode ?? "default")"] ?? RegisterAccount.XMPPServer[$selectedServerIndex.wrappedValue]["TermsSite_default"])!)!) - .navigationBarTitle("Terms of \(RegisterAccount.XMPPServer[$selectedServerIndex.wrappedValue]["XMPPServer"]!)", displayMode: .inline) + .navigationBarTitle(Text("Terms of \(RegisterAccount.XMPPServer[$selectedServerIndex.wrappedValue]["XMPPServer"]!)"), displayMode: .inline) .toolbar(content: { ToolbarItem(placement: .bottomBar) { Button (action: { From 751043f5ce23ebe3cd5d29905ac04b0a588b2707 Mon Sep 17 00:00:00 2001 From: Justin Krehel <39449589+krehel@users.noreply.github.com> Date: Sat, 25 May 2024 22:41:46 -0400 Subject: [PATCH 106/143] docs: update Homebrew beta install instructions --- ReadMe.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ReadMe.md b/ReadMe.md index 3eab0cb559..b8ed7627d7 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -11,7 +11,7 @@ If you want to use the latest stable versions, search for Monal in the iOS or O | | iOS | macOS | macOS (homebrew) | |--------|---------------------------------------------------------------|----------------------------------------------------------|---------------------------------------------------------------------------| | Stable | [App Store](https://apps.apple.com/app/id317711500) | [App Store](https://apps.apple.com/app/id1499227291) | brew install --cask monal | -| Beta | [Testflight](https://testflight.apple.com/join/lLLlgHpB) | [Testflight](https://testflight.apple.com/join/tGH2m5vf) | brew tap homebrew/cask-versions
brew install --cask monal-beta | +| Beta | [Testflight](https://testflight.apple.com/join/lLLlgHpB) | [Testflight](https://testflight.apple.com/join/tGH2m5vf) | brew install --cask monal@beta | | Alpha | upon request to [info@monal-im.org](mailto:info@monal-im.org)
Then download from our [alpha download site](https://downloads.monal-im.org/monal-im/alpha/) | | brew tap monal-im/homebrew-monal-alpha
brew install --cask monal-alpha | From f11b426fa847cdc3540856fb2c9b2f99a084cea5 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Sun, 26 May 2024 05:09:00 +0200 Subject: [PATCH 107/143] Update doap file --- monal.doap | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/monal.doap b/monal.doap index 7fc3229462..6a689bb2f4 100644 --- a/monal.doap +++ b/monal.doap @@ -435,9 +435,9 @@ - partial + complete 4.8 - XEP-0333: Chat Markers (received markers won't ever be implemented, use XEP-0184 instead) + XEP-0333: Displayed Markers @@ -505,13 +505,6 @@ XEP-0368: SRV records for XMPP over TLS - - - - planned - XEP-0369: Mediated Information eXchange (MIX) - - @@ -715,6 +708,21 @@ XEP-0480: SASL Upgrade Tasks + + + + planned + XEP-0484: Fast Authentication Streamlining Tokens + + + + + + complete + 5.0 + XEP-0486: MUC Avatars + + From d670bcbb766fc087e078cc12c493a8c5ed2bfc17 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Sun, 26 May 2024 22:02:44 +0200 Subject: [PATCH 108/143] Use "device" instead of "phone" in general settings explanation string --- Monal/Classes/GeneralSettings.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Monal/Classes/GeneralSettings.swift b/Monal/Classes/GeneralSettings.swift index aa432f4969..838f3f9e5e 100644 --- a/Monal/Classes/GeneralSettings.swift +++ b/Monal/Classes/GeneralSettings.swift @@ -205,7 +205,7 @@ like hotel wifi, ugly mobile carriers etc. Toggle(isOn: $generalSettingsDefaultsDB.webrtcAllowP2P) { Text("Calls: Allow P2P sessions").font(.body) - Text("Allow your phone to establish a direct network connection to the remote party. This might leak your IP address to the caller/callee.").font(.footnote) + Text("Allow your device to establish a direct network connection to the remote party. This might leak your IP address to the caller/callee.").font(.footnote) } } From dc51c3a4b6d7f0fa3e70d56c514187a2a1be1507 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Sun, 26 May 2024 22:03:41 +0200 Subject: [PATCH 109/143] Show "old app" notifications for every incoming push for alpha builds --- Monal/NotificationService/NotificationService.m | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Monal/NotificationService/NotificationService.m b/Monal/NotificationService/NotificationService.m index 88d21331a7..6bb79c6e57 100644 --- a/Monal/NotificationService/NotificationService.m +++ b/Monal/NotificationService/NotificationService.m @@ -483,8 +483,12 @@ -(void) didReceiveNotificationRequest:(UNNotificationRequest*) request withConte [handlers addObject:contentHandler]; //only show this notification once a day at maximum (and if a build number was given in our push) +#ifdef IS_ALPHA + if(request.content.userInfo[@"firstGoodBuildNumber"] != nil) +#else NSDate* lastAppVersionAlert = [[HelperTools defaultsDB] objectForKey:@"lastAppVersionAlert"]; if((lastAppVersionAlert == nil || [[NSDate date] timeIntervalSinceDate:lastAppVersionAlert] > 86400) && request.content.userInfo[@"firstGoodBuildNumber"] != nil) +#endif { NSDictionary* infoDict = [[NSBundle mainBundle] infoDictionary]; long buildNumber = ((NSString*)[infoDict objectForKey:@"CFBundleVersion"]).integerValue; From d8829646b410e827a1bce5c57344529f22fe2aed Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Wed, 5 Jun 2024 02:31:46 +0200 Subject: [PATCH 110/143] Add es-AR and copy over es-419 --- Monal/Monal.xcodeproj/project.pbxproj | 9 +++++++++ Monal/localization/external | 2 +- Monal/shareSheet-iOS/localization/external | 2 +- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/Monal/Monal.xcodeproj/project.pbxproj b/Monal/Monal.xcodeproj/project.pbxproj index a6721e1d73..ffb7703525 100644 --- a/Monal/Monal.xcodeproj/project.pbxproj +++ b/Monal/Monal.xcodeproj/project.pbxproj @@ -589,6 +589,10 @@ 84C1CD532A8F6196007076ED /* MLStreamRedirect.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MLStreamRedirect.h; sourceTree = ""; }; 84D31CE528653B83006D7926 /* WebRTCClient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = WebRTCClient.swift; path = Classes/WebRTCClient.swift; sourceTree = SOURCE_ROOT; }; 84E55E7F2964426D003E191A /* ActiveChatsViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ActiveChatsViewController.h; sourceTree = ""; }; + 84F194C32C0FE70900F0A994 /* es-AR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-AR"; path = "external/es-AR.lproj/Main.strings"; sourceTree = ""; }; + 84F194C42C0FE74500F0A994 /* es-AR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-AR"; path = "external/es-AR.lproj/Settings.strings"; sourceTree = ""; }; + 84F194C52C0FE78B00F0A994 /* es-AR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-AR"; path = "external/es-AR.lproj/iosShare.strings"; sourceTree = ""; }; + 84F194C62C0FE79000F0A994 /* es-AR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-AR"; path = "external/es-AR.lproj/Localizable.strings"; sourceTree = ""; }; 84FC37542897521400634E3E /* snprintf.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = snprintf.m; sourceTree = ""; }; 84FC37562897523500634E3E /* metamacros.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = metamacros.h; sourceTree = ""; }; 84FC375828981A5600634E3E /* PasswordMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordMigration.swift; sourceTree = ""; }; @@ -1724,6 +1728,7 @@ pa, ko, eu, + "es-AR", ); mainGroup = 29B97314FDCFA39411CA2CEA /* CustomTemplate */; packageReferences = ( @@ -2277,6 +2282,7 @@ C132EA9B26C92E9000BB9A67 /* pa */, C15A4E5F279D2AC80055CD11 /* ko */, C1E856A828DECF5F00B104E9 /* eu */, + 84F194C52C0FE78B00F0A994 /* es-AR */, ); name = iosShare.storyboard; sourceTree = ""; @@ -2318,6 +2324,7 @@ C132EA9626C92DD900BB9A67 /* pa */, C15A4E5D279D2AC70055CD11 /* ko */, C1E856A728DECF5F00B104E9 /* eu */, + 84F194C42C0FE74500F0A994 /* es-AR */, ); name = Settings.storyboard; sourceTree = ""; @@ -2359,6 +2366,7 @@ C132EA9426C92DD900BB9A67 /* pa */, C15A4E5B279D2AC60055CD11 /* ko */, C1E856A628DECF5F00B104E9 /* eu */, + 84F194C32C0FE70900F0A994 /* es-AR */, ); name = Main.storyboard; sourceTree = ""; @@ -2399,6 +2407,7 @@ C132EA9926C92DDA00BB9A67 /* pa */, C15A4E60279D2AC80055CD11 /* ko */, C1E856A928DECF5F00B104E9 /* eu */, + 84F194C62C0FE79000F0A994 /* es-AR */, ); name = Localizable.strings; sourceTree = ""; diff --git a/Monal/localization/external b/Monal/localization/external index 3dbb185c77..0e154dda94 160000 --- a/Monal/localization/external +++ b/Monal/localization/external @@ -1 +1 @@ -Subproject commit 3dbb185c775b425d8b88c5cc053f7058477aa67b +Subproject commit 0e154dda9407207c8f4c1a62b94fd09cdb657115 diff --git a/Monal/shareSheet-iOS/localization/external b/Monal/shareSheet-iOS/localization/external index 5b78cb41ca..dcde486d23 160000 --- a/Monal/shareSheet-iOS/localization/external +++ b/Monal/shareSheet-iOS/localization/external @@ -1 +1 @@ -Subproject commit 5b78cb41ca8af29f50617ab95d1348ea4739e912 +Subproject commit dcde486d236887eba560cf1b3a4496422f33eb6a From 8b355eb8bdafa12ea0653f0cecab42abf8e0c101 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Wed, 5 Jun 2024 03:14:21 +0200 Subject: [PATCH 111/143] Try to get rid of parse queue freeze glitch --- Monal/Classes/HelperTools.m | 2 +- Monal/Classes/xmpp.m | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Monal/Classes/HelperTools.m b/Monal/Classes/HelperTools.m index abfa82d971..ccf2b26755 100644 --- a/Monal/Classes/HelperTools.m +++ b/Monal/Classes/HelperTools.m @@ -535,7 +535,7 @@ +(void) busyWaitForOperationQueue:(NSOperationQueue*) queue int busyWaitCounter = 0; NSTimeInterval waitTime = 0.0; NSDate* startTime = [NSDate date]; - while(queue.suspended != YES) + while([queue isSuspended] != YES) { busyWaitCounter++; waitTime = [[NSDate date] timeIntervalSinceDate:startTime]; diff --git a/Monal/Classes/xmpp.m b/Monal/Classes/xmpp.m index c567e94e6b..34cb710034 100644 --- a/Monal/Classes/xmpp.m +++ b/Monal/Classes/xmpp.m @@ -716,7 +716,7 @@ -(BOOL) connectionTask -(BOOL) parseQueueFrozen { - return _parseQueue.suspended == YES; + return [_parseQueue isSuspended] == YES; } -(void) freezeParseQueue @@ -729,15 +729,16 @@ -(void) freezeParseQueue //apparently setting _parseQueue.suspended = YES does return before the queue is actually suspended //--> busy wait for _parseQueue.suspended == YES [HelperTools busyWaitForOperationQueue:_parseQueue]; + MLAssert([self parseQueueFrozen] == YES, @"Parse queue not frozen after setting suspended to YES!"); //this has to be synchronous because we want to be sure no further stanzas are leaking from the parse queue //into the receive queue once we leave this method //--> wait for all blocks put into the receive queue by the parse queue right before it was frozen [self dispatchOnReceiveQueue: ^{ + [HelperTools busyWaitForOperationQueue:self->_parseQueue]; MLAssert([self parseQueueFrozen] == YES, @"Parse queue not frozen after setting suspended to YES (in receive queue)!"); DDLogInfo(@"Parse queue is frozen now!"); }]; - MLAssert([self parseQueueFrozen] == YES, @"Parse queue not frozen after setting suspended to YES!"); } -(void) unfreezeParseQueue From 6bdc840ff3607830f7583265277baf161142d05c Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Wed, 5 Jun 2024 07:18:46 +0200 Subject: [PATCH 112/143] Make muc invites call ui error handlers, too This also modernizes kMonalSentMessageNotice. --- Monal/Classes/MLMucProcessor.m | 89 ++++++++++++++++++++++++++++-- Monal/Classes/MLXMPPManager.m | 4 +- Monal/Classes/chatViewController.m | 5 +- Monal/Classes/xmpp.m | 2 +- 4 files changed, 90 insertions(+), 10 deletions(-) diff --git a/Monal/Classes/MLMucProcessor.m b/Monal/Classes/MLMucProcessor.m index 4d57520872..a3787d4a29 100644 --- a/Monal/Classes/MLMucProcessor.m +++ b/Monal/Classes/MLMucProcessor.m @@ -39,7 +39,7 @@ @interface MLMucProcessor() NSDate* _lastPing; NSMutableSet* _noUpdateBookmarks; BOOL _hasFetchedBookmarks; - //this won't be persisted because it is only for the ui + //these won't be persisted because it is only for the ui NSMutableDictionary* _uiHandler; } @end @@ -87,6 +87,7 @@ -(id) initWithAccount:(xmpp*) account [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleResourceBound:) name:kMLResourceBoundNotice object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleCatchupDone:) name:kMonalFinishedCatchup object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleSentMessage:) name:kMonalSentMessageNotice object:nil]; return self; } @@ -183,6 +184,39 @@ -(void) handleCatchupDone:(NSNotification*) notification } } +-(void) handleSentMessage:(NSNotification*) notification +{ + XMPPMessage* msg = notification.userInfo[@"message"]; + NSString* callUiHandlerFor = nil; + + //check if this is a direct invite (direct invites always follow indirect ones, so we don't have to check for indirect ones) + if([msg check:@"/{jabber:client}message/{jabber:x:conference}x@jid"]) + callUiHandlerFor = [msg findFirst:@"/{jabber:client}message/{jabber:x:conference}x@jid"]; + + //check for muc subject change + if([msg check:@"/{jabber:client}message/subject"]) + callUiHandlerFor = msg.toUser; + + if(callUiHandlerFor != nil) + { + monal_id_block_t uiHandler = [self getUIHandlerForMuc:callUiHandlerFor]; + if(uiHandler) + { + //remove handler (it will only be called once) + [self removeUIHandlerForMuc:callUiHandlerFor]; + + DDLogInfo(@"Calling UI handler for muc %@...", callUiHandlerFor); + dispatch_async(dispatch_get_main_queue(), ^{ + uiHandler(@{ + @"success": @YES, + @"muc": callUiHandlerFor, + @"account": self->_account + }); + }); + } + } +} + -(BOOL) isCreating:(NSString*) room { @synchronized(_stateLockObject) { @@ -550,7 +584,7 @@ -(void) configureMuc:(NSString*) roomJid withMandatoryOptions:(NSDictionary*) ma [self removeRoomFromCreating:roomJid]; [self deleteMuc:roomJid withBookmarksUpdate:NO keepBuddylistEntry:NO]; } - [self handleError:[NSString stringWithFormat:@"Could not configure new group '%@': config option '%@' not available!", roomJid, option] forMuc:roomJid withNode:nil andIsSevere:YES]; + [self handleError:[NSString stringWithFormat:NSLocalizedString(@"Could not configure (new) group '%@': config option '%@' not available!", @""), roomJid, option] forMuc:roomJid withNode:nil andIsSevere:YES]; return; } else @@ -604,6 +638,22 @@ -(void) configureMuc:(NSString*) roomJid withMandatoryOptions:(NSDictionary*) ma @"roomJid": [NSString stringWithFormat:@"%@", roomJid], })); + monal_id_block_t uiHandler = [self getUIHandlerForMuc:iqNode.fromUser]; + if(uiHandler) + { + //remove handler (it will only be called once) + [self removeUIHandlerForMuc:iqNode.fromUser]; + + DDLogInfo(@"Calling UI handler for muc %@...", iqNode.fromUser); + dispatch_async(dispatch_get_main_queue(), ^{ + uiHandler(@{ + @"success": @YES, + @"muc": iqNode.fromUser, + @"account": self->_account + }); + }); + } + if(joinOnSuccess) { //group is now properly configured and we are joined, but all the code handling a proper join was not run @@ -1247,13 +1297,14 @@ -(void) inviteUser:(NSString*) jid inMuc:(NSString*) roomJid @"to": jid } andChildren:@[] andData:nil] ] andData:nil]]; - [_account send:indirectInviteMsg]; + [self->_account send:indirectInviteMsg]; XMPPMessage* directInviteMsg = [[XMPPMessage alloc] initWithType:kMessageNormalType to:jid]; [directInviteMsg addChildNode:[[MLXMLNode alloc] initWithElement:@"x" andNamespace:@"jabber:x:conference" withAttributes:@{ @"jid": roomJid } andChildren:@[] andData:nil]]; - [_account send:directInviteMsg]; + [self->_account send:directInviteMsg]; + } -(void) setAffiliation:(NSString*) affiliation ofUser:(NSString*) jid inMuc:(NSString*) roomJid @@ -1277,6 +1328,21 @@ -(void) setAffiliation:(NSString*) affiliation ofUser:(NSString*) jid inMuc:(NSS return; } DDLogInfo(@"Successfully changed affiliation of '%@' in '%@' to '%@'", jid, roomJid, affiliation); + monal_id_block_t uiHandler = [self getUIHandlerForMuc:iqNode.fromUser]; + if(uiHandler) + { + //remove handler (it will only be called once) + [self removeUIHandlerForMuc:iqNode.fromUser]; + + DDLogInfo(@"Calling UI handler for muc %@...", iqNode.fromUser); + dispatch_async(dispatch_get_main_queue(), ^{ + uiHandler(@{ + @"success": @YES, + @"muc": iqNode.fromUser, + @"account": self->_account + }); + }); + } $$ -(void) changeNameOfMuc:(NSString*) room to:(NSString*) name @@ -1327,6 +1393,21 @@ -(void) publishAvatar:(UIImage* _Nullable) image forMuc:(NSString*) room return; } DDLogInfo(@"Successfully published avatar for muc: %@", iqNode.fromUser); + monal_id_block_t uiHandler = [self getUIHandlerForMuc:iqNode.fromUser]; + if(uiHandler) + { + //remove handler (it will only be called once) + [self removeUIHandlerForMuc:iqNode.fromUser]; + + DDLogInfo(@"Calling UI handler for muc %@...", iqNode.fromUser); + dispatch_async(dispatch_get_main_queue(), ^{ + uiHandler(@{ + @"success": @YES, + @"muc": iqNode.fromUser, + @"account": self->_account + }); + }); + } $$ $$instance_handler(handleDiscoResponseInvalidation, account.mucProcessor, $$ID(xmpp*, account), $$ID(NSString*, roomJid)) diff --git a/Monal/Classes/MLXMPPManager.m b/Monal/Classes/MLXMPPManager.m index 35857c5bac..5c995a32be 100644 --- a/Monal/Classes/MLXMPPManager.m +++ b/Monal/Classes/MLXMPPManager.m @@ -13,6 +13,7 @@ #import "DataLayer.h" #import "HelperTools.h" #import "xmpp.h" +#import "XMPPMessage.h" #import "MLNotificationQueue.h" #import "MLNotificationManager.h" #import "MLOMEMO.h" @@ -862,8 +863,7 @@ -(void) block:(BOOL) isBlocked fullJid:(NSString*) fullJid onAccount:(NSNumber*) -(void) handleSentMessage:(NSNotification*) notification { - NSDictionary* info = notification.userInfo; - NSString* messageId = [info objectForKey:kMessageId]; + NSString* messageId = ((XMPPMessage*)notification.userInfo[@"message"]).id; DDLogInfo(@"message %@ sent, setting status accordingly", messageId); [[DataLayer sharedInstance] setMessageId:messageId sent:YES]; } diff --git a/Monal/Classes/chatViewController.m b/Monal/Classes/chatViewController.m index d404b74115..9a49abc771 100644 --- a/Monal/Classes/chatViewController.m +++ b/Monal/Classes/chatViewController.m @@ -32,6 +32,7 @@ #import "MLXEPSlashMeHandler.h" #import "MonalAppDelegate.h" #import "xmpp.h" +#import "XMPPMessage.h" #import #import @@ -1863,11 +1864,9 @@ -(void) updateMsgState:(NSString *) messageId withEvent:(size_t) event withOptDi } } - -(void) handleSentMessage:(NSNotification*) notification { - NSDictionary* dic = notification.userInfo; - [self updateMsgState:[dic objectForKey:kMessageId] withEvent:msgSent withOptDic:nil]; + [self updateMsgState:((XMPPMessage*)notification.userInfo[@"message"]).id withEvent:msgSent withOptDic:nil]; } -(void) handleMessageError:(NSNotification*) notification diff --git a/Monal/Classes/xmpp.m b/Monal/Classes/xmpp.m index 34cb710034..dcf509759e 100644 --- a/Monal/Classes/xmpp.m +++ b/Monal/Classes/xmpp.m @@ -1685,7 +1685,7 @@ -(BOOL) removeAckedStanzasFromQueue:(NSNumber*) hvalue { XMPPMessage* messageNode = (XMPPMessage*)node; if(messageNode.id) - [[MLNotificationQueue currentQueue] postNotificationName:kMonalSentMessageNotice object:self userInfo:@{kMessageId:messageNode.id}]; + [[MLNotificationQueue currentQueue] postNotificationName:kMonalSentMessageNotice object:self userInfo:@{@"message":messageNode}]; } } } From c7473721c830cf3347869beb8c5f81a778f7488f Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Wed, 5 Jun 2024 09:13:07 +0200 Subject: [PATCH 113/143] Make string translatable in OmemoKeys --- Monal/Classes/OmemoKeys.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Monal/Classes/OmemoKeys.swift b/Monal/Classes/OmemoKeys.swift index 826bd8d6e5..09eea4dd70 100644 --- a/Monal/Classes/OmemoKeys.swift +++ b/Monal/Classes/OmemoKeys.swift @@ -387,7 +387,7 @@ struct OmemoKeys: View { .alert(isPresented: $showScannedContactMissmatchAlert) { Alert( title: Text("QR code: Fingerprints found"), - message: Text(String.localizedStringWithFormat("Do you want to trust the scanned fingerprints of contact %@ when using your account %@?", self.scannedJid, self.account!.connectionProperties.identity.jid)), + message: Text("Do you want to trust the scanned fingerprints of contact \(self.scannedJid) when using your account \(self.account!.connectionProperties.identity.jid)?"), primaryButton: .cancel(Text("No")), secondaryButton: .default(Text("Yes"), action: { resetTrustFromQR(scannedJid: self.scannedJid, scannedFingerprints: self.scannedFingerprints) From 642fc086b179964e7763133f45d6fef478d13bd4 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Wed, 5 Jun 2024 09:16:41 +0200 Subject: [PATCH 114/143] Introduce promises and simplify muc ui handler using promises --- Monal/Classes/AddContactMenu.swift | 20 +++-- Monal/Classes/ContactDetails.swift | 49 +++++-------- Monal/Classes/MemberList.swift | 102 ++++++++++++++------------ Monal/Classes/SwiftHelpers.swift | 4 + Monal/Classes/SwiftuiHelpers.swift | 28 +++++++ Monal/Monal.xcodeproj/project.pbxproj | 17 +++++ 6 files changed, 134 insertions(+), 86 deletions(-) diff --git a/Monal/Classes/AddContactMenu.swift b/Monal/Classes/AddContactMenu.swift index 8ad73be92b..c5b13617db 100644 --- a/Monal/Classes/AddContactMenu.swift +++ b/Monal/Classes/AddContactMenu.swift @@ -145,18 +145,16 @@ struct AddContactMenu: View { trustFingerprints(self.importScannedFingerprints ? self.scannedFingerprints : [:], for:jid, on:account) 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" { - showLoadingOverlay(overlay, headline: NSLocalizedString("Adding Group/Channel...", comment: "")) - account.mucProcessor.addUIHandler({_data in let data = _data as! NSDictionary - let success : Bool = data["success"] as! Bool; + performMucAction(account:account, mucJid:jid, overlay:overlay, headlineView:Text("Adding Group/Channel..."), descriptionView:Text("")) { + account.joinMuc(jid) + }.done { _ in + self.newContact = MLContact.createContact(fromJid: jid, andAccountNo: account.accountNo) + successAlert(title: Text("Success!"), message: Text("Successfully joined group/channel \(jid)!")) + }.catch { error in + errorAlert(title: Text("Error entering group/channel!"), message: Text("\(String(describing:error))")) + }.finally { hideLoadingOverlay(overlay) - if success { - self.newContact = MLContact.createContact(fromJid: jid, andAccountNo: account.accountNo) - successAlert(title: Text("Success!"), message: Text(String.localizedStringWithFormat("Successfully joined group/channel %@!", jid))) - } else { - errorAlert(title: Text("Error entering group/channel!"), message: Text(data["errorMessage"] as! String)) - } - }, forMuc: jid) - account.joinMuc(jid) + } } else { hideLoadingOverlay(overlay) errorAlert(title: Text("Error"), message: Text(errorMsg ?? "Undefined error")) diff --git a/Monal/Classes/ContactDetails.swift b/Monal/Classes/ContactDetails.swift index 1e68cddf5c..7501060366 100644 --- a/Monal/Classes/ContactDetails.swift +++ b/Monal/Classes/ContactDetails.swift @@ -61,19 +61,6 @@ struct ContactDetails: View { success = true // < dismiss entire view on close } - private func performAction(_ title: Text, action: @escaping ()->Void) { - self.account.mucProcessor.addUIHandler({_data in let data = _data as! NSDictionary - DispatchQueue.main.async { - hideLoadingOverlay(self.overlay) - let success : Bool = data["success"] as! Bool; - if !success { - errorAlert(title: title, message: Text(data["errorMessage"] as? String ?? "Unknown error!")) - } - } - }, forMuc:self.contact.contactJid) - action() - } - private func showImagePicker() { #if targetEnvironment(macCatalyst) let picker = DocumentPickerViewController( @@ -173,9 +160,11 @@ struct ContactDetails: View { .destructive( Text("Yes"), action: { - showLoadingOverlay(overlay, headline: NSLocalizedString("Removing avatar...", comment: "")) - performAction(Text("Error removing avatar!")) { + performMucAction(account:account, mucJid:contact.contactJid, overlay:overlay, headlineView:Text("Removing avatar..."), descriptionView:Text("")) { self.account.mucProcessor.publishAvatar(nil, forMuc: contact.contactJid) + }.catch { error in + errorAlert(title: Text("Error removing avatar!"), message: Text("\(String(describing:error))")) + hideLoadingOverlay(overlay) } } ) @@ -557,20 +546,18 @@ struct ContactDetails: View { .destructive( Text("Yes"), action: { - showLoadingOverlay(overlay, headline: contact.mucType == "group" ? NSLocalizedString("Destroying group...", comment: "") : NSLocalizedString("Destroying channel...", comment: "")) - self.account.mucProcessor.destroyRoom(contact.contactJid as String) - self.account.mucProcessor.addUIHandler({_data in let data = _data as! NSDictionary - hideLoadingOverlay(overlay) - let success : Bool = data["success"] as! Bool; - if success { - if let callback = data["callback"] { - self.successCallback = objcCast(callback) as monal_void_block_t - } - successAlert(title: Text("Success"), message: contact.mucType == "group" ? Text("Successfully destroyed group.") : Text("Successfully destroyed channel.")) - } else { - errorAlert(title: Text("Error destroying group!"), message: Text(data["errorMessage"] as! String)) + performMucAction(account:account, mucJid:contact.contactJid, overlay:overlay, headlineView:contact.mucType == "group" ? Text("Destroying group...") : Text("Destroying channel..."), descriptionView:Text("")) { + self.account.mucProcessor.destroyRoom(contact.contactJid as String) + }.done { callback in + if let callback = callback { + self.successCallback = callback } - }, forMuc:contact.contactJid) + successAlert(title: Text("Success"), message: contact.mucType == "group" ? Text("Successfully destroyed group.") : Text("Successfully destroyed channel.")) + }.catch { error in + errorAlert(title: Text("Error destroying group!"), message: Text("\(String(describing:error))")) + }.finally { + hideLoadingOverlay(overlay) + } } ) ] @@ -658,9 +645,11 @@ struct ContactDetails: View { })) } .onChange(of:inputImage) { _ in - showLoadingOverlay(overlay, headline: NSLocalizedString("Uploading avatar...", comment: "")) - performAction(Text("Error changing avatar!")) { + performMucAction(account:account, mucJid:contact.contactJid, overlay:overlay, headlineView:Text("Uploading avatar..."), descriptionView:Text("")) { self.account.mucProcessor.publishAvatar(inputImage, forMuc: contact.contactJid) + }.catch { error in + errorAlert(title: Text("Error changing avatar!"), message: Text("\(String(describing:error))")) + hideLoadingOverlay(overlay) } } .onChange(of:contact.avatar as UIImage) { _ in diff --git a/Monal/Classes/MemberList.swift b/Monal/Classes/MemberList.swift index 03cf491b6c..669e2559f1 100644 --- a/Monal/Classes/MemberList.swift +++ b/Monal/Classes/MemberList.swift @@ -16,7 +16,7 @@ struct ActionSheetPrompt { struct MemberList: View { private let account: xmpp - @State private var ownAffiliation: String; + @State private var ownAffiliation: String @StateObject var muc: ObservableKVOWrapper @State private var memberList: OrderedSet> @State private var affiliations: Dictionary, String> @@ -57,17 +57,8 @@ struct MemberList: View { } } - func performAction(_ title: Text, action: @escaping ()->Void) { - self.account.mucProcessor.addUIHandler({_data in let data = _data as! NSDictionary - DispatchQueue.main.async { - hideLoadingOverlay(overlay) - let success : Bool = data["success"] as! Bool; - if !success { - showAlert(title: title, description: Text(data["errorMessage"] as? String ?? "Unknown error!")) - } - } - }, forMuc:self.muc.contactJid) - action() + func performAction(headlineView: some View, descriptionView: some View, action: @escaping ()->Void) -> Promise { + return performMucAction(account:self.account, mucJid:self.muc.contactJid, overlay:self.overlay, headlineView:headlineView, descriptionView:descriptionView, action:action) } func showAlert(title: Text, description: Text) { @@ -150,19 +141,27 @@ struct MemberList: View { for member in newMemberList { if !memberList.contains(member) { if self.muc.mucType == "group" { - showLoadingOverlay(overlay, headlineView: Text("Adding new member"), descriptionView: Text("Adding \(member.contactJid as String)...")) - performAction(Text("Error adding new member!")) { - DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 1.0) { - account.mucProcessor.setAffiliation("member", ofUser:member.contactJid, inMuc:self.muc.contactJid) + performAction(headlineView: Text("Adding new member"), descriptionView: Text("Adding \(member.contactJid as String)...")) { + account.mucProcessor.setAffiliation("member", ofUser:member.contactJid, inMuc:self.muc.contactJid) + }.then { _ in + return performAction(headlineView: Text("Inviting new member"), descriptionView: Text("Adding \(member.contactJid as String)...")) { account.mucProcessor.inviteUser(member.contactJid, inMuc: self.muc.contactJid) + }.recover { error in + showAlert(title:Text("Error inviting new member!"), description:Text("\(String(describing:error))")) + return Guarantee.value(nil as monal_void_block_t?) } + }.catch { error in + showAlert(title:Text("Error adding new member!"), description:Text("\(String(describing:error))")) + }.finally { + hideLoadingOverlay(overlay) } } else { - showLoadingOverlay(overlay, headlineView: Text("Inviting new participant"), descriptionView: Text("Adding \(member.contactJid as String)...")) - performAction(Text("Error adding new participant!")) { - DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 1.0) { - account.mucProcessor.inviteUser(member.contactJid, inMuc: self.muc.contactJid) - } + performAction(headlineView: Text("Inviting new participant"), descriptionView: Text("Adding \(member.contactJid as String)...")) { + account.mucProcessor.inviteUser(member.contactJid, inMuc: self.muc.contactJid) + }.catch { error in + showAlert(title:Text("Error inviting new participant!"), description:Text("\(String(describing:error))")) + }.finally { + hideLoadingOverlay(overlay) } } } @@ -190,34 +189,47 @@ struct MemberList: View { DDLogVerbose("Activating navigation to \(String(describing:contact))") navigationActive = contact } else if newAffiliation == "reinvite" { - showLoadingOverlay(overlay, headlineView: Text("Inviting user"), descriptionView: Text("Inviting user to this group/channel: \(contact.contactJid as String)")) - performAction(Text("Error inviting user!")) { - DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 1.0) { - //first remove potential ban, then reinvite - if affiliations[contact] == "outcast" { - account.mucProcessor.setAffiliation(self.muc.mucType == "group" ? "member" : "none", ofUser:contact.contactJid, inMuc:self.muc.contactJid) + //first remove potential ban, then reinvite + (affiliations[contact] == "outcast" ? + performAction(headlineView: Text("Unblocking user"), descriptionView: Text("Unblocking user for this group/channel: \(contact.contactJid as String)")) { + account.mucProcessor.setAffiliation(self.muc.mucType == "group" ? "member" : "none", ofUser:contact.contactJid, inMuc:self.muc.contactJid) + } : + Promise.value(nil) + ).then { _ in + return performAction(headlineView: Text("Inviting user"), descriptionView: Text("Inviting user to this group/channel: \(contact.contactJid as String)")) { + DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 1.0) { + account.mucProcessor.inviteUser(contact.contactJid, inMuc: self.muc.contactJid) } - account.mucProcessor.inviteUser(contact.contactJid, inMuc: self.muc.contactJid) } + .recover { error in + showAlert(title:Text("Error inviting user!"), description:Text("\(String(describing:error))")) + return Guarantee.value(nil as monal_void_block_t?) + } + }.catch { error in + showAlert(title:Text("Error unblocking user!"), description:Text("\(String(describing:error))")) + }.finally { + hideLoadingOverlay(overlay) } } else if newAffiliation == "outcast" { showActionSheet(title: Text("Block user?"), description: Text("Do you want to block this user from entering this group/channel?")) { DDLogVerbose("Changing affiliation of \(String(describing:contact)) to: \(String(describing:newAffiliation))...") - showLoadingOverlay(overlay, headlineView: Text("Blocking member"), descriptionView: Text("Blocking \(contact.contactJid as String)")) - performAction(Text("Error blocking user!")) { - DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 1.0) { - account.mucProcessor.setAffiliation(newAffiliation, ofUser:contact.contactJid, inMuc:self.muc.contactJid) - } + performAction(headlineView: Text("Blocking member"), descriptionView: Text("Blocking \(contact.contactJid as String)")) { + account.mucProcessor.setAffiliation(newAffiliation, ofUser:contact.contactJid, inMuc:self.muc.contactJid) + }.catch { error in + showAlert(title:Text("Error blocking user!"), description:Text("\(String(describing:error))")) + }.finally { + hideLoadingOverlay(overlay) } } } else { DDLogVerbose("Changing affiliation of \(String(describing:contact)) to: \(String(describing:newAffiliation))...") - showLoadingOverlay(overlay, headlineView: Text("Changing affiliation of member"), descriptionView: - Text("Changing \(contact.contactJid as String) to ") + Text(mucAffiliationToString(newAffiliation))) - performAction(Text("Error changing affiliation!")) { - DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 1.0) { - account.mucProcessor.setAffiliation(newAffiliation, ofUser:contact.contactJid, inMuc:self.muc.contactJid) - } + performAction(headlineView: Text("Changing affiliation"), descriptionView: + Text("Changing \(contact.contactJid as String) to ") + Text(mucAffiliationToString(newAffiliation))) { + account.mucProcessor.setAffiliation(newAffiliation, ofUser:contact.contactJid, inMuc:self.muc.contactJid) + }.catch { error in + showAlert(title:Text("Error changing affiliation!"), description:Text("\(String(describing:error))")) + }.finally { + hideLoadingOverlay(overlay) } } } @@ -241,11 +253,12 @@ struct MemberList: View { .onDelete(perform: { memberIdx in let member = memberList[memberIdx.first!] showActionSheet(title: Text("Remove user?"), description: self.muc.mucType == "group" ? Text("Do you want to remove this user from this group? The user won't be able to enter it again until added back to the group.") : Text("Do you want to remove this user from this channel? The user will be able to enter it again.")) { - showLoadingOverlay(overlay, headlineView: Text("Removing member"), descriptionView: Text("Removing \(member.contactJid as String)...")) - performAction(Text("Error removing user!")) { - DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 1.0) { - account.mucProcessor.setAffiliation("none", ofUser: member.contactJid, inMuc: self.muc.contactJid) - } + performAction(headlineView: Text("Removing member"), descriptionView: Text("Removing \(member.contactJid as String)...")) { + account.mucProcessor.setAffiliation("none", ofUser: member.contactJid, inMuc: self.muc.contactJid) + }.catch { error in + showAlert(title:Text("Error removing user!"), description:Text("\(String(describing:error))")) + }.finally { + hideLoadingOverlay(overlay) } } }) @@ -291,7 +304,6 @@ struct MemberList: View { DDLogVerbose("Got muc participants/members update from account \(xmppAccount)...") if contact == self.muc { updateMemberlist() - hideLoadingOverlay(overlay) } } } diff --git a/Monal/Classes/SwiftHelpers.swift b/Monal/Classes/SwiftHelpers.swift index a2b0d39adb..862460151b 100644 --- a/Monal/Classes/SwiftHelpers.swift +++ b/Monal/Classes/SwiftHelpers.swift @@ -10,6 +10,7 @@ @_exported import Foundation @_exported import CocoaLumberjackSwift @_exported import Logging +@_exported import PromiseKit import CocoaLumberjackSwiftLogBackend import LibMonalRustSwiftBridge import Combine @@ -28,6 +29,9 @@ let BGFETCH_DEFAULT_INTERVAL = HelperTools.getObjcDefinedValue(.BGFETCH_DEFAULT_ public typealias monal_void_block_t = @convention(block) () -> Void; public typealias monal_id_block_t = @convention(block) (AnyObject?) -> Void; +//see https://stackoverflow.com/a/40629365/3528174 +extension String: Error {} + //see https://stackoverflow.com/a/40592109/3528174 public func objcCast(_ obj: Any) -> T { return unsafeBitCast(obj as AnyObject, to:T.self) diff --git a/Monal/Classes/SwiftuiHelpers.swift b/Monal/Classes/SwiftuiHelpers.swift index 0403f62d86..1234e2fb71 100644 --- a/Monal/Classes/SwiftuiHelpers.swift +++ b/Monal/Classes/SwiftuiHelpers.swift @@ -88,6 +88,34 @@ func getContactList(viewContact: (ObservableKVOWrapper?)) -> OrderedS } } +extension UIPickerView { + override open func didMoveToSuperview() { + super.didMoveToSuperview() + self.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + } +} + +func performMucAction(account: xmpp, mucJid: String, overlay: LoadingOverlayState, headlineView: Optional, descriptionView: Optional, action: @escaping ()->Void) -> Promise { + showLoadingOverlay(overlay, headlineView:headlineView, descriptionView:descriptionView) + return Promise { seal in + DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 1.0) { + account.mucProcessor.addUIHandler({_data in let data = _data as! NSDictionary + let success : Bool = data["success"] as! Bool; + if !success { + seal.reject(data["errorMessage"] as? String ?? "Unknown error!") + } else { + if let callback = data["callback"] { + seal.fulfill(objcCast(callback) as monal_void_block_t) + } else { + seal.fulfill(nil) + } + } + }, forMuc:mucJid) + action() + } + } +} + func mucAffiliationToString(_ affiliation: String?) -> String { if let affiliation = affiliation { if affiliation == "owner" { diff --git a/Monal/Monal.xcodeproj/project.pbxproj b/Monal/Monal.xcodeproj/project.pbxproj index ffb7703525..043e4a2d4e 100644 --- a/Monal/Monal.xcodeproj/project.pbxproj +++ b/Monal/Monal.xcodeproj/project.pbxproj @@ -162,6 +162,7 @@ 84D31CE628653B83006D7926 /* WebRTCClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84D31CE528653B83006D7926 /* WebRTCClient.swift */; }; 84E55E7D2964424E003E191A /* ActiveChatsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 261A6284176C156500059090 /* ActiveChatsViewController.m */; }; 84E55E8029644279003E191A /* ActiveChatsViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = 84E55E7F2964426D003E191A /* ActiveChatsViewController.h */; }; + 84F194CE2C101A3E00F0A994 /* PromiseKit in Frameworks */ = {isa = PBXBuildFile; productRef = 84F194CD2C101A3E00F0A994 /* PromiseKit */; }; 84FC37552897521500634E3E /* snprintf.m in Sources */ = {isa = PBXBuildFile; fileRef = 84FC37542897521400634E3E /* snprintf.m */; }; 84FC37572897523500634E3E /* metamacros.h in Headers */ = {isa = PBXBuildFile; fileRef = 84FC37562897523500634E3E /* metamacros.h */; }; 84FC375928981A5600634E3E /* PasswordMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84FC375828981A5600634E3E /* PasswordMigration.swift */; }; @@ -801,6 +802,7 @@ buildActionMask = 2147483647; files = ( 849ADF3F2BACF0360009BCD7 /* CocoaLumberjack in Frameworks */, + 84F194CE2C101A3E00F0A994 /* PromiseKit in Frameworks */, BE8B63D2491B1E5582965A8F /* Pods_monalxmpp.framework in Frameworks */, 849ADF412BACF0360009BCD7 /* CocoaLumberjackSwift in Frameworks */, 849ADF432BACF0360009BCD7 /* CocoaLumberjackSwiftLogBackend in Frameworks */, @@ -1585,6 +1587,7 @@ 849ADF3E2BACF0360009BCD7 /* CocoaLumberjack */, 849ADF402BACF0360009BCD7 /* CocoaLumberjackSwift */, 849ADF422BACF0360009BCD7 /* CocoaLumberjackSwiftLogBackend */, + 84F194CD2C101A3E00F0A994 /* PromiseKit */, ); productName = monalxmpp; productReference = 26CC579223A0867400ABB92A /* monalxmpp.framework */; @@ -1736,6 +1739,7 @@ C1E1EC79286A025F0097EC74 /* XCRemoteSwiftPackageReference "SwiftSoup" */, 841898A82957712000FEC77D /* XCRemoteSwiftPackageReference "ViewExtractor" */, 849ADF3D2BACF0360009BCD7 /* XCRemoteSwiftPackageReference "cocoalumberjack" */, + 84F194CC2C101A3E00F0A994 /* XCRemoteSwiftPackageReference "PromiseKit" */, ); productRefGroup = 19C28FACFE9D520D11CA2CBB /* Products */; projectDirPath = ""; @@ -4622,6 +4626,14 @@ minimumVersion = 3.8.5; }; }; + 84F194CC2C101A3E00F0A994 /* XCRemoteSwiftPackageReference "PromiseKit" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/mxcl/PromiseKit"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 8.1.2; + }; + }; C1E1EC79286A025F0097EC74 /* XCRemoteSwiftPackageReference "SwiftSoup" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/scinfu/SwiftSoup.git"; @@ -4665,6 +4677,11 @@ package = 849ADF3D2BACF0360009BCD7 /* XCRemoteSwiftPackageReference "cocoalumberjack" */; productName = CocoaLumberjackSwiftLogBackend; }; + 84F194CD2C101A3E00F0A994 /* PromiseKit */ = { + isa = XCSwiftPackageProductDependency; + package = 84F194CC2C101A3E00F0A994 /* XCRemoteSwiftPackageReference "PromiseKit" */; + productName = PromiseKit; + }; C1E1EC7A286A025F0097EC74 /* SwiftSoup */ = { isa = XCSwiftPackageProductDependency; package = C1E1EC79286A025F0097EC74 /* XCRemoteSwiftPackageReference "SwiftSoup" */; From d1806cccdb828d8f78ab9ccdcca1af94c25419a4 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Thu, 6 Jun 2024 21:23:56 +0200 Subject: [PATCH 115/143] Don't show new deviceid alers on first login, see #1071 --- Monal/Classes/MLOMEMO.m | 39 ++++++++++++++++++++++++--------------- Monal/Classes/xmpp.h | 1 + Monal/Classes/xmpp.m | 33 ++++++++++++++++++++++++--------- 3 files changed, 49 insertions(+), 24 deletions(-) diff --git a/Monal/Classes/MLOMEMO.m b/Monal/Classes/MLOMEMO.m index d3e327dbe5..ad8a7e912c 100644 --- a/Monal/Classes/MLOMEMO.m +++ b/Monal/Classes/MLOMEMO.m @@ -52,8 +52,15 @@ -(MLOMEMO*) initWithAccount:(xmpp*) account; self.openBundleFetchCnt = 0; self.closedBundleFetchCnt = 0; - //create empty state (will be updated from [xmpp readState] before [self activate] is called - self->_state = [OmemoState new]; + //_state is intentionally left unset and will be updated from [xmpp readState] before [self activate] is called + //(but only if the state wasn't invalidated, in which case [self activate] will create a new empty state) + return self; +} + +-(void) activate +{ + if(self->_state == nil) + self->_state = [OmemoState new]; //read own devicelist from database self.ownDeviceList = [[self knownDevicesForAddressName:self.account.connectionProperties.identity.jid] mutableCopy]; @@ -62,11 +69,6 @@ -(MLOMEMO*) initWithAccount:(xmpp*) account; [self createLocalIdentiyKeyPairIfNeeded]; - return self; -} - --(void) activate -{ //init pubsub devicelist handler [self.account.pubsub registerForNode:@"eu.siacs.conversations.axolotl.devicelist" withHandler:$newHandler(self, devicelistHandler)]; @@ -103,26 +105,30 @@ -(BOOL) createLocalIdentiyKeyPairIfNeeded { if(self.monalSignalStore.deviceid == 0) { - // signal key helper + //signal key helper SignalKeyHelper* signalHelper = [[SignalKeyHelper alloc] initWithContext:self.signalContext]; - // Generate a new device id + //Generate a new device id do { self.monalSignalStore.deviceid = [signalHelper generateRegistrationId]; } while(self.monalSignalStore.deviceid == 0 || [self.ownDeviceList containsObject:[NSNumber numberWithUnsignedInt:self.monalSignalStore.deviceid]]); - // Create identity key pair + //Create identity key pair self.monalSignalStore.identityKeyPair = [signalHelper generateIdentityKeyPair]; self.monalSignalStore.signedPreKey = [signalHelper generateSignedPreKeyWithIdentity:self.monalSignalStore.identityKeyPair signedPreKeyId:1]; SignalAddress* address = [[SignalAddress alloc] initWithName:self.account.connectionProperties.identity.jid deviceId:self.monalSignalStore.deviceid]; [self.monalSignalStore saveIdentity:address identityKey:self.monalSignalStore.identityKeyPair.publicKey]; - // do everything done in MLSignalStore init not already mimicked above + //do everything done in MLSignalStore init not already mimicked above [self.monalSignalStore cleanupKeys]; [self.monalSignalStore reloadCachedPrekeys]; - // we generated a new identity + //we generated a new identity DDLogWarn(@"Created new omemo identity with deviceid: %@", @(self.monalSignalStore.deviceid)); + //don't alert on new deviceids we could never see before because this is our first connection (otherwise, we'd already have our own deviceid) + //this has to be a property of the xmpp class to persist it even across state resets + self.account.hasSeenOmemoDeviceListAfterOwnDeviceid = NO; return YES; } - // we did not generate a new identity + //we did not generate a new identity + //keep the value of hasSeenOmemoDeviceListAfterOwnDeviceid in this case return NO; } @@ -468,8 +474,9 @@ -(void) processOMEMODevices:(NSSet*) receivedDevices from:(NSString*) -(void) handleOwnDevicelistUpdate:(NSSet*) receivedDevices { - //check for new deviceids not previously known, but only if the devicelist is not empty - if([self.ownDeviceList count] > 0) + //check for new deviceids not previously known, but only if this isn't the first login we see a devicelist + //this has to be a property of the xmpp class to persist it even across state resets + if(self.account.hasSeenOmemoDeviceListAfterOwnDeviceid) { NSMutableSet* newDevices = [receivedDevices mutableCopy]; [newDevices minusSet:self.ownDeviceList]; @@ -493,6 +500,8 @@ -(void) handleOwnDevicelistUpdate:(NSSet*) receivedDevices //update own devicelist (this can be an empty list, if the list on our server is empty) self.ownDeviceList = [receivedDevices mutableCopy]; + //this has to be a property of the xmpp class to persist it even across state resets + self.account.hasSeenOmemoDeviceListAfterOwnDeviceid = YES; DDLogVerbose(@"Own devicelist for account %@ is now: %@", self.account, self.ownDeviceList); //make sure to add our own deviceid to the devicelist if it's not yet there diff --git a/Monal/Classes/xmpp.h b/Monal/Classes/xmpp.h index 49302cd42f..5aa9e8bbdc 100644 --- a/Monal/Classes/xmpp.h +++ b/Monal/Classes/xmpp.h @@ -86,6 +86,7 @@ typedef void (^monal_iq_handler_t)(XMPPIQ* _Nullable); @property (nonatomic, readonly) xmppState accountState; @property (nonatomic, readonly) BOOL reconnectInProgress; @property (nonatomic, readonly) BOOL isDoingFullReconnect; +@property (atomic, assign) BOOL hasSeenOmemoDeviceListAfterOwnDeviceid; // discovered properties @property (nonatomic, strong) NSArray* discoveredServersList; diff --git a/Monal/Classes/xmpp.m b/Monal/Classes/xmpp.m index dcf509759e..a36fec53b6 100644 --- a/Monal/Classes/xmpp.m +++ b/Monal/Classes/xmpp.m @@ -48,7 +48,7 @@ @import AVFoundation; -#define STATE_VERSION 16 +#define STATE_VERSION 17 #define CONNECT_TIMEOUT 7.0 #define IQ_TIMEOUT 60.0 NSString* const kQueueID = @"queueID"; @@ -197,7 +197,7 @@ -(id) initWithServer:(nonnull MLXMPPServer*) server andIdentity:(nonnull MLXMPPI //WARNING: pubsub node registrations should only be made *after* the first readState call [self readState]; - //register devicelist and notification handler (MUSt be done *after* reading state + //register devicelist and notification handler (MUST be done *after* reading state) //[self readState] needs a valid self.omemo to load omemo state, //but [self.omemo activate] needs a valid pubsub node registration loaded by [self readState] //--> split "init" method into "init" and "activate" methods @@ -3545,14 +3545,16 @@ -(void) realPersistState [values setValue:[NSDate date] forKey:@"stateSavedAt"]; [values setValue:@(STATE_VERSION) forKey:@"VERSION"]; - if(self.omemo != nil) + if(self.omemo != nil && self.omemo.state != nil) [values setObject:self.omemo.state forKey:@"omemoState"]; + [values setObject:[NSNumber numberWithBool:self.hasSeenOmemoDeviceListAfterOwnDeviceid] forKey:@"hasSeenOmemoDeviceListAfterOwnDeviceid"]; + //save state dictionary [[DataLayer sharedInstance] persistState:values forAccount:self.accountNo]; //debug output - DDLogVerbose(@"%@ --> persistState(saved at %@):\n\tisDoingFullReconnect=%@,\n\tlastHandledInboundStanza=%@,\n\tlastHandledOutboundStanza=%@,\n\tlastOutboundStanza=%@,\n\t#unAckedStanzas=%lu%s,\n\tstreamID=%@\n\tlastInteractionDate=%@\n\tpersistentIqHandlers=%@\n\tsupportsPush=%d\n\tsupportsHttpUpload=%d\n\tpushEnabled=%d\n\tsupportsPubSub=%d\n\tsupportsModernPubSub=%d\n\tsupportsPubSubMax=%d\n\tsupportsBlocking=%d\n\tsupportsClientState=%d\n\tsupportsBookmarksCompat=%d\n\taccountDiscoDone=%d\n\t_inCatchup=%@\n\tomemo.state=%@", + DDLogVerbose(@"%@ --> persistState(saved at %@):\n\tisDoingFullReconnect=%@,\n\tlastHandledInboundStanza=%@,\n\tlastHandledOutboundStanza=%@,\n\tlastOutboundStanza=%@,\n\t#unAckedStanzas=%lu%s,\n\tstreamID=%@\n\tlastInteractionDate=%@\n\tpersistentIqHandlers=%@\n\tsupportsPush=%d\n\tsupportsHttpUpload=%d\n\tpushEnabled=%d\n\tsupportsPubSub=%d\n\tsupportsModernPubSub=%d\n\tsupportsPubSubMax=%d\n\tsupportsBlocking=%d\n\tsupportsClientState=%d\n\tsupportsBookmarksCompat=%d\n\taccountDiscoDone=%d\n\t_inCatchup=%@\n\tomemo.state=%@\n\thasSeenOmemoDeviceListAfterOwnDeviceid=%@\n", self.accountNo, values[@"stateSavedAt"], bool2str(self.isDoingFullReconnect), @@ -3574,7 +3576,8 @@ -(void) realPersistState self.connectionProperties.supportsBookmarksCompat, self.connectionProperties.accountDiscoDone, self->_inCatchup, - self.omemo.state + self.omemo.state, + bool2str(self.hasSeenOmemoDeviceListAfterOwnDeviceid) ); DDLogVerbose(@"%@ --> realPersistState after: used/available memory: %.3fMiB / %.3fMiB)...", self.accountNo, [HelperTools report_memory], (CGFloat)os_proc_available_memory() / 1048576); } @@ -3596,10 +3599,15 @@ -(void) realReadState if(dic) { //check state version - if([dic[@"VERSION"] intValue] != STATE_VERSION) + int oldVersion = [dic[@"VERSION"] intValue]; + if(oldVersion != STATE_VERSION) { DDLogWarn(@"Account state upgraded from %@ to %d, invalidating state...", dic[@"VERSION"], STATE_VERSION); dic = [[self class] invalidateState:dic]; + + //don't show deviceid alerts on state update (if we need to regenerate our own deviceid, MLOMEMO will reset this to NO anyways) + if(oldVersion <= 16) + self.hasSeenOmemoDeviceListAfterOwnDeviceid = YES; } //collect smacks state @@ -3784,8 +3792,14 @@ -(void) realReadState if([dic objectForKey:@"omemoState"] && self.omemo) self.omemo.state = [dic objectForKey:@"omemoState"]; + if([dic objectForKey:@"hasSeenOmemoDeviceListAfterOwnDeviceid"]) + { + NSNumber* hasSeenOmemoDeviceListAfterOwnDeviceid = [dic objectForKey:@"hasSeenOmemoDeviceListAfterOwnDeviceid"]; + self.hasSeenOmemoDeviceListAfterOwnDeviceid = hasSeenOmemoDeviceListAfterOwnDeviceid.boolValue; + } + //debug output - DDLogVerbose(@"%@ --> readState(saved at %@):\n\tisDoingFullReconnect=%@,\n\tlastHandledInboundStanza=%@,\n\tlastHandledOutboundStanza=%@,\n\tlastOutboundStanza=%@,\n\t#unAckedStanzas=%lu%s,\n\tstreamID=%@,\n\tlastInteractionDate=%@\n\tpersistentIqHandlers=%@\n\tsupportsPush=%d\n\tsupportsHttpUpload=%d\n\tpushEnabled=%d\n\tsupportsPubSub=%d\n\tsupportsModernPubSub=%d\n\tsupportsPubSubMax=%d\n\tsupportsBlocking=%d\n\tsupportsClientSate=%d\n\tsupportsBookmarksCompat=%d\n\taccountDiscoDone=%d\n\t_inCatchup=%@\n\tomemo.state=%@", + DDLogVerbose(@"%@ --> readState(saved at %@):\n\tisDoingFullReconnect=%@,\n\tlastHandledInboundStanza=%@,\n\tlastHandledOutboundStanza=%@,\n\tlastOutboundStanza=%@,\n\t#unAckedStanzas=%lu%s,\n\tstreamID=%@,\n\tlastInteractionDate=%@\n\tpersistentIqHandlers=%@\n\tsupportsPush=%d\n\tsupportsHttpUpload=%d\n\tpushEnabled=%d\n\tsupportsPubSub=%d\n\tsupportsModernPubSub=%d\n\tsupportsPubSubMax=%d\n\tsupportsBlocking=%d\n\tsupportsClientSate=%d\n\tsupportsBookmarksCompat=%d\n\taccountDiscoDone=%d\n\t_inCatchup=%@\n\tomemo.state=%@\n\thasSeenOmemoDeviceListAfterOwnDeviceid=%@\n", self.accountNo, dic[@"stateSavedAt"], bool2str(self.isDoingFullReconnect), @@ -3807,7 +3821,8 @@ -(void) realReadState self.connectionProperties.supportsBookmarksCompat, self.connectionProperties.accountDiscoDone, self->_inCatchup, - self.omemo.state + self.omemo.state, + bool2str(self.hasSeenOmemoDeviceListAfterOwnDeviceid) ); if(self.unAckedStanzas) for(NSDictionary* dic in self.unAckedStanzas) @@ -3824,7 +3839,7 @@ -(void) realReadState +(NSMutableDictionary*) invalidateState:(NSDictionary*) dic { - NSArray* toKeep = @[@"lastHandledInboundStanza", @"lastHandledOutboundStanza", @"lastOutboundStanza", @"unAckedStanzas", @"loggedInOnce", @"lastInteractionDate", @"inCatchup"]; + NSArray* toKeep = @[@"lastHandledInboundStanza", @"lastHandledOutboundStanza", @"lastOutboundStanza", @"unAckedStanzas", @"loggedInOnce", @"lastInteractionDate", @"inCatchup", @"hasSeenOmemoDeviceListAfterOwnDeviceid"]; NSMutableDictionary* newState = [NSMutableDictionary new]; if(dic) From ea43a73e1087a376e4021df94a69366c4ac1fec7 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Thu, 6 Jun 2024 21:50:26 +0200 Subject: [PATCH 116/143] Don't show "unknown error" omemo errors on alpha --- Monal/Classes/MLOMEMO.m | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Monal/Classes/MLOMEMO.m b/Monal/Classes/MLOMEMO.m index ad8a7e912c..752696b827 100644 --- a/Monal/Classes/MLOMEMO.m +++ b/Monal/Classes/MLOMEMO.m @@ -1071,7 +1071,9 @@ -(void) addEncryptionKeyForAllDevices:(NSSet*) devices encryptForJid: SignalCiphertext* deviceEncryptedKey = [cipher encryptData:encryptedPayload.key error:&error]; if(error) { - showErrorOnAlpha(self.account, @"Error while adding encryption key for jid: %@ device: %@ error: %@", encryptForJid, device, error); + //only show errors not being of type "unknown error" + if(![error.domain isEqualToString:@"org.whispersystems.SignalProtocol"] || error.code != 0) + showErrorOnAlpha(self.account, @"Error while adding encryption key for jid: %@ device: %@ error: %@", encryptForJid, device, error); [self rebuildSessionWithJid:encryptForJid forRid:device]; continue; } From f605172988eb0b00baf8a7993294340527cc1b6d Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Thu, 6 Jun 2024 23:54:13 +0200 Subject: [PATCH 117/143] Implement inline safari versus default browser app setting This will be initialized to default browser in EU and inline safari otherwise. --- Monal/Classes/GeneralSettings.swift | 7 +++++++ Monal/Classes/MLChatCell.m | 5 ++++- Monal/Classes/MLLinkCell.m | 5 ++++- Monal/Classes/MLXMPPManager.m | 8 ++++++++ 4 files changed, 23 insertions(+), 2 deletions(-) diff --git a/Monal/Classes/GeneralSettings.swift b/Monal/Classes/GeneralSettings.swift index 838f3f9e5e..c8f183dcf4 100644 --- a/Monal/Classes/GeneralSettings.swift +++ b/Monal/Classes/GeneralSettings.swift @@ -46,6 +46,9 @@ class GeneralSettingsDefaultsDB: ObservableObject { @defaultsDB("ShowURLPreview") var showURLPreview: Bool + @defaultsDB("useInlineSafari") + var useInlineSafari: Bool + @defaultsDB("webrtcAllowP2P") var webrtcAllowP2P: Bool @@ -158,6 +161,10 @@ struct UserInterfaceSettings: View { Text("Show URL previews").font(.body) Text("The operator of the webserver providing that URL may see your IP address.").font(.footnote) } + Toggle(isOn: $generalSettingsDefaultsDB.useInlineSafari) { + Text("Open URLs inline in Safari").font(.body) + Text("When disabled, URLs will opened in your default browser (that might not be Safari).").font(.footnote) + } } Section(header: Text("Input")) { diff --git a/Monal/Classes/MLChatCell.m b/Monal/Classes/MLChatCell.m index 22abe614bd..ba14e10f57 100644 --- a/Monal/Classes/MLChatCell.m +++ b/Monal/Classes/MLChatCell.m @@ -9,6 +9,8 @@ #import "MLChatCell.h" #import "MLImageManager.h" #import "MLConstants.h" +#import "HelperTools.h" + @import SafariServices; @@ -47,7 +49,8 @@ -(void) openlink:(id) sender { if(self.link) { NSURL* url = [NSURL URLWithString:self.link]; - if([url.scheme isEqualToString:@"http"] || [url.scheme isEqualToString:@"https"]) + DDLogInfo(@"Opening link (inline=%@): %@", bool2str([[HelperTools defaultsDB] boolForKey: @"useInlineSafari"]), url); + if([[HelperTools defaultsDB] boolForKey: @"useInlineSafari"] && ([url.scheme.lowercaseString isEqualToString:@"http"] || [url.scheme.lowercaseString isEqualToString:@"https"])) { SFSafariViewController* safariView = [[SFSafariViewController alloc] initWithURL:url]; [self.parent presentViewController:safariView animated:YES completion:nil]; diff --git a/Monal/Classes/MLLinkCell.m b/Monal/Classes/MLLinkCell.m index d2bf62cb2c..8f00c9a1d9 100644 --- a/Monal/Classes/MLLinkCell.m +++ b/Monal/Classes/MLLinkCell.m @@ -9,6 +9,8 @@ #import "MLLinkCell.h" #import "UIImageView+WebCache.h" #import "MonalAppDelegate.h" +#import "HelperTools.h" + @import SafariServices; @@ -34,7 +36,8 @@ -(void) openlink: (id) sender { if(self.link) { NSURL* url = [NSURL URLWithString:self.link]; - if([url.scheme isEqualToString:@"http"] || [url.scheme isEqualToString:@"https"]) + DDLogInfo(@"Opening link (inline=%@): %@", bool2str([[HelperTools defaultsDB] boolForKey: @"useInlineSafari"]), url); + if([[HelperTools defaultsDB] boolForKey: @"useInlineSafari"] && ([url.scheme.lowercaseString isEqualToString:@"http"] || [url.scheme.lowercaseString isEqualToString:@"https"])) { SFSafariViewController *safariView = [[ SFSafariViewController alloc] initWithURL:url]; [self.parent presentViewController:safariView animated:YES completion:nil]; diff --git a/Monal/Classes/MLXMPPManager.m b/Monal/Classes/MLXMPPManager.m index 5c995a32be..f4095737a9 100644 --- a/Monal/Classes/MLXMPPManager.m +++ b/Monal/Classes/MLXMPPManager.m @@ -156,6 +156,14 @@ -(void) defaultSettings #else [self upgradeBoolUserSettingsIfUnset:@"useDnssecForAllConnections" toDefault:NO]; #endif + + + NSTimeZone* timeZone = [NSTimeZone localTimeZone]; + DDLogVerbose(@"Current timezone name: '%@'...", [timeZone name]); + if([[timeZone name] containsString:@"Europe"]) + [self upgradeBoolUserSettingsIfUnset:@"useInlineSafari" toDefault:NO]; + else + [self upgradeBoolUserSettingsIfUnset:@"useInlineSafari" toDefault:YES]; } -(void) upgradeFloatUserSettingsToInteger:(NSString*) settingsName From 706046888647a3cefd648bd0112a4260d70b770e Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Fri, 7 Jun 2024 00:34:33 +0200 Subject: [PATCH 118/143] Improve display of general settings --- Monal/Classes/GeneralSettings.swift | 160 +++++++++++++++------- Monal/Classes/NotificationDebugging.swift | 8 +- 2 files changed, 112 insertions(+), 56 deletions(-) diff --git a/Monal/Classes/GeneralSettings.swift b/Monal/Classes/GeneralSettings.swift index c8f183dcf4..8466de48ff 100644 --- a/Monal/Classes/GeneralSettings.swift +++ b/Monal/Classes/GeneralSettings.swift @@ -6,6 +6,40 @@ // Copyright © 2024 monal-im.org. All rights reserved. // +import ViewExtractor +struct SettingsToggle: View where T: View { + let value: Binding + let contents: T + + init(isOn value: Binding, @ViewBuilder contents: @escaping () -> T) { + self.value = value + self.contents = contents() + } + + var body:some View { + VStack(alignment: .leading, spacing: 0) { + Extract(contents) { views in + if views.count == 0 { + Text("") + } else { + Toggle(isOn: value) { + views[0] + .font(.body) + } + if views.count > 1 { + Group { + ForEach(views[1...]) { view in + view + .foregroundColor(Color(UIColor.secondaryLabel)) + .font(.footnote) + } + }.fixedSize(horizontal: false, vertical: true).frame(maxWidth: .infinity, alignment: .leading) + } + } + } + } + } +} func getNotificationPrivacyOption(_ option: NotificationPrivacySettingOption) -> String { switch option{ @@ -130,7 +164,7 @@ struct GeneralSettings: View { } } NavigationLink(destination: LazyClosureView(AttachmentSettings())) { - HStack{ + HStack { Image(systemName: "paperclip") .resizable() .aspectRatio(contentMode: .fit) @@ -138,6 +172,23 @@ struct GeneralSettings: View { Text("Attachments") } } + + Button(action: { + UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!) + }, label: { + HStack { + Image(systemName: "gear") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 20, height: 20) + #if targetEnvironment(macCatalyst) + Text("Open macOS settings") + #else + Text("Open iOS settings") + #endif + }.foregroundColor(Color(UIColor.label)) + }) + .buttonStyle(.borderless) } } .navigationBarTitle(Text("General Settings")) @@ -153,31 +204,36 @@ struct UserInterfaceSettings: View { var body: some View { Form { Section(header: Text("Previews")) { - Toggle(isOn: $generalSettingsDefaultsDB.showGeoLocation) { - Text("Show inline geo location").font(.body) - Text("Received geo locations are shared with Apple's Maps App.").font(.footnote) + SettingsToggle(isOn: $generalSettingsDefaultsDB.showGeoLocation) { + Text("Show inline geo location") + Text("Received geo locations are shared with Apple's Maps App.") } - Toggle(isOn: $generalSettingsDefaultsDB.showURLPreview) { - Text("Show URL previews").font(.body) - Text("The operator of the webserver providing that URL may see your IP address.").font(.footnote) + SettingsToggle(isOn: $generalSettingsDefaultsDB.showURLPreview) { + Text("Show URL previews") + Text("The operator of the webserver providing that URL may see your IP address.") } - Toggle(isOn: $generalSettingsDefaultsDB.useInlineSafari) { - Text("Open URLs inline in Safari").font(.body) - Text("When disabled, URLs will opened in your default browser (that might not be Safari).").font(.footnote) + SettingsToggle(isOn: $generalSettingsDefaultsDB.useInlineSafari) { + Text("Open URLs inline in Safari") + Text("When disabled, URLs will opened in your default browser (that might not be Safari).") } } Section(header: Text("Input")) { - Toggle(isOn: $generalSettingsDefaultsDB.showKeyboardOnChatOpen) { - Text("Autofocus text input on chat open").font(.body) - Text("Will focus the textfield on macOS or iOS with hardware keyboard attached, will open the software keyboard otherwise.").font(.footnote) + SettingsToggle(isOn: $generalSettingsDefaultsDB.showKeyboardOnChatOpen) { + Text("Autofocus text input on chat open") + Text("Will focus the textfield on macOS or iOS with hardware keyboard attached, will open the software keyboard otherwise.") } } Section(header: Text("Appearance")) { - NavigationLink(destination: LazyClosureView(BackgroundSettings(contact:nil))) { - Text("Chat background image").font(.body) - Text("Configure the background image displayed in open chats.").font(.footnote) + VStack(alignment: .leading, spacing: 0) { + NavigationLink(destination: LazyClosureView(BackgroundSettings(contact:nil))) { + Text("Chat background image").font(.body) + } + Text("Configure the background image displayed in open chats.") + .foregroundColor(Color(UIColor.secondaryLabel)) + .font(.footnote) + .fixedSize(horizontal: false, vertical: true) } } } @@ -191,14 +247,14 @@ struct SecuritySettings: View { var body: some View { Form { Section(header: Text("Encryption")) { - Toggle(isOn: $generalSettingsDefaultsDB.omemoDefaultOn) { - Text("Enable encryption by default for new chats").font(.body) - Text("Every new contact will have encryption enabled, but already known contacts will preserve their encryption settings.").font(.footnote) + SettingsToggle(isOn: $generalSettingsDefaultsDB.omemoDefaultOn) { + Text("Enable encryption by default for new chats") + Text("Every new contact will have encryption enabled, but already known contacts will preserve their encryption settings.") } if #available(iOS 16.0, macCatalyst 16.0, *) { - Toggle(isOn: $generalSettingsDefaultsDB.useDnssecForAllConnections) { - Text("Use DNSSEC validation for all connections").font(.body) + SettingsToggle(isOn: $generalSettingsDefaultsDB.useDnssecForAllConnections) { + Text("Use DNSSEC validation for all connections") Text( """ Use DNSSEC to validate all DNS query responses before connecting to the IP address designated \ @@ -206,18 +262,18 @@ in the DNS response.\n\ While being more secure, this can lead to connection problems in certain networks \ like hotel wifi, ugly mobile carriers etc. """ - ).font(.footnote) + ) } } - Toggle(isOn: $generalSettingsDefaultsDB.webrtcAllowP2P) { - Text("Calls: Allow P2P sessions").font(.body) - Text("Allow your device to establish a direct network connection to the remote party. This might leak your IP address to the caller/callee.").font(.footnote) + SettingsToggle(isOn: $generalSettingsDefaultsDB.webrtcAllowP2P) { + Text("Calls: Allow P2P sessions") + Text("Allow your device to establish a direct network connection to the remote party. This might leak your IP address to the caller/callee.") } } Section(header: Text("On this device")) { - Toggle(isOn: $generalSettingsDefaultsDB.autodeleteAllMessagesAfter3Days) { + SettingsToggle(isOn: $generalSettingsDefaultsDB.autodeleteAllMessagesAfter3Days) { Text("Autodelete all messages after 3 days") } } @@ -232,43 +288,43 @@ struct PrivacySettings: View { var body: some View { Form { Section(header: Text("Activity indications")) { - Toggle(isOn: $generalSettingsDefaultsDB.sendReceivedMarkers) { - Text("Send message received").font(.body) - Text("Let your contacts know if you received a message.").font(.footnote) + SettingsToggle(isOn: $generalSettingsDefaultsDB.sendReceivedMarkers) { + Text("Send message received") + Text("Let your contacts know if you received a message.") } - Toggle(isOn: $generalSettingsDefaultsDB.sendDisplayedMarkers) { - Text("Send message displayed state").font(.body) - Text("Let your contacts know if you read a message.").font(.footnote) + SettingsToggle(isOn: $generalSettingsDefaultsDB.sendDisplayedMarkers) { + Text("Send message displayed state") + Text("Let your contacts know if you read a message.") } - Toggle(isOn: $generalSettingsDefaultsDB.sendLastChatState) { - Text("Send typing notifications").font(.body) - Text("Let your contacts know if you are typing a message.").font(.footnote) + SettingsToggle(isOn: $generalSettingsDefaultsDB.sendLastChatState) { + Text("Send typing notifications") + Text("Let your contacts know if you are typing a message.") } - Toggle(isOn: $generalSettingsDefaultsDB.sendLastUserInteraction) { - Text("Send last interaction time").font(.body) - Text("Let your contacts know when you last opened the app.").font(.footnote) + SettingsToggle(isOn: $generalSettingsDefaultsDB.sendLastUserInteraction) { + Text("Send last interaction time") + Text("Let your contacts know when you last opened the app.") } } Section(header: Text("Interactions")) { - Toggle(isOn: $generalSettingsDefaultsDB.allowNonRosterContacts) { - Text("Accept incoming messages from strangers").font(.body) - Text("Allow contacts not in your contact list to contact you.").font(.footnote) + SettingsToggle(isOn: $generalSettingsDefaultsDB.allowNonRosterContacts) { + Text("Accept incoming messages from strangers") + Text("Allow contacts not in your contact list to contact you.") } - Toggle(isOn: $generalSettingsDefaultsDB.allowCallsFromNonRosterContacts) { - Text("Accept incoming calls from strangers").font(.body) - Text("Allow contacts not in your contact list to call you.").font(.footnote) + SettingsToggle(isOn: $generalSettingsDefaultsDB.allowCallsFromNonRosterContacts) { + Text("Accept incoming calls from strangers") + Text("Allow contacts not in your contact list to call you.") }.disabled(!generalSettingsDefaultsDB.allowNonRosterContacts) } Section(header: Text("Misc")) { - Toggle(isOn: $generalSettingsDefaultsDB.allowVersionIQ) { - Text("Publish version").font(.body) - Text("Allow contacts in your contact list to query your Monal and iOS versions.").font(.footnote) + SettingsToggle(isOn: $generalSettingsDefaultsDB.allowVersionIQ) { + Text("Publish version") + Text("Allow contacts in your contact list to query your Monal and iOS versions.") } - Toggle(isOn: $generalSettingsDefaultsDB.webrtcUseFallbackTurn) { - Text("Calls: Allow TURN fallback to Monal-Servers").font(.body) - Text("This will make calls possible even if your XMPP server does not provide a TURN server.").font(.footnote) + SettingsToggle(isOn: $generalSettingsDefaultsDB.webrtcUseFallbackTurn) { + Text("Calls: Allow TURN fallback to Monal-Servers") + Text("This will make calls possible even if your XMPP server does not provide a TURN server.") } } } @@ -292,7 +348,7 @@ struct NotificationSettings: View { var body: some View { Form { Section(header: Text("Settings")) { - Picker(selection: $generalSettingsDefaultsDB.notificationPrivacySetting, label: Text("Notification privacy")) { + Picker(selection: $generalSettingsDefaultsDB.notificationPrivacySetting, label: Text("Privacy")) { ForEach(NotificationPrivacySettingOption.allCases, id: \.self) { option in Text(getNotificationPrivacyOption(option)).tag(option.rawValue) } @@ -321,7 +377,7 @@ struct AttachmentSettings: View { var body: some View { Form { Section(header: Text("General File Transfer Settings")) { - Toggle(isOn: $generalSettingsDefaultsDB.autodownloadFiletransfers) { + SettingsToggle(isOn: $generalSettingsDefaultsDB.autodownloadFiletransfers) { Text("Auto-Download Media") } } diff --git a/Monal/Classes/NotificationDebugging.swift b/Monal/Classes/NotificationDebugging.swift index 859a7506ba..3dc7396466 100644 --- a/Monal/Classes/NotificationDebugging.swift +++ b/Monal/Classes/NotificationDebugging.swift @@ -27,7 +27,7 @@ struct NotificationDebugging: View { VStack(alignment: .leading) { buildNotificationStateLabel(Text("Apple Push Service"), isWorking: self.applePushEnabled); Divider() - Text("Apple push service should always be on. If it is off, your device can not talk to Apple's server.").font(.footnote) + Text("Apple push service should always be on. If it is off, your device can not talk to Apple's server.").foregroundColor(Color(UIColor.secondaryLabel)).font(.footnote) if !self.applePushEnabled, let apnsError = MLXMPPManager.sharedInstance().apnsError { Text("Error: \(String(describing:apnsError))").foregroundColor(.red).font(.footnote) } @@ -51,7 +51,7 @@ struct NotificationDebugging: View { VStack(alignment: .leading) { buildNotificationStateLabel(Text("Can Show Notifications"), isWorking: self.pushPermissionEnabled); Divider() - Text("If Monal can't show notifications, you will not see alerts when a message arrives. This happens if you tapped 'Decline' when Monal first asked permission. Fix it by going to iOS Settings -> Monal -> Notifications and select 'Allow Notifications'.").font(.footnote) + Text("If Monal can't show notifications, you will not see alerts when a message arrives. This happens if you tapped 'Decline' when Monal first asked permission. Fix it by going to iOS Settings -> Monal -> Notifications and select 'Allow Notifications'.").foregroundColor(Color(UIColor.secondaryLabel)).font(.footnote) } } if(self.xmppAccountInfo.count > 0) { @@ -61,12 +61,12 @@ struct NotificationDebugging: View { buildNotificationStateLabel(Text(account.connectionProperties.identity.jid), isWorking: account.connectionProperties.pushEnabled) Divider() } - Text("If this is off your device could not activate push on your xmpp server, make sure to have configured it to support XEP-0357.").font(.footnote) + Text("If this is off your device could not activate push on your xmpp server, make sure to have configured it to support XEP-0357.").foregroundColor(Color(UIColor.secondaryLabel)).font(.footnote) } } } else { Section { - Text("No accounts set up currently").font(.footnote) + Text("No accounts set up currently").foregroundColor(Color(UIColor.secondaryLabel)).font(.footnote) }.opacity(0.5) } } From 3f2a03d591a3599b1c76f1ff67bdd32d580fed15 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Sun, 9 Jun 2024 05:43:34 +0200 Subject: [PATCH 119/143] Add FrameUp lib written by Ryan Lintott --- Monal/Monal.xcodeproj/project.pbxproj | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/Monal/Monal.xcodeproj/project.pbxproj b/Monal/Monal.xcodeproj/project.pbxproj index 043e4a2d4e..ef8825afb7 100644 --- a/Monal/Monal.xcodeproj/project.pbxproj +++ b/Monal/Monal.xcodeproj/project.pbxproj @@ -163,6 +163,7 @@ 84E55E7D2964424E003E191A /* ActiveChatsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 261A6284176C156500059090 /* ActiveChatsViewController.m */; }; 84E55E8029644279003E191A /* ActiveChatsViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = 84E55E7F2964426D003E191A /* ActiveChatsViewController.h */; }; 84F194CE2C101A3E00F0A994 /* PromiseKit in Frameworks */ = {isa = PBXBuildFile; productRef = 84F194CD2C101A3E00F0A994 /* PromiseKit */; }; + 84F194D12C15197200F0A994 /* FrameUp in Frameworks */ = {isa = PBXBuildFile; productRef = 84F194D02C15197200F0A994 /* FrameUp */; }; 84FC37552897521500634E3E /* snprintf.m in Sources */ = {isa = PBXBuildFile; fileRef = 84FC37542897521400634E3E /* snprintf.m */; }; 84FC37572897523500634E3E /* metamacros.h in Headers */ = {isa = PBXBuildFile; fileRef = 84FC37562897523500634E3E /* metamacros.h */; }; 84FC375928981A5600634E3E /* PasswordMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84FC375828981A5600634E3E /* PasswordMigration.swift */; }; @@ -772,6 +773,7 @@ files = ( 261E542523A0A1D300394F59 /* monalxmpp.framework in Frameworks */, C1E1EC7B286A025F0097EC74 /* SwiftSoup in Frameworks */, + 84F194D12C15197200F0A994 /* FrameUp in Frameworks */, C176F1EC2AF11C31002034E5 /* UserNotifications.framework in Frameworks */, C1F5C7AF2777638B0001F295 /* OrderedCollections in Frameworks */, 841898AA2957712000FEC77D /* ViewExtractor in Frameworks */, @@ -1524,6 +1526,7 @@ C1F5C7AE2777638B0001F295 /* OrderedCollections */, C1E1EC7A286A025F0097EC74 /* SwiftSoup */, 841898A92957712000FEC77D /* ViewExtractor */, + 84F194D02C15197200F0A994 /* FrameUp */, ); productName = SworIM; productReference = 26080210110ABA4E005E194D /* Monal.app */; @@ -1740,6 +1743,7 @@ 841898A82957712000FEC77D /* XCRemoteSwiftPackageReference "ViewExtractor" */, 849ADF3D2BACF0360009BCD7 /* XCRemoteSwiftPackageReference "cocoalumberjack" */, 84F194CC2C101A3E00F0A994 /* XCRemoteSwiftPackageReference "PromiseKit" */, + 84F194CF2C15197200F0A994 /* XCRemoteSwiftPackageReference "FrameUp" */, ); productRefGroup = 19C28FACFE9D520D11CA2CBB /* Products */; projectDirPath = ""; @@ -4634,6 +4638,14 @@ minimumVersion = 8.1.2; }; }; + 84F194CF2C15197200F0A994 /* XCRemoteSwiftPackageReference "FrameUp" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/ryanlintott/FrameUp"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 0.8.0; + }; + }; C1E1EC79286A025F0097EC74 /* XCRemoteSwiftPackageReference "SwiftSoup" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/scinfu/SwiftSoup.git"; @@ -4682,6 +4694,11 @@ package = 84F194CC2C101A3E00F0A994 /* XCRemoteSwiftPackageReference "PromiseKit" */; productName = PromiseKit; }; + 84F194D02C15197200F0A994 /* FrameUp */ = { + isa = XCSwiftPackageProductDependency; + package = 84F194CF2C15197200F0A994 /* XCRemoteSwiftPackageReference "FrameUp" */; + productName = FrameUp; + }; C1E1EC7A286A025F0097EC74 /* SwiftSoup */ = { isa = XCSwiftPackageProductDependency; package = C1E1EC79286A025F0097EC74 /* XCRemoteSwiftPackageReference "SwiftSoup" */; From 56e3f5db850574300fd2fb94c43ece6e9563c3d5 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Sun, 9 Jun 2024 05:43:59 +0200 Subject: [PATCH 120/143] Fix display and accessibility of group members --- Monal/Classes/ContactEntry.swift | 19 +- Monal/Classes/MemberList.swift | 349 ++++++++++++++++++++--------- Monal/Classes/SwiftuiHelpers.swift | 35 ++- 3 files changed, 289 insertions(+), 114 deletions(-) diff --git a/Monal/Classes/ContactEntry.swift b/Monal/Classes/ContactEntry.swift index 4171715a22..ce3ccea0c9 100644 --- a/Monal/Classes/ContactEntry.swift +++ b/Monal/Classes/ContactEntry.swift @@ -6,13 +6,23 @@ // Copyright © 2023 monal-im.org. All rights reserved. // -struct ContactEntry: View { +struct ContactEntry: View { let contact: ObservableKVOWrapper let selfnotesPrefix: Bool + @ViewBuilder let additionalContent: () -> AdditionalContent - init(contact:ObservableKVOWrapper, selfnotesPrefix: Bool = true) { + init(contact:ObservableKVOWrapper, selfnotesPrefix: Bool = true) where AdditionalContent == EmptyView { + self.init(contact:contact, selfnotesPrefix:selfnotesPrefix, additionalContent:{ EmptyView() }) + } + + init(contact:ObservableKVOWrapper, @ViewBuilder additionalContent: @escaping () -> AdditionalContent) { + self.init(contact:contact, selfnotesPrefix:true, additionalContent:additionalContent) + } + + init(contact:ObservableKVOWrapper, selfnotesPrefix: Bool, @ViewBuilder additionalContent: @escaping () -> AdditionalContent) { self.contact = contact self.selfnotesPrefix = selfnotesPrefix + self.additionalContent = additionalContent } var body:some View { @@ -27,7 +37,10 @@ struct ContactEntry: View { } else { Text(contact.contactDisplayNameWithoutSelfnotesPrefix as String) } - Text(contact.contactJid as String).font(.footnote).opacity(0.6) + additionalContent() + Text(contact.contactJid as String) + .foregroundColor(Color(UIColor.secondaryLabel)) + .font(.footnote) } } } diff --git a/Monal/Classes/MemberList.swift b/Monal/Classes/MemberList.swift index 669e2559f1..00d5b571ef 100644 --- a/Monal/Classes/MemberList.swift +++ b/Monal/Classes/MemberList.swift @@ -100,26 +100,26 @@ struct MemberList: View { } if self.muc.mucType == "group" { if ownAffiliation == "owner" { - return ["profile"] + reinviteEntry + ["owner", "admin", "member", "outcast"] + return [/*"profile"*/] + reinviteEntry + ["owner", "admin", "member", "outcast"] } else { //only admin left, because other affiliations don't call actionsAllowed at all if ["member", "outcast"].contains(contactAffiliation) { - return ["profile"] + reinviteEntry + ["member", "outcast"] + return [/*"profile"*/] + reinviteEntry + ["member", "outcast"] } else { //if this contact is a co-admin or owner, we aren't allowed to do much to their affiliation //return contact affiliation because that should be displayed as selected in picker - return ["profile"] + reinviteEntry + [contactAffiliation] + return [/*"profile"*/] + reinviteEntry + [contactAffiliation] } } } else { if ownAffiliation == "owner" { - return ["profile"] + reinviteEntry + ["owner", "admin", "member", "none", "outcast"] + return [/*"profile"*/] + reinviteEntry + ["owner", "admin", "member", "none", "outcast"] } else { //only admin left, because other affiliations don't call actionsAllowed at all if ["member", "none", "outcast"].contains(contactAffiliation) { - return ["profile"] + reinviteEntry + ["member", "none", "outcast"] + return [/*"profile"*/] + reinviteEntry + ["member", "none", "outcast"] } else { //if this contact is a co-admin or owner, we aren't allowed to do much to their affiliation //return contact affiliation because that should be displayed as selected in picker - return ["profile"] + reinviteEntry + [contactAffiliation] + return [/*"profile"*/] + reinviteEntry + [contactAffiliation] } } } @@ -127,11 +127,74 @@ struct MemberList: View { //fallback (should hopefully never be needed) DDLogWarn("Fallback for group/channel \(String(describing:self.muc.contactJid as String)): affiliation=\(String(describing:affiliations[contact])), online=\(String(describing:online[contact]))") if self.muc.mucType == "group" { - return ["profile"] + return [/*"profile",*/ "reinvite"] } else { - return ["profile", "reinvite", "none"] + return [/*"profile",*/ "reinvite", "none"] } } + + @ViewBuilder + func makePickerView(contact: ObservableKVOWrapper) -> some View { + Picker(selection: Binding( + get: { affiliations[contact] ?? "none" }, + set: { newAffiliation in + if newAffiliation == affiliations[contact] { + return + } + if newAffiliation == "profile" { + DDLogVerbose("Activating navigation to \(String(describing:contact))") + navigationActive = contact + } else if newAffiliation == "reinvite" { + //first remove potential ban, then reinvite + (affiliations[contact] == "outcast" ? + performAction(headlineView: Text("Unblocking user"), descriptionView: Text("Unblocking user for this group/channel: \(contact.contactJid as String)")) { + account.mucProcessor.setAffiliation(self.muc.mucType == "group" ? "member" : "none", ofUser:contact.contactJid, inMuc:self.muc.contactJid) + } : + Promise.value(nil) + ).then { _ in + return performAction(headlineView: Text("Inviting user"), descriptionView: Text("Inviting user to this group/channel: \(contact.contactJid as String)")) { + DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 1.0) { + account.mucProcessor.inviteUser(contact.contactJid, inMuc: self.muc.contactJid) + } + } + .recover { error in + showAlert(title:Text("Error inviting user!"), description:Text("\(String(describing:error))")) + return Guarantee.value(nil as monal_void_block_t?) + } + }.catch { error in + showAlert(title:Text("Error unblocking user!"), description:Text("\(String(describing:error))")) + }.finally { + hideLoadingOverlay(overlay) + } + } else if newAffiliation == "outcast" { + showActionSheet(title: Text("Block user?"), description: Text("Do you want to block this user from entering this group/channel?")) { + DDLogVerbose("Changing affiliation of \(String(describing:contact)) to: \(String(describing:newAffiliation))...") + performAction(headlineView: Text("Blocking member"), descriptionView: Text("Blocking \(contact.contactJid as String)")) { + account.mucProcessor.setAffiliation(newAffiliation, ofUser:contact.contactJid, inMuc:self.muc.contactJid) + }.catch { error in + showAlert(title:Text("Error blocking user!"), description:Text("\(String(describing:error))")) + }.finally { + hideLoadingOverlay(overlay) + } + } + } else { + DDLogVerbose("Changing affiliation of \(String(describing:contact)) to: \(String(describing:newAffiliation))...") + performAction(headlineView: Text("Changing affiliation"), descriptionView: + Text("Changing affiliation to \(mucAffiliationToString(affiliations[contact])): \(contact.contactJid as String)")) { + account.mucProcessor.setAffiliation(newAffiliation, ofUser:contact.contactJid, inMuc:self.muc.contactJid) + }.catch { error in + showAlert(title:Text("Error changing affiliation!"), description:Text("\(String(describing:error))")) + }.finally { + hideLoadingOverlay(overlay) + } + } + } + ), label: EmptyView()) { + ForEach(actionsAllowed(for:contact), id:\.self) { affiliation in + Text(mucAffiliationToString(affiliation)).tag(affiliation) + } + }.collapsedPickerStyle(accessibilityLabel: Text("Change affiliation")) + } var body: some View { List { @@ -173,109 +236,47 @@ struct MemberList: View { Text("Invite participants to channel") } } - - ForEach(memberList, id:\.self) { contact in - if !contact.isSelfChat { - HStack(alignment: .center) { - ContactEntry(contact:contact) - Spacer() - Picker(selection: Binding( - get: { affiliations[contact] ?? "none" }, - set: { newAffiliation in - if newAffiliation == affiliations[contact] { - return - } - if newAffiliation == "profile" { - DDLogVerbose("Activating navigation to \(String(describing:contact))") - navigationActive = contact - } else if newAffiliation == "reinvite" { - //first remove potential ban, then reinvite - (affiliations[contact] == "outcast" ? - performAction(headlineView: Text("Unblocking user"), descriptionView: Text("Unblocking user for this group/channel: \(contact.contactJid as String)")) { - account.mucProcessor.setAffiliation(self.muc.mucType == "group" ? "member" : "none", ofUser:contact.contactJid, inMuc:self.muc.contactJid) - } : - Promise.value(nil) - ).then { _ in - return performAction(headlineView: Text("Inviting user"), descriptionView: Text("Inviting user to this group/channel: \(contact.contactJid as String)")) { - DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 1.0) { - account.mucProcessor.inviteUser(contact.contactJid, inMuc: self.muc.contactJid) - } - } - .recover { error in - showAlert(title:Text("Error inviting user!"), description:Text("\(String(describing:error))")) - return Guarantee.value(nil as monal_void_block_t?) - } - }.catch { error in - showAlert(title:Text("Error unblocking user!"), description:Text("\(String(describing:error))")) - }.finally { - hideLoadingOverlay(overlay) - } - } else if newAffiliation == "outcast" { - showActionSheet(title: Text("Block user?"), description: Text("Do you want to block this user from entering this group/channel?")) { - DDLogVerbose("Changing affiliation of \(String(describing:contact)) to: \(String(describing:newAffiliation))...") - performAction(headlineView: Text("Blocking member"), descriptionView: Text("Blocking \(contact.contactJid as String)")) { - account.mucProcessor.setAffiliation(newAffiliation, ofUser:contact.contactJid, inMuc:self.muc.contactJid) - }.catch { error in - showAlert(title:Text("Error blocking user!"), description:Text("\(String(describing:error))")) - }.finally { - hideLoadingOverlay(overlay) - } - } - } else { - DDLogVerbose("Changing affiliation of \(String(describing:contact)) to: \(String(describing:newAffiliation))...") - performAction(headlineView: Text("Changing affiliation"), descriptionView: - Text("Changing \(contact.contactJid as String) to ") + Text(mucAffiliationToString(newAffiliation))) { - account.mucProcessor.setAffiliation(newAffiliation, ofUser:contact.contactJid, inMuc:self.muc.contactJid) - }.catch { error in - showAlert(title:Text("Error changing affiliation!"), description:Text("\(String(describing:error))")) - }.finally { - hideLoadingOverlay(overlay) - } - } - } - ), label: EmptyView()) { - ForEach(actionsAllowed(for:contact), id:\.self) { affiliation in - Text(mucAffiliationToString(affiliation)).tag(affiliation) - } + } + + ForEach(memberList, id:\.self) { contact in + if !contact.isSelfChat { + HStack { + HStack { + ContactEntry(contact:contact) { + Text("Affiliation: \(mucAffiliationToString(affiliations[contact]))") + //.foregroundColor(Color(UIColor.secondaryLabel)) + .font(.footnote) } - .pickerStyle(.menu) - //invisible navigation link triggered programmatically - .background( - NavigationLink(destination: LazyClosureView(ContactDetails(delegate:SheetDismisserProtocol(), contact:contact)), tag:contact, selection:$navigationActive) { EmptyView() } - .opacity(0) - ) + Spacer() } - .deleteDisabled( - !ownUserHasAffiliationToRemove(contact: contact) + .accessibilityLabel(Text("Open Profile of \(contact.contactDisplayName as String)")) + //invisible navigation link that can be triggered programmatically + .background( + NavigationLink(destination: LazyClosureView(ContactDetails(delegate:SheetDismisserProtocol(), contact:contact)), tag:contact, selection:$navigationActive) { EmptyView() } + .opacity(0) ) - } - } - .onDelete(perform: { memberIdx in - let member = memberList[memberIdx.first!] - showActionSheet(title: Text("Remove user?"), description: self.muc.mucType == "group" ? Text("Do you want to remove this user from this group? The user won't be able to enter it again until added back to the group.") : Text("Do you want to remove this user from this channel? The user will be able to enter it again.")) { - performAction(headlineView: Text("Removing member"), descriptionView: Text("Removing \(member.contactJid as String)...")) { - account.mucProcessor.setAffiliation("none", ofUser: member.contactJid, inMuc: self.muc.contactJid) - }.catch { error in - showAlert(title:Text("Error removing user!"), description:Text("\(String(describing:error))")) - }.finally { - hideLoadingOverlay(overlay) - } - } - }) - } else { - ForEach(memberList, id:\.self) { contact in - if !contact.isSelfChat { - NavigationLink(destination: LazyClosureView(ContactDetails(delegate:SheetDismisserProtocol(), contact:contact))) { - HStack(alignment: .center) { - ContactEntry(contact:contact) - Spacer() - Text(mucAffiliationToString(affiliations[contact])) - } + + if ownAffiliation == "owner" || ownAffiliation == "admin" { + makePickerView(contact:contact) + .fixedSize() + .offset(x:8, y:0) } - .deleteDisabled(true) } + .deleteDisabled(!ownUserHasAffiliationToRemove(contact: contact)) } } + .onDelete(perform: { memberIdx in + let member = memberList[memberIdx.first!] + showActionSheet(title: Text("Remove \(mucAffiliationToString(affiliations[member]))?"), description: self.muc.mucType == "group" ? Text("Do you want to remove that user from this group? That user won't be able to enter it again until added back to the group.") : Text("Do you want to remove that user from this channel? That user will be able to enter it again if you don't block them.")) { + performAction(headlineView: Text("Removing \(mucAffiliationToString(affiliations[member]))"), descriptionView: Text("Removing \(member.contactJid as String)...")) { + account.mucProcessor.setAffiliation("none", ofUser: member.contactJid, inMuc: self.muc.contactJid) + }.catch { error in + showAlert(title:Text("Error removing user!"), description:Text("\(String(describing:error))")) + }.finally { + hideLoadingOverlay(overlay) + } + } + }) } } .actionSheet(isPresented: $showActionSheet) { @@ -308,6 +309,146 @@ struct MemberList: View { } } } + +// var bodyy: some View { +// List { +// Section(header: Text("\(self.muc.contactDisplayName as String) (affiliation: \(mucAffiliationToString(ownAffiliation)))")) { +// if ownAffiliation == "owner" || ownAffiliation == "admin" { +// NavigationLink(destination: LazyClosureView(ContactPicker(account, initializeFrom: memberList, allowRemoval: false) { newMemberList in +// for member in newMemberList { +// if !memberList.contains(member) { +// if self.muc.mucType == "group" { +// performAction(headlineView: Text("Adding new member"), descriptionView: Text("Adding \(member.contactJid as String)...")) { +// account.mucProcessor.setAffiliation("member", ofUser:member.contactJid, inMuc:self.muc.contactJid) +// }.then { _ in +// return performAction(headlineView: Text("Inviting new member"), descriptionView: Text("Adding \(member.contactJid as String)...")) { +// account.mucProcessor.inviteUser(member.contactJid, inMuc: self.muc.contactJid) +// }.recover { error in +// showAlert(title:Text("Error inviting new member!"), description:Text("\(String(describing:error))")) +// return Guarantee.value(nil as monal_void_block_t?) +// } +// }.catch { error in +// showAlert(title:Text("Error adding new member!"), description:Text("\(String(describing:error))")) +// }.finally { +// hideLoadingOverlay(overlay) +// } +// } else { +// performAction(headlineView: Text("Inviting new participant"), descriptionView: Text("Adding \(member.contactJid as String)...")) { +// account.mucProcessor.inviteUser(member.contactJid, inMuc: self.muc.contactJid) +// }.catch { error in +// showAlert(title:Text("Error inviting new participant!"), description:Text("\(String(describing:error))")) +// }.finally { +// hideLoadingOverlay(overlay) +// } +// } +// } +// } +// })) { +// if self.muc.mucType == "group" { +// Text("Add members to group") +// } else { +// Text("Invite participants to channel") +// } +// } +// +// ForEach(memberList, id:\.self) { contact in +// if !contact.isSelfChat { +// if #available(iOS 16, *) { +// Group { +// // ContactEntry(contact:contact) +// Text(contact.contactDisplayName as String) +// Text(contact.contactJid as String) +// +// } +// } else { +// VStack { +// ContactEntry(contact:contact) +// makePickerView(contact:contact, labelText:Text("Affiliation:")) +// .fixedSize() +// .applyClosure { view in +// if #available(iOS 15, *) { +// view.border(.red) +// } else { +// view +// } +// } +// } +// } +// } +// +// //invisible navigation link triggered programmatically +// // .background( +// // NavigationLink(destination: LazyClosureView(ContactDetails(delegate:SheetDismisserProtocol(), contact:contact)), tag:contact, selection:$navigationActive) { EmptyView() } +// // .opacity(0) +// // ) +// +// .deleteDisabled(!ownUserHasAffiliationToRemove(contact: contact)) +// } +// .onDelete(perform: { memberIdx in +// let member = memberList[memberIdx.first!] +// showActionSheet(title: Text("Remove user?"), description: self.muc.mucType == "group" ? Text("Do you want to remove this user from this group? The user won't be able to enter it again until added back to the group.") : Text("Do you want to remove this user from this channel? The user will be able to enter it again.")) { +// performAction(headlineView: Text("Removing member"), descriptionView: Text("Removing \(member.contactJid as String)...")) { +// account.mucProcessor.setAffiliation("none", ofUser: member.contactJid, inMuc: self.muc.contactJid) +// }.catch { error in +// showAlert(title:Text("Error removing user!"), description:Text("\(String(describing:error))")) +// }.finally { +// hideLoadingOverlay(overlay) +// } +// } +// }) +// } else { +// ForEach(memberList, id:\.self) { contact in +// if !contact.isSelfChat { +// NavigationLink(destination: LazyClosureView(ContactDetails(delegate:SheetDismisserProtocol(), contact:contact))) { +// HStack(alignment: .center) { +// ContactEntry(contact:contact) +// Spacer() +// Text(mucAffiliationToString(affiliations[contact])) +// } +// } +// .deleteDisabled(true) +// } +// } +// } +// } +// } +// .actionSheet(isPresented: $showActionSheet) { +// ActionSheet( +// title: actionSheetPrompt.title, +// message: actionSheetPrompt.message, +// buttons: [ +// .cancel(), +// .destructive( +// Text("Yes"), +// action: actionSheetPrompt.closure +// ) +// ] +// ) +// } +// .alert(isPresented: $showAlert, content: { +// Alert(title: alertPrompt.title, message: alertPrompt.message, dismissButton: .default(alertPrompt.dismissLabel)) +// }) +// .addLoadingOverlay(overlay) +// .navigationBarTitle(Text("Group Members"), displayMode: .inline) +// .onAppear { +// updateMemberlist() +// } +// .onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("kMonalMucParticipantsAndMembersUpdated")).receive(on: RunLoop.main)) { notification in +// if let xmppAccount = notification.object as? xmpp, let contact = notification.userInfo?["contact"] as? MLContact { +// DDLogVerbose("Got muc participants/members update from account \(xmppAccount)...") +// if contact == self.muc { +// updateMemberlist() +// } +// } +// } +// } +} + +extension UIPickerView { + override open func didMoveToSuperview() { + super.didMoveToSuperview() + self.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + } } struct MemberList_Previews: PreviewProvider { diff --git a/Monal/Classes/SwiftuiHelpers.swift b/Monal/Classes/SwiftuiHelpers.swift index 1234e2fb71..be8a03e1c0 100644 --- a/Monal/Classes/SwiftuiHelpers.swift +++ b/Monal/Classes/SwiftuiHelpers.swift @@ -88,13 +88,6 @@ func getContactList(viewContact: (ObservableKVOWrapper?)) -> OrderedS } } -extension UIPickerView { - override open func didMoveToSuperview() { - super.didMoveToSuperview() - self.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) - } -} - func performMucAction(account: xmpp, mucJid: String, overlay: LoadingOverlayState, headlineView: Optional, descriptionView: Optional, action: @escaping ()->Void) -> Promise { showLoadingOverlay(overlay, headlineView:headlineView, descriptionView:descriptionView) return Promise { seal in @@ -137,6 +130,34 @@ func mucAffiliationToString(_ affiliation: String?) -> String { return NSLocalizedString("", comment:"muc affiliation") } +struct CollapsedPickerStyle: ViewModifier { + let accessibilityLabel: Text + func body(content: Content) -> some View { + Menu { + content + } label: { + Button(action: { }) { + HStack { + Spacer().frame(width:8) + Image(systemName: "ellipsis") + .rotationEffect(.degrees(90)) + .foregroundColor(.primary) + Spacer().frame(width:8) + } + .contentShape(Rectangle()) + } + .frame(width: 24, height: 20) + .accessibilityLabel(accessibilityLabel) + } + } + +} +extension View { + func collapsedPickerStyle(accessibilityLabel label: Text) -> some View { + self.modifier(CollapsedPickerStyle(accessibilityLabel:label)) + } +} + struct TopRight: ViewModifier { let overlay: T public func body(content: Content) -> some View { From 21cda8b68c3de8d435f092552a7e6812cc078043 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Sun, 9 Jun 2024 05:46:59 +0200 Subject: [PATCH 121/143] Add bigger margin around loading overlay bubble to look better --- Monal/Classes/LoadingOverlay.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Monal/Classes/LoadingOverlay.swift b/Monal/Classes/LoadingOverlay.swift index 97387f815a..89fbbbe801 100644 --- a/Monal/Classes/LoadingOverlay.swift +++ b/Monal/Classes/LoadingOverlay.swift @@ -35,7 +35,7 @@ struct LoadingOverlay: ViewModifier { state.description.font(.footnote) ProgressView() } - .padding(12) + .padding(20) .frame(minWidth: 250, minHeight: 100) .background(Color.secondary.colorInvert()) .cornerRadius(20) From f194673185a35fc1d41d39042bb8b37b5756248a Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Sun, 9 Jun 2024 06:52:36 +0200 Subject: [PATCH 122/143] Oder muc members list by online status and affiliation --- Monal/Classes/ChannelMemberList.swift | 9 ++++++--- Monal/Classes/DataLayer.m | 4 ++-- Monal/Classes/MemberList.swift | 25 ++++++++++++++++++++++++- Monal/Classes/SwiftuiHelpers.swift | 21 +++++++++++++++++++++ 4 files changed, 53 insertions(+), 6 deletions(-) diff --git a/Monal/Classes/ChannelMemberList.swift b/Monal/Classes/ChannelMemberList.swift index 42ddf06f1e..f303bb6c7b 100644 --- a/Monal/Classes/ChannelMemberList.swift +++ b/Monal/Classes/ChannelMemberList.swift @@ -35,18 +35,21 @@ struct ChannelMemberList: View { participants[nick] = memberInfo["affiliation"] as? String ?? "none" } } + participants.sort { + (mucAffiliationToInt($0.value), $0.key) < (mucAffiliationToInt($1.value), $1.key) + } } var body: some View { List { Section(header: Text("\(self.channel.contactDisplayName as String) (affiliation: \(mucAffiliationToString(ownAffiliation)))")) { - ForEach(participants.sorted(by: <), id: \.self.key) { participant in + ForEach(participants.keys, id: \.self) { participant_key in ZStack(alignment: .topLeading) { HStack(alignment: .center) { - Text(participant.key) + Text(participant_key) Spacer() - Text(mucAffiliationToString(participant.value)) + Text(mucAffiliationToString(participants[participant_key])) } } } diff --git a/Monal/Classes/DataLayer.m b/Monal/Classes/DataLayer.m index 1effe8c0e5..e22fb7deef 100644 --- a/Monal/Classes/DataLayer.m +++ b/Monal/Classes/DataLayer.m @@ -1024,8 +1024,8 @@ -(NSDictionary* _Nullable) getParticipantForNick:(NSString*) nick inRoom:(NSStri return [self.db idReadTransaction:^{ NSMutableArray*>* toReturn = [[NSMutableArray*> alloc] init]; - [toReturn addObjectsFromArray:[self.db executeReader:@"SELECT *, 1 as 'online' FROM muc_participants WHERE account_id=? AND room=?;" andArguments:@[accountNo, room]]]; - [toReturn addObjectsFromArray:[self.db executeReader:@"SELECT *, 0 as 'online' FROM muc_members WHERE account_id=? AND room=? AND NOT EXISTS(SELECT * FROM muc_participants WHERE muc_members.account_id=muc_participants.account_id AND muc_members.room=muc_participants.room AND muc_members.member_jid=muc_participants.participant_jid);" andArguments:@[accountNo, room]]]; + [toReturn addObjectsFromArray:[self.db executeReader:@"SELECT *, 1 as 'online' FROM muc_participants WHERE account_id=? AND room=? ORDER BY affiliation, room_nick;" andArguments:@[accountNo, room]]]; + [toReturn addObjectsFromArray:[self.db executeReader:@"SELECT *, 0 as 'online' FROM muc_members WHERE account_id=? AND room=? AND NOT EXISTS(SELECT * FROM muc_participants WHERE muc_members.account_id=muc_participants.account_id AND muc_members.room=muc_participants.room AND muc_members.member_jid=muc_participants.participant_jid) ORDER BY affiliation;" andArguments:@[accountNo, room]]]; return toReturn; }]; diff --git a/Monal/Classes/MemberList.swift b/Monal/Classes/MemberList.swift index 00d5b571ef..1979a6ab8d 100644 --- a/Monal/Classes/MemberList.swift +++ b/Monal/Classes/MemberList.swift @@ -48,6 +48,9 @@ struct MemberList: View { continue } let contact = ObservableKVOWrapper(MLContact.createContact(fromJid:jid, andAccountNo:account.accountNo)) + if !memberList.contains(contact) { + continue + } affiliations[contact] = memberInfo["affiliation"] as? String ?? "none" if let num = memberInfo["online"] as? NSNumber { online[contact] = num.boolValue @@ -55,6 +58,19 @@ struct MemberList: View { online[contact] = false } } + memberList.sort { + ( + (online[$0]! ? 0 : 1), + mucAffiliationToInt(affiliations[$0]), + ($0.contactDisplayNameWithoutSelfnotesPrefix as String), + ($0.contactJid as String) + ) < ( + (online[$1]! ? 0 : 1), + mucAffiliationToInt(affiliations[$1]), + ($1.contactDisplayNameWithoutSelfnotesPrefix as String), + ($1.contactJid as String) + ) + } } func performAction(headlineView: some View, descriptionView: some View, action: @escaping ()->Void) -> Promise { @@ -243,7 +259,7 @@ struct MemberList: View { HStack { HStack { ContactEntry(contact:contact) { - Text("Affiliation: \(mucAffiliationToString(affiliations[contact]))") + Text("Affiliation: \(mucAffiliationToString(affiliations[contact]))\(!(online[contact] ?? false) ? Text(" (offline)") : Text(""))") //.foregroundColor(Color(UIColor.secondaryLabel)) .font(.footnote) } @@ -262,6 +278,13 @@ struct MemberList: View { .offset(x:8, y:0) } } + .applyClosure { view in + if !(online[contact] ?? false) { + view.opacity(0.5) + } else { + view + } + } .deleteDisabled(!ownUserHasAffiliationToRemove(contact: contact)) } } diff --git a/Monal/Classes/SwiftuiHelpers.swift b/Monal/Classes/SwiftuiHelpers.swift index be8a03e1c0..5b37a84dcb 100644 --- a/Monal/Classes/SwiftuiHelpers.swift +++ b/Monal/Classes/SwiftuiHelpers.swift @@ -130,6 +130,27 @@ func mucAffiliationToString(_ affiliation: String?) -> String { return NSLocalizedString("", comment:"muc affiliation") } +func mucAffiliationToInt(_ affiliation: String?) -> Int { + if let affiliation = affiliation { + if affiliation == "owner" { + return 1 + } else if affiliation == "admin" { + return 2 + } else if affiliation == "member" { + return 3 + } else if affiliation == "none" { + return 4 + } else if affiliation == "outcast" { + return 5 + } else if affiliation == "profile" { + return 1000 + } else if affiliation == "reinvite" { + return 100 + } + } + return 0 +} + struct CollapsedPickerStyle: ViewModifier { let accessibilityLabel: Text func body(content: Content) -> some View { From 25484e5ad70cb7aac1620ba47ee0eba6d8bd24a9 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Sun, 9 Jun 2024 07:41:46 +0200 Subject: [PATCH 123/143] Make all accessibility labels translatable Since we can't compile our swift code to let XCode extract all translatable strings, we have to go with the uncompiled Text() strings extraction only. --- Monal/Classes/BackgroundSettings.swift | 4 ++-- Monal/Classes/ContactDetails.swift | 14 +++++++------- Monal/Classes/SwiftuiHelpers.swift | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Monal/Classes/BackgroundSettings.swift b/Monal/Classes/BackgroundSettings.swift index 81ce6d6900..839e78ef1f 100644 --- a/Monal/Classes/BackgroundSettings.swift +++ b/Monal/Classes/BackgroundSettings.swift @@ -70,7 +70,7 @@ struct BackgroundSettings: View { Image(systemName: "xmark.circle.fill") .resizable() .frame(width: 32.0, height: 32.0) - .accessibilityLabel("Remove Background Image") + .accessibilityLabel(Text("Remove Background Image")) .applyClosure { view in if #available(iOS 15, *) { view @@ -89,7 +89,7 @@ struct BackgroundSettings: View { .frame(maxWidth: .infinity, alignment: .center) } } - .accessibilityLabel("Change Background Image") + .accessibilityLabel(Text("Change Background Image")) .sheet(isPresented:$showingImagePicker) { ImagePicker(image:$inputImage) } diff --git a/Monal/Classes/ContactDetails.swift b/Monal/Classes/ContactDetails.swift index 7501060366..6b16fc8798 100644 --- a/Monal/Classes/ContactDetails.swift +++ b/Monal/Classes/ContactDetails.swift @@ -92,7 +92,7 @@ struct ContactDetails: View { .applyClosure {view in if contact.isGroup { if ownAffiliation == "owner" { - view.accessibilityLabel((contact.mucType == "group") ? "Change Group Avatar" : "Change Channel Avatar") + view.accessibilityLabel((contact.mucType == "group") ? Text("Change Group Avatar") : Text("Change Channel Avatar")) .onTapGesture { showImagePicker() } @@ -104,7 +104,7 @@ struct ContactDetails: View { Image(systemName: "xmark.circle.fill") .resizable() .frame(width: 24.0, height: 24.0) - .accessibilityLabel((contact.mucType == "group") ? "Remove Group Avatar" : "Remove Channel Avatar") + .accessibilityLabel((contact.mucType == "group") ? Text("Remove Group Avatar") : Text("Remove Channel Avatar")) .applyClosure { view in if #available(iOS 15, *) { view @@ -124,7 +124,7 @@ struct ContactDetails: View { Image(systemName: "pencil.circle.fill") .resizable() .frame(width: 24.0, height: 24.0) - .accessibilityLabel((contact.mucType == "group") ? "Change Group Avatar" : "Change Channel Avatar") + .accessibilityLabel((contact.mucType == "group") ? Text("Change Group Avatar") : Text("Change Channel Avatar")) // .applyClosure { view in // if #available(iOS 15, *) { // view @@ -140,10 +140,10 @@ struct ContactDetails: View { } } } else { - view.accessibilityLabel((contact.mucType == "group") ? "Group Avatar" : "Channel Avatar") + view.accessibilityLabel((contact.mucType == "group") ? Text("Group Avatar") : Text("Channel Avatar")) } } else { - view.accessibilityLabel("Avatar") + view.accessibilityLabel(Text("Avatar")) } } .frame(width: 150, height: 150, alignment: .center) @@ -353,13 +353,13 @@ struct ContactDetails: View { TextField(label, text: $contact.fullNameView, onEditingChanged: { isEditingNickname = $0 }) - .accessibilityLabel(contact.obj.mucType == "group" ? "Group name" : "Channel name") + .accessibilityLabel(contact.obj.mucType == "group" ? Text("Group name") : Text("Channel name")) .addClearButton(isEditing: isEditingNickname, text: $contact.fullNameView) } else if !contact.isGroup && !contact.isSelfChat { TextField(NSLocalizedString("Rename Contact", comment: "placeholder text in contact details"), text: $contact.nickNameView, onEditingChanged: { isEditingNickname = $0 }) - .accessibilityLabel("Nickname") + .accessibilityLabel(Text("Nickname")) .addClearButton(isEditing: isEditingNickname, text: $contact.nickNameView) } diff --git a/Monal/Classes/SwiftuiHelpers.swift b/Monal/Classes/SwiftuiHelpers.swift index 5b37a84dcb..a370d1a8e3 100644 --- a/Monal/Classes/SwiftuiHelpers.swift +++ b/Monal/Classes/SwiftuiHelpers.swift @@ -360,7 +360,7 @@ struct ClearButton: ViewModifier { } label: { Image(systemName: "xmark.circle.fill") .foregroundColor(Color(UIColor.tertiaryLabel)) - .accessibilityLabel("Clear text") + .accessibilityLabel(Text("Clear text")) } .padding(.trailing, 8) .accessibilitySortPriority(1) From caed641db5abcbba1d44adc813ad4c4410c135f2 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Sun, 9 Jun 2024 08:18:13 +0200 Subject: [PATCH 124/143] Make RichAlert smart scrollable, thanks Ryan Lintott --- Monal/Classes/RichAlert.swift | 48 +++++++++++++++-------------------- 1 file changed, 20 insertions(+), 28 deletions(-) diff --git a/Monal/Classes/RichAlert.swift b/Monal/Classes/RichAlert.swift index 0c0983f08c..d03e90d9f4 100644 --- a/Monal/Classes/RichAlert.swift +++ b/Monal/Classes/RichAlert.swift @@ -7,6 +7,7 @@ // import ViewExtractor +import FrameUp struct RichAlertView: ViewModifier where TitleContent: View, BodyContent: View, ButtonContent: View { @Binding public var isPresented: T? @@ -29,40 +30,31 @@ struct RichAlertView: ViewModifier .font(.headline) .padding([.leading, .trailing], 24) Divider() - ScrollView { + SmartScrollView(.vertical, showsIndicators: true, optionalScrolling: true, shrinkToFit: true) { VStack { alertBody(data) .padding([.leading, .trailing], 24) - let buttonViews = alertButtons(data) - Extract(buttonViews) { views in - if views.count == 0 || buttonViews is EmptyView { - Divider() - Button("Close") { - isPresented = nil - } - .padding([.leading, .trailing], 24) - .buttonStyle(DefaultButtonStyle()) - } else { - ForEach(views) { view in - Divider() - .padding(0) - view - .padding([.leading, .trailing], 24) - .buttonStyle(DefaultButtonStyle()) - } - } - } } - .background( - GeometryReader { geo -> Color in - DispatchQueue.main.async { - scrollViewContentSize = geo.size - } - return Color.background + } + let buttonViews = alertButtons(data) + Extract(buttonViews) { views in + if views.count == 0 || buttonViews is EmptyView { + Divider() + Button("Close") { + isPresented = nil } - ) + .padding([.leading, .trailing], 24) + .buttonStyle(DefaultButtonStyle()) + } else { + ForEach(views) { view in + Divider() + .padding(0) + view + .padding([.leading, .trailing], 24) + .buttonStyle(DefaultButtonStyle()) + } + } } - .frame(maxHeight: scrollViewContentSize.height) } .foregroundColor(.primary) .padding([.top, .bottom], 13) From 0fbbb14204057c8c5b9b6ad217acc84362a0de69 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Sun, 9 Jun 2024 10:24:41 +0200 Subject: [PATCH 125/143] Remove leftover code --- Monal/Classes/MemberList.swift | 133 --------------------------------- 1 file changed, 133 deletions(-) diff --git a/Monal/Classes/MemberList.swift b/Monal/Classes/MemberList.swift index 1979a6ab8d..a00f412b35 100644 --- a/Monal/Classes/MemberList.swift +++ b/Monal/Classes/MemberList.swift @@ -332,139 +332,6 @@ struct MemberList: View { } } } - -// var bodyy: some View { -// List { -// Section(header: Text("\(self.muc.contactDisplayName as String) (affiliation: \(mucAffiliationToString(ownAffiliation)))")) { -// if ownAffiliation == "owner" || ownAffiliation == "admin" { -// NavigationLink(destination: LazyClosureView(ContactPicker(account, initializeFrom: memberList, allowRemoval: false) { newMemberList in -// for member in newMemberList { -// if !memberList.contains(member) { -// if self.muc.mucType == "group" { -// performAction(headlineView: Text("Adding new member"), descriptionView: Text("Adding \(member.contactJid as String)...")) { -// account.mucProcessor.setAffiliation("member", ofUser:member.contactJid, inMuc:self.muc.contactJid) -// }.then { _ in -// return performAction(headlineView: Text("Inviting new member"), descriptionView: Text("Adding \(member.contactJid as String)...")) { -// account.mucProcessor.inviteUser(member.contactJid, inMuc: self.muc.contactJid) -// }.recover { error in -// showAlert(title:Text("Error inviting new member!"), description:Text("\(String(describing:error))")) -// return Guarantee.value(nil as monal_void_block_t?) -// } -// }.catch { error in -// showAlert(title:Text("Error adding new member!"), description:Text("\(String(describing:error))")) -// }.finally { -// hideLoadingOverlay(overlay) -// } -// } else { -// performAction(headlineView: Text("Inviting new participant"), descriptionView: Text("Adding \(member.contactJid as String)...")) { -// account.mucProcessor.inviteUser(member.contactJid, inMuc: self.muc.contactJid) -// }.catch { error in -// showAlert(title:Text("Error inviting new participant!"), description:Text("\(String(describing:error))")) -// }.finally { -// hideLoadingOverlay(overlay) -// } -// } -// } -// } -// })) { -// if self.muc.mucType == "group" { -// Text("Add members to group") -// } else { -// Text("Invite participants to channel") -// } -// } -// -// ForEach(memberList, id:\.self) { contact in -// if !contact.isSelfChat { -// if #available(iOS 16, *) { -// Group { -// // ContactEntry(contact:contact) -// Text(contact.contactDisplayName as String) -// Text(contact.contactJid as String) -// -// } -// } else { -// VStack { -// ContactEntry(contact:contact) -// makePickerView(contact:contact, labelText:Text("Affiliation:")) -// .fixedSize() -// .applyClosure { view in -// if #available(iOS 15, *) { -// view.border(.red) -// } else { -// view -// } -// } -// } -// } -// } -// -// //invisible navigation link triggered programmatically -// // .background( -// // NavigationLink(destination: LazyClosureView(ContactDetails(delegate:SheetDismisserProtocol(), contact:contact)), tag:contact, selection:$navigationActive) { EmptyView() } -// // .opacity(0) -// // ) -// -// .deleteDisabled(!ownUserHasAffiliationToRemove(contact: contact)) -// } -// .onDelete(perform: { memberIdx in -// let member = memberList[memberIdx.first!] -// showActionSheet(title: Text("Remove user?"), description: self.muc.mucType == "group" ? Text("Do you want to remove this user from this group? The user won't be able to enter it again until added back to the group.") : Text("Do you want to remove this user from this channel? The user will be able to enter it again.")) { -// performAction(headlineView: Text("Removing member"), descriptionView: Text("Removing \(member.contactJid as String)...")) { -// account.mucProcessor.setAffiliation("none", ofUser: member.contactJid, inMuc: self.muc.contactJid) -// }.catch { error in -// showAlert(title:Text("Error removing user!"), description:Text("\(String(describing:error))")) -// }.finally { -// hideLoadingOverlay(overlay) -// } -// } -// }) -// } else { -// ForEach(memberList, id:\.self) { contact in -// if !contact.isSelfChat { -// NavigationLink(destination: LazyClosureView(ContactDetails(delegate:SheetDismisserProtocol(), contact:contact))) { -// HStack(alignment: .center) { -// ContactEntry(contact:contact) -// Spacer() -// Text(mucAffiliationToString(affiliations[contact])) -// } -// } -// .deleteDisabled(true) -// } -// } -// } -// } -// } -// .actionSheet(isPresented: $showActionSheet) { -// ActionSheet( -// title: actionSheetPrompt.title, -// message: actionSheetPrompt.message, -// buttons: [ -// .cancel(), -// .destructive( -// Text("Yes"), -// action: actionSheetPrompt.closure -// ) -// ] -// ) -// } -// .alert(isPresented: $showAlert, content: { -// Alert(title: alertPrompt.title, message: alertPrompt.message, dismissButton: .default(alertPrompt.dismissLabel)) -// }) -// .addLoadingOverlay(overlay) -// .navigationBarTitle(Text("Group Members"), displayMode: .inline) -// .onAppear { -// updateMemberlist() -// } -// .onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("kMonalMucParticipantsAndMembersUpdated")).receive(on: RunLoop.main)) { notification in -// if let xmppAccount = notification.object as? xmpp, let contact = notification.userInfo?["contact"] as? MLContact { -// DDLogVerbose("Got muc participants/members update from account \(xmppAccount)...") -// if contact == self.muc { -// updateMemberlist() -// } -// } -// } -// } } extension UIPickerView { From 406d06768530dd81e803adc9904ad88c060c9118 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Sun, 9 Jun 2024 10:24:41 +0200 Subject: [PATCH 126/143] Remove leftover code --- Monal/Classes/MemberList.swift | 133 --------------------------------- 1 file changed, 133 deletions(-) diff --git a/Monal/Classes/MemberList.swift b/Monal/Classes/MemberList.swift index 88b7997595..f9e54681e9 100644 --- a/Monal/Classes/MemberList.swift +++ b/Monal/Classes/MemberList.swift @@ -336,139 +336,6 @@ struct MemberList: View { } } } - -// var bodyy: some View { -// List { -// Section(header: Text("\(self.muc.contactDisplayName as String) (affiliation: \(mucAffiliationToString(ownAffiliation)))")) { -// if ownAffiliation == "owner" || ownAffiliation == "admin" { -// NavigationLink(destination: LazyClosureView(ContactPicker(account, initializeFrom: memberList, allowRemoval: false) { newMemberList in -// for member in newMemberList { -// if !memberList.contains(member) { -// if self.muc.mucType == "group" { -// performAction(headlineView: Text("Adding new member"), descriptionView: Text("Adding \(member.contactJid as String)...")) { -// account.mucProcessor.setAffiliation("member", ofUser:member.contactJid, inMuc:self.muc.contactJid) -// }.then { _ in -// return performAction(headlineView: Text("Inviting new member"), descriptionView: Text("Adding \(member.contactJid as String)...")) { -// account.mucProcessor.inviteUser(member.contactJid, inMuc: self.muc.contactJid) -// }.recover { error in -// showAlert(title:Text("Error inviting new member!"), description:Text("\(String(describing:error))")) -// return Guarantee.value(nil as monal_void_block_t?) -// } -// }.catch { error in -// showAlert(title:Text("Error adding new member!"), description:Text("\(String(describing:error))")) -// }.finally { -// hideLoadingOverlay(overlay) -// } -// } else { -// performAction(headlineView: Text("Inviting new participant"), descriptionView: Text("Adding \(member.contactJid as String)...")) { -// account.mucProcessor.inviteUser(member.contactJid, inMuc: self.muc.contactJid) -// }.catch { error in -// showAlert(title:Text("Error inviting new participant!"), description:Text("\(String(describing:error))")) -// }.finally { -// hideLoadingOverlay(overlay) -// } -// } -// } -// } -// })) { -// if self.muc.mucType == "group" { -// Text("Add members to group") -// } else { -// Text("Invite participants to channel") -// } -// } -// -// ForEach(memberList, id:\.self) { contact in -// if !contact.isSelfChat { -// if #available(iOS 16, *) { -// Group { -// // ContactEntry(contact:contact) -// Text(contact.contactDisplayName as String) -// Text(contact.contactJid as String) -// -// } -// } else { -// VStack { -// ContactEntry(contact:contact) -// makePickerView(contact:contact, labelText:Text("Affiliation:")) -// .fixedSize() -// .applyClosure { view in -// if #available(iOS 15, *) { -// view.border(.red) -// } else { -// view -// } -// } -// } -// } -// } -// -// //invisible navigation link triggered programmatically -// // .background( -// // NavigationLink(destination: LazyClosureView(ContactDetails(delegate:SheetDismisserProtocol(), contact:contact)), tag:contact, selection:$navigationActive) { EmptyView() } -// // .opacity(0) -// // ) -// -// .deleteDisabled(!ownUserHasAffiliationToRemove(contact: contact)) -// } -// .onDelete(perform: { memberIdx in -// let member = memberList[memberIdx.first!] -// showActionSheet(title: Text("Remove user?"), description: self.muc.mucType == "group" ? Text("Do you want to remove this user from this group? The user won't be able to enter it again until added back to the group.") : Text("Do you want to remove this user from this channel? The user will be able to enter it again.")) { -// performAction(headlineView: Text("Removing member"), descriptionView: Text("Removing \(member.contactJid as String)...")) { -// account.mucProcessor.setAffiliation("none", ofUser: member.contactJid, inMuc: self.muc.contactJid) -// }.catch { error in -// showAlert(title:Text("Error removing user!"), description:Text("\(String(describing:error))")) -// }.finally { -// hideLoadingOverlay(overlay) -// } -// } -// }) -// } else { -// ForEach(memberList, id:\.self) { contact in -// if !contact.isSelfChat { -// NavigationLink(destination: LazyClosureView(ContactDetails(delegate:SheetDismisserProtocol(), contact:contact))) { -// HStack(alignment: .center) { -// ContactEntry(contact:contact) -// Spacer() -// Text(mucAffiliationToString(affiliations[contact])) -// } -// } -// .deleteDisabled(true) -// } -// } -// } -// } -// } -// .actionSheet(isPresented: $showActionSheet) { -// ActionSheet( -// title: actionSheetPrompt.title, -// message: actionSheetPrompt.message, -// buttons: [ -// .cancel(), -// .destructive( -// Text("Yes"), -// action: actionSheetPrompt.closure -// ) -// ] -// ) -// } -// .alert(isPresented: $showAlert, content: { -// Alert(title: alertPrompt.title, message: alertPrompt.message, dismissButton: .default(alertPrompt.dismissLabel)) -// }) -// .addLoadingOverlay(overlay) -// .navigationBarTitle(Text("Group Members"), displayMode: .inline) -// .onAppear { -// updateMemberlist() -// } -// .onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("kMonalMucParticipantsAndMembersUpdated")).receive(on: RunLoop.main)) { notification in -// if let xmppAccount = notification.object as? xmpp, let contact = notification.userInfo?["contact"] as? MLContact { -// DDLogVerbose("Got muc participants/members update from account \(xmppAccount)...") -// if contact == self.muc { -// updateMemberlist() -// } -// } -// } -// } } extension UIPickerView { From 23be752ccc9666ea236222e8d66dacbbcc719b36 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Sun, 9 Jun 2024 10:27:44 +0200 Subject: [PATCH 127/143] Fix broken merge-conflict fix --- Monal/Classes/MemberList.swift | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Monal/Classes/MemberList.swift b/Monal/Classes/MemberList.swift index f9e54681e9..a00f412b35 100644 --- a/Monal/Classes/MemberList.swift +++ b/Monal/Classes/MemberList.swift @@ -277,9 +277,6 @@ struct MemberList: View { .fixedSize() .offset(x:8, y:0) } - .deleteDisabled( - !ownUserHasAffiliationToRemove(contact: contact) - ) } .applyClosure { view in if !(online[contact] ?? false) { @@ -287,7 +284,6 @@ struct MemberList: View { } else { view } - .deleteDisabled(true) } .deleteDisabled(!ownUserHasAffiliationToRemove(contact: contact)) } From dbe99aadd095c21d5fcea12b27978d19719fdd0f Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Sun, 9 Jun 2024 10:29:40 +0200 Subject: [PATCH 128/143] --- 906 --- 6.4.0b2 From a48fdf64d5d34d03bdc2e4e307063d15b73c45ee Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Sun, 9 Jun 2024 10:58:10 +0200 Subject: [PATCH 129/143] Fix missing MLContact singleton update --- Monal/Classes/MLContact.m | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Monal/Classes/MLContact.m b/Monal/Classes/MLContact.m index fffaaea2e1..6bf237a76d 100644 --- a/Monal/Classes/MLContact.m +++ b/Monal/Classes/MLContact.m @@ -253,9 +253,10 @@ +(MLContact*) createContactFromJid:(NSString*) jid andAccountNo:(NSNumber*) acco -(instancetype) init { self = [super init]; - // watch for changes in lastInteractionTime and update dynamically + //watch for all sorts of changes and update our singleton dynamically [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleLastInteractionTimeUpdate:) name:kMonalLastInteractionUpdatedNotice object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleBlockListRefresh:) name:kMonalBlockListRefresh object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(refresh) name:kMonalRefresh object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleContactRefresh:) name:kMonalContactRefresh object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleContactRefresh:) name:kMonalContactRemoved object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleMucSubjectChange:) name:kMonalMucSubjectChanged object:nil]; From 39a8f0765dfc2f29121dc5ac9fbb9769988ffbeb Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Sun, 9 Jun 2024 11:36:49 +0200 Subject: [PATCH 130/143] --- 907 --- 6.4.0rc1 From 4f4bd01a5f87e0640688b17dd44150d0195f7fe9 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Sun, 9 Jun 2024 12:24:05 +0200 Subject: [PATCH 131/143] Fix audio recorder typos --- Monal/Classes/MLAudioRecoderManager.m | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/Monal/Classes/MLAudioRecoderManager.m b/Monal/Classes/MLAudioRecoderManager.m index 4d483167b0..111f2f3fc7 100644 --- a/Monal/Classes/MLAudioRecoderManager.m +++ b/Monal/Classes/MLAudioRecoderManager.m @@ -32,14 +32,14 @@ -(void) start [audioSession setCategory:AVAudioSessionCategoryRecord error:&audioSessionCategoryError]; [audioSession setActive:YES error:&audioRecodSetActiveError]; if (audioSessionCategoryError) { - DDLogError(@"Audio Recoder set category error: %@", audioSessionCategoryError); - [recoderManagerDelegate notifyResult:NO error:NSLocalizedString(@"Audio Recoder set category error: %@", audioSessionCategoryError)]; + DDLogError(@"Audio Recorder set category error: %@", audioSessionCategoryError); + [recoderManagerDelegate notifyResult:NO error:NSLocalizedString(@"Audio Recorder set category error: %@", audioSessionCategoryError)]; return; } if (audioRecodSetActiveError) { - DDLogError(@"Audio Recoder set active error: %@", audioRecodSetActiveError); - [recoderManagerDelegate notifyResult:NO error:NSLocalizedString(@"Audio Recoder set active error: %@", audioRecodSetActiveError)]; + DDLogError(@"Audio Recorder set active error: %@", audioRecodSetActiveError); + [recoderManagerDelegate notifyResult:NO error:NSLocalizedString(@"Audio Recorder set active error: %@", audioRecodSetActiveError)]; return; } @@ -56,8 +56,8 @@ -(void) start if(recoderError) { - DDLogError(@"recoderError: %@", recoderError); - [recoderManagerDelegate notifyResult:NO error:NSLocalizedString(@"Audio recoder init fail.", @"")]; + DDLogError(@"recorderError: %@", recoderError); + [recoderManagerDelegate notifyResult:NO error:NSLocalizedString(@"Audio recorder init fail.", @"")]; return; } self.audioRecorder.delegate = self; @@ -65,14 +65,14 @@ -(void) start if(!isPrepare) { - [recoderManagerDelegate notifyResult:NO error:NSLocalizedString(@"Audio recoder prepareToRecord fail.", @"")]; + [recoderManagerDelegate notifyResult:NO error:NSLocalizedString(@"Audio recorder prepareToRecord fail.", @"")]; return; } BOOL isRecord = [self.audioRecorder record]; if(!isRecord) { - [recoderManagerDelegate notifyResult:NO error:NSLocalizedString(@"Audio recoder record fail.", @"")]; + [recoderManagerDelegate notifyResult:NO error:NSLocalizedString(@"Audio recorder record fail.", @"")]; return; } [recoderManagerDelegate notifyStart]; @@ -110,14 +110,14 @@ - (void) audioRecorderDidFinishRecording:(AVAudioRecorder*) recorder successfull } else { - [recoderManagerDelegate notifyResult:NO error:NSLocalizedString(@"Audio Recoder recode fail", @"")]; - DDLogError(@"Audio Recoder recode fail"); + [recoderManagerDelegate notifyResult:NO error:NSLocalizedString(@"Audio Recorder: failed to record", @"")]; + DDLogError(@"Audio Recorder record fail"); } } -(void) audioRecorderEncodeErrorDidOccur:(AVAudioRecorder*) recorder error:(NSError*) error { - DDLogError(@"Audio Recoder EncodeError: %@", [error description]); + DDLogError(@"Audio Recorder EncodeError: %@", [error description]); [self.recoderManagerDelegate notifyResult:NO error:[error description]]; } @@ -128,7 +128,7 @@ -(NSString*) getAudioPath NSError* error = nil; [fileManager createDirectoryAtPath:writablePath withIntermediateDirectories:YES attributes:nil error:&error]; if(error) - DDLogError(@"Audio Recoder create directory fail: %@", [error description]); + DDLogError(@"Audio Recorder create directory fail: %@", [error description]); [HelperTools configureFileProtectionFor:writablePath]; NSString* audioFilePath = [writablePath stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.m4a",[[NSUUID UUID] UUIDString]]]; return audioFilePath; From 7ae2f119084f62e8f3fd0b384884a6ff15e30439 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Mon, 10 Jun 2024 00:11:34 +0200 Subject: [PATCH 132/143] Relax jid faulty pattern and improve error message after jid parse --- Monal/Classes/AddContactMenu.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Monal/Classes/AddContactMenu.swift b/Monal/Classes/AddContactMenu.swift index c5b13617db..94011b2dc9 100644 --- a/Monal/Classes/AddContactMenu.swift +++ b/Monal/Classes/AddContactMenu.swift @@ -11,7 +11,7 @@ import UniformTypeIdentifiers struct AddContactMenu: View { var delegate: SheetDismisserProtocol - static private let jidFaultyPattern = "^([^@]+@)?.+\\..{2,}$" + static private let jidFaultyPattern = "^([^@]+@)?.+(\\..{2,})?$" @State private var connectedAccounts: [xmpp] @State private var selectedAccount: Int @@ -218,7 +218,7 @@ struct AddContactMenu: View { 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 the string...")) + errorAlert(title: Text("Error"), message: Text("Something went wrong while parsing your input...")) showAlert = true return } From 8d5e7ccfcbe09278615d8c1ba3d552add5df65f4 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Mon, 10 Jun 2024 00:42:14 +0200 Subject: [PATCH 133/143] Update opensource.html to include PromiseKit, FrameUp and art.of-sopy.ch --- Monal/opensource.html | 68 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 60 insertions(+), 8 deletions(-) diff --git a/Monal/opensource.html b/Monal/opensource.html index 0bc30c1e50..400482f49d 100644 --- a/Monal/opensource.html +++ b/Monal/opensource.html @@ -27,15 +27,15 @@ The views and conclusions contained in the software and documentation are those of the authors and should not be interpreted as representing official policies, either expressed or implied, of The Monal Developers

-
- Logo, Empty DataView and Chat-Placeholder Artwork by Ann-Sophie Zwahlen
- All rights reserved.


+ Logo, Empty DataView and Chat-Placeholder Artwork by Ann-Sophie Zwahlen - https://art.of-sophy.ch/
+ All rights reserved.

+
Parts of the WebRTC Demo by Stasel

-
-
+ https://github.com/stasel/WebRTC-iOS
+ https://github.com/stasel/WebRTC-iOS/blob/main/WebRTC-Demo-App/Sources/Services/WebRTCClient.swift
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
Definitions.

@@ -104,10 +104,62 @@
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

+
+ FrameUP: Reframing SwiftUI Views. A collection of tools to help with layout.
+ https://github.com/ryanlintott/FrameUp
+ MIT License
+
+ Copyright (c) 2022 Ryan Lintott
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE.
+
+ +
+ PromiseKit: Promises for Swift & ObjC.
+ https://github.com/mxcl/PromiseKit
+ MIT License
+
+ Copyright 2016-present, Max Howell; mxcl@me.com
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE.
+
+
HSLuv-C: Human-friendly HSL

-
-
+ https://github.com/hsluv/hsluv-c
+ https://www.hsluv.org/
Copyright (c) 2015 Alexei Boronine (original idea, JavaScript implementation)
Copyright (c) 2015 Roger Tallada (Obj-C implementation)
Copyright (c) 2017 Martin Mitas (C implementation, based on Obj-C implementation)

@@ -216,7 +268,7 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


SnapKit

- Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit

+ Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From adbba512153bc868348df992bc9f91f8aaf561ec Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Mon, 10 Jun 2024 01:02:12 +0200 Subject: [PATCH 134/143] Show all websites linked in Settings as inline using MLWebViewControlleir, fixes #1077 --- Monal/Classes/MLSettingsTableViewController.m | 27 ++++++++++++++++--- Monal/Classes/MLWebViewController.m | 23 ++++++++++++++++ .../Base.lproj/Settings.storyboard | 1 + 3 files changed, 48 insertions(+), 3 deletions(-) diff --git a/Monal/Classes/MLSettingsTableViewController.m b/Monal/Classes/MLSettingsTableViewController.m index 64d5b11c67..a73297329e 100644 --- a/Monal/Classes/MLSettingsTableViewController.m +++ b/Monal/Classes/MLSettingsTableViewController.m @@ -155,6 +155,27 @@ -(void) prepareForSegue:(UIStoryboardSegue*) segue sender:(id) sender [web initViewWithUrl:[NSURL fileURLWithPath:myFile]]; } + else if([segue.identifier isEqualToString:@"showAbout"]) + { + UINavigationController* nav = (UINavigationController*) segue.destinationViewController; + MLWebViewController* web = (MLWebViewController*) nav.topViewController; + + [web initViewWithUrl:[NSURL URLWithString:@"https://monal-im.org/about"]]; + } + else if([segue.identifier isEqualToString:@"showPrivacy"]) + { + UINavigationController* nav = (UINavigationController*) segue.destinationViewController; + MLWebViewController* web = (MLWebViewController*) nav.topViewController; + + [web initViewWithUrl:[NSURL URLWithString:@"https://monal-im.org/privacy"]]; + } + else if([segue.identifier isEqualToString:@"showBug"]) + { + UINavigationController* nav = (UINavigationController*) segue.destinationViewController; + MLWebViewController* web = (MLWebViewController*) nav.topViewController; + + [web initViewWithUrl:[NSURL URLWithString:@"https://github.com/monal-im/Monal/issues"]]; + } else if([segue.identifier isEqualToString:@"editXMPP"]) { XMPPEdit* editor = (XMPPEdit*) segue.destinationViewController.childViewControllers.firstObject; // segue.destinationViewController; @@ -333,7 +354,7 @@ -(void)tableView:(UITableView*) tableView didSelectRowAtIndexPath:(NSIndexPath*) [self composeMail]; break; case SubmitABugRow: - [self openLink:@"https://github.com/monal-im/Monal/issues"]; + [self performSegueWithIdentifier:@"showBug" sender:self]; break; default: unreachable(); @@ -349,10 +370,10 @@ -(void)tableView:(UITableView*) tableView didSelectRowAtIndexPath:(NSIndexPath*) [self performSegueWithIdentifier:@"showOpenSource" sender:self]; break; case PrivacyRow: - [self openLink:@"https://monal-im.org/privacy"]; + [self performSegueWithIdentifier:@"showPrivacy" sender:self]; break; case AboutRow: - [self openLink:@"https://monal-im.org/about"]; + [self performSegueWithIdentifier:@"showAbout" sender:self]; break; #ifdef DEBUG case LogRow: diff --git a/Monal/Classes/MLWebViewController.m b/Monal/Classes/MLWebViewController.m index 33ae3a260f..5bda2b8461 100644 --- a/Monal/Classes/MLWebViewController.m +++ b/Monal/Classes/MLWebViewController.m @@ -21,6 +21,29 @@ -(void) viewDidLoad [super viewDidLoad]; self.webview.contentMode = UIViewContentModeScaleAspectFill; self.splitViewController.preferredDisplayMode = UISplitViewControllerDisplayModeOneBesideSecondary; + + UIBarButtonItem* openExternally = [[UIBarButtonItem alloc] init]; + openExternally.image = [UIImage systemImageNamed:@"safari"]; + [openExternally setTarget:self]; + [openExternally setAction:@selector(openExternally:)]; + [openExternally setIsAccessibilityElement:YES]; + [openExternally setAccessibilityLabel:NSLocalizedString(@"Open in default browser", @"")]; + self.navigationItem.rightBarButtonItems = [[NSArray alloc] initWithObjects:openExternally, nil]; +} + +-(void) openExternally:(id) sender +{ + DDLogDebug(@"Trying to open in default browser: %@", self.webview.URL); + if(self.webview.URL.fileURL) + { + UIAlertController* alert = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"Error", @"") message:NSLocalizedString(@"This is an embedded file that can not be opened externally.", @"") preferredStyle:UIAlertControllerStyleAlert]; + [alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"OK", @"") style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action __unused) { + [alert dismissViewControllerAnimated:YES completion:nil]; + }]]; + [self presentViewController:alert animated:YES completion:nil]; + } + else + [[UIApplication sharedApplication] performSelector:@selector(openURL:) withObject:self.webview.URL]; } -(void) viewWillAppear:(BOOL)animated diff --git a/Monal/localization/Base.lproj/Settings.storyboard b/Monal/localization/Base.lproj/Settings.storyboard index 091283cf0a..62cdc50351 100644 --- a/Monal/localization/Base.lproj/Settings.storyboard +++ b/Monal/localization/Base.lproj/Settings.storyboard @@ -241,6 +241,7 @@ + From 26b0558e3481b862036afd9db20cf69d673c758c Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Mon, 10 Jun 2024 02:31:33 +0200 Subject: [PATCH 135/143] Update doap file --- monal.doap | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/monal.doap b/monal.doap index 6a689bb2f4..7ece02a1f2 100644 --- a/monal.doap +++ b/monal.doap @@ -469,7 +469,7 @@ complete 6.0 - 6.0 + 0.6.0 XEP-0353: Jingle Message Initiation
@@ -555,7 +555,7 @@ complete 6.0 - 0.4.0 + 1.0.1 XEP-0388: Extensible SASL Profile @@ -609,7 +609,7 @@ complete 5.4 - 1.1.3 + 1.1.4 XEP-0402: PEP Native Bookmarks From 9ca16548d65beaf3f1f2cb468e5d78abf421986a Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Mon, 10 Jun 2024 10:37:11 +0200 Subject: [PATCH 136/143] Implement proper SVG support --- Monal/Classes/HelperTools.h | 6 +- Monal/Classes/HelperTools.m | 94 ++++++++++++++++++++++++++- Monal/Classes/ImageViewer.swift | 61 ++++++++++++----- Monal/Classes/MLChatImageCell.m | 18 ++++- Monal/Classes/MLNotificationManager.m | 41 +++++++++--- Monal/Classes/SwiftHelpers.swift | 21 ++++++ Monal/Classes/chatViewController.m | 22 +++---- Monal/Monal.xcodeproj/project.pbxproj | 17 +++++ Monal/opensource.html | 26 ++++++++ 9 files changed, 266 insertions(+), 40 deletions(-) diff --git a/Monal/Classes/HelperTools.h b/Monal/Classes/HelperTools.h index dc149d6722..2f01185235 100644 --- a/Monal/Classes/HelperTools.h +++ b/Monal/Classes/HelperTools.h @@ -92,6 +92,7 @@ void swizzle(Class c, SEL orig, SEL new); +(MLXMLNode* _Nullable) candidate2xml:(NSString*) candidate withMid:(NSString*) mid pwd:(NSString* _Nullable) pwd ufrag:(NSString* _Nullable) ufrag andInitiator:(BOOL) initiator; +(NSString* _Nullable) xml2candidate:(MLXMLNode*) xml withInitiator:(BOOL) initiator; ++(UIImage* _Nullable) renderUIImageFromSVGURL:(NSURL* _Nullable) url API_AVAILABLE(ios(16.0), macosx(13.0)); //means: API_AVAILABLE(ios(16.0), maccatalyst(16.0)) +(void) busyWaitForOperationQueue:(NSOperationQueue*) queue; +(id) getObjcDefinedValue:(MLDefinedIdentifier) identifier; +(NSRunLoop*) getExtraRunloopWithIdentifier:(MLRunLoopIdentifier) identifier; @@ -107,8 +108,11 @@ void swizzle(Class c, SEL orig, SEL new); +(NSError* _Nullable) postUserNotificationRequest:(UNNotificationRequest*) request; +(void) addUploadItemPreviewForItem:(NSURL* _Nullable) url provider:(NSItemProvider* _Nullable) provider andPayload:(NSMutableDictionary*) payload withCompletionHandler:(void(^)(NSMutableDictionary* _Nullable)) completion; +(void) handleUploadItemProvider:(NSItemProvider*) provider withCompletionHandler:(void (^)(NSMutableDictionary* _Nullable)) completion; -+(UIView*) buttonWithNotificationBadgeForImage:(UIImage*) image hasNotification:(bool) hasNotification withTapHandler: (UITapGestureRecognizer*) handler; ++(UIImage* _Nullable) rotateImage:(UIImage* _Nullable) image byRadians:(CGFloat) rotation; ++(UIImage* _Nullable) mirrorImageOnXAxis:(UIImage* _Nullable) image; ++(UIImage* _Nullable) mirrorImageOnYAxis:(UIImage* _Nullable) image; +(UIImage*) imageWithNotificationBadgeForImage:(UIImage*) image; ++(UIView*) buttonWithNotificationBadgeForImage:(UIImage*) image hasNotification:(bool) hasNotification withTapHandler: (UITapGestureRecognizer*) handler; +(NSData*) resizeAvatarImage:(UIImage* _Nullable) image withCircularMask:(BOOL) circularMask toMaxBase64Size:(unsigned long) length; +(double) report_memory; +(UIColor*) generateColorFromJid:(NSString*) jid; diff --git a/Monal/Classes/HelperTools.m b/Monal/Classes/HelperTools.m index ccf2b26755..f720791c79 100644 --- a/Monal/Classes/HelperTools.m +++ b/Monal/Classes/HelperTools.m @@ -528,6 +528,12 @@ +(NSString*) getSelectedPushServerBasedOnLocale ]; } +//this wrapper is needed, because MLChatImageCell can't import our monalxmpp-Swift bridging header, but importing HelperTools is okay ++(UIImage* _Nullable) renderUIImageFromSVGURL:(NSURL* _Nullable) url API_AVAILABLE(ios(16.0), macosx(13.0)) //means: API_AVAILABLE(ios(16.0), maccatalyst(16.0)) +{ + return [SwiftHelpers _renderUIImageFromSVGURL:url]; +} + +(void) busyWaitForOperationQueue:(NSOperationQueue*) queue { //apparently setting someQueue.suspended = YES does return before the queue is actually suspended @@ -1151,7 +1157,90 @@ +(void) handleUploadItemProvider:(NSItemProvider*) provider withCompletionHandle } #pragma clang diagnostic pop -+(UIImage*) imageWithNotificationBadgeForImage:(UIImage*) image { +//see https://gist.github.com/giaesp/7704753 ++(UIImage* _Nullable) rotateImage:(UIImage* _Nullable) image byRadians:(CGFloat) rotation +{ + if(image == nil) + return nil; + + //Calculate Destination Size + CGAffineTransform t = CGAffineTransformMakeRotation(rotation); + CGRect sizeRect = (CGRect) {.size = image.size}; + CGRect destRect = CGRectApplyAffineTransform(sizeRect, t); + CGSize destinationSize = destRect.size; + + //Draw image + UIGraphicsBeginImageContext(destinationSize); + CGContextRef context = UIGraphicsGetCurrentContext(); + CGContextTranslateCTM(context, destinationSize.width / 2.0f, destinationSize.height / 2.0f); + CGContextRotateCTM(context, rotation); + [image drawInRect:CGRectMake(-image.size.width / 2.0f, -image.size.height / 2.0f, image.size.width, image.size.height)]; + + //Save image (and save space) + image = nil; + image = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + return image; +} + ++(UIImage* _Nullable) mirrorImageOnXAxis:(UIImage* _Nullable) image +{ + if(image == nil) + return nil; + + //Create a new image context + UIGraphicsBeginImageContextWithOptions(image.size, NO, image.scale); + CGContextRef context = UIGraphicsGetCurrentContext(); + + //Move the origin to the middle of the image to apply the transformation + CGContextTranslateCTM(context, image.size.width / 2, image.size.height / 2); + + //Apply the x-axis mirroring transform + CGContextScaleCTM(context, 1.0, -1.0); + + //Move the origin back to the bottom left corner + CGContextTranslateCTM(context, -image.size.width / 2, -image.size.height / 2); + + //Draw the original image into the transformed context + [image drawInRect:CGRectMake(0, 0, image.size.width, image.size.height)]; + + //Save image (and save space) + image = nil; + image = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + return image; +} + ++(UIImage* _Nullable) mirrorImageOnYAxis:(UIImage* _Nullable) image +{ + if(image == nil) + return nil; + + //Create a new image context + UIGraphicsBeginImageContextWithOptions(image.size, NO, image.scale); + CGContextRef context = UIGraphicsGetCurrentContext(); + + //Move the origin to the middle of the image to apply the transformation + CGContextTranslateCTM(context, image.size.width / 2, image.size.height / 2); + + //Apply the y-axis mirroring transform + CGContextScaleCTM(context, -1.0, 1.0); + + //Move the origin back to the bottom left corner + CGContextTranslateCTM(context, -image.size.width / 2, -image.size.height / 2); + + //Draw the original image into the transformed context + [image drawInRect:CGRectMake(0, 0, image.size.width, image.size.height)]; + + //Save image (and save space) + image = nil; + image = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + return image; +} + ++(UIImage*) imageWithNotificationBadgeForImage:(UIImage*) image +{ UIImage* finalImage; UIImage* badge = [[UIImage systemImageNamed:@"circle.fill"] imageWithTintColor:UIColor.redColor]; @@ -1168,7 +1257,8 @@ +(UIImage*) imageWithNotificationBadgeForImage:(UIImage*) image { return finalImage; } -+(UIImageView*) buttonWithNotificationBadgeForImage:(UIImage*) image hasNotification:(bool) hasNotification withTapHandler: (UITapGestureRecognizer*) handler { ++(UIImageView*) buttonWithNotificationBadgeForImage:(UIImage*) image hasNotification:(bool) hasNotification withTapHandler: (UITapGestureRecognizer*) handler +{ UIImageView* result; if(hasNotification) result = [[UIImageView alloc] initWithImage:[self imageWithNotificationBadgeForImage:image]]; diff --git a/Monal/Classes/ImageViewer.swift b/Monal/Classes/ImageViewer.swift index 06d7012c31..e50d0b784e 100644 --- a/Monal/Classes/ImageViewer.swift +++ b/Monal/Classes/ImageViewer.swift @@ -7,6 +7,7 @@ // import UniformTypeIdentifiers +import SVGView @available(iOS 16, *) struct GifRepresentation: Transferable { @@ -34,6 +35,19 @@ struct JpegRepresentation: Transferable { } } +@available(iOS 16, *) +struct SVGRepresentation: Transferable { + let getData: () -> Data + + static var transferRepresentation: some TransferRepresentation { + DataRepresentation(exportedContentType: .svg) { item in + { () -> Data in + return item.getData() + }() + } + } +} + struct ImageViewer: View { var delegate: SheetDismisserProtocol let info: [String:AnyObject] @@ -56,7 +70,13 @@ struct ImageViewer: View { Color.background .edgesIgnoringSafeArea(.all) - if let image = UIImage(contentsOfFile:info["cacheFile"] as! String) { + if (info["mimeType"] as! String).hasPrefix("image/svg") { + VStack { + ZoomableContainer(maxScale:8.0, doubleTapScale:4.0) { + SVGView(contentsOf: URL(fileURLWithPath:info["cacheFile"] as! String)) + } + } + } else if let image = UIImage(contentsOfFile:info["cacheFile"] as! String) { VStack { ZoomableContainer(maxScale:8.0, doubleTapScale:4.0) { if (info["mimeType"] as! String).hasPrefix("image/gif") { @@ -98,27 +118,38 @@ struct ImageViewer: View { Spacer().frame(width:20) Text(info["filename"] as! String).foregroundColor(.primary) Spacer() - if #available(iOS 16, *), let image = UIImage(contentsOfFile:info["cacheFile"] as! String) { - if (info["mimeType"] as! String).hasPrefix("image/gif") { - ShareLink( - item: GifRepresentation(getData: { - try! NSData(contentsOfFile:info["cacheFile"] as! String) as Data - }), preview: SharePreview("Share image", image: Image(uiImage: image)) - ) - .labelStyle(.iconOnly) - .foregroundColor(.primary) - } else { - // even share non-gif images as Data instead of Image, because this leads to fewer crashes of other apps - // see https://medium.com/@timonus/reduce-share-extension-crashes-from-your-app-with-this-one-weird-trick-6b86211bb175 + if #available(iOS 16, *) { + if (info["mimeType"] as! String).hasPrefix("image/svg"), let image = HelperTools.renderUIImage(fromSVGURL:URL(fileURLWithPath:info["cacheFile"] as! String)) { ShareLink( - item: JpegRepresentation(getData: { + item: SVGRepresentation(getData: { try! NSData(contentsOfFile:info["cacheFile"] as! String) as Data }), preview: SharePreview("Share image", image: Image(uiImage: image)) ) .labelStyle(.iconOnly) .foregroundColor(.primary) + Spacer().frame(width:20) + } else if let image = UIImage(contentsOfFile:info["cacheFile"] as! String) { + if (info["mimeType"] as! String).hasPrefix("image/gif") { + ShareLink( + item: GifRepresentation(getData: { + try! NSData(contentsOfFile:info["cacheFile"] as! String) as Data + }), preview: SharePreview("Share image", image: Image(uiImage: image)) + ) + .labelStyle(.iconOnly) + .foregroundColor(.primary) + } else { + // even share non-gif images as Data instead of Image, because this leads to fewer crashes of other apps + // see https://medium.com/@timonus/reduce-share-extension-crashes-from-your-app-with-this-one-weird-trick-6b86211bb175 + ShareLink( + item: JpegRepresentation(getData: { + try! NSData(contentsOfFile:info["cacheFile"] as! String) as Data + }), preview: SharePreview("Share image", image: Image(uiImage: image)) + ) + .labelStyle(.iconOnly) + .foregroundColor(.primary) + } + Spacer().frame(width:20) } - Spacer().frame(width:20) } Button(action: { self.delegate.dismiss() diff --git a/Monal/Classes/MLChatImageCell.m b/Monal/Classes/MLChatImageCell.m index fe3304cb07..7e8f2a5fcd 100644 --- a/Monal/Classes/MLChatImageCell.m +++ b/Monal/Classes/MLChatImageCell.m @@ -69,7 +69,7 @@ -(void) loadImage:(MLMessage*) msg if(!image) return; _animatedImageView = [FLAnimatedImageView new]; - DDLogVerbose(@"image: %fx%f", image.size.height, image.size.width); + DDLogVerbose(@"image %@\n--> %fx%f", info, image.size.height, image.size.width); CGFloat wi = image.size.width; CGFloat hi = image.size.height; CGFloat ws = 225.0; @@ -90,10 +90,22 @@ -(void) loadImage:(MLMessage*) msg { self.link = msg.messageText; // uses cached file if the file was already downloaded - UIImage* image = [[UIImage alloc] initWithContentsOfFile:info[@"cacheFile"]]; + UIImage* image = nil; + if([info[@"mimeType"] hasPrefix:@"image/svg"]) + { + if(@available(iOS 16.0, macCatalyst 16.0, *)) + image = [HelperTools renderUIImageFromSVGURL:[NSURL fileURLWithPath:info[@"cacheFile"]]]; + else + { + DDLogWarn(@"Using photo placeholder for SVG on ios < 16..."); + image = [UIImage systemImageNamed:@"photo.fill"]; + } + } + else + image = [[UIImage alloc] initWithContentsOfFile:info[@"cacheFile"]]; if(!image) return; - DDLogVerbose(@"image: %fx%f", image.size.height, image.size.width); + DDLogVerbose(@"image %@\n--> %fx%f", info, image.size.height, image.size.width); CGFloat wi = image.size.width; CGFloat hi = image.size.height; CGFloat ws = 225.0; diff --git a/Monal/Classes/MLNotificationManager.m b/Monal/Classes/MLNotificationManager.m index b424b87b70..32b1cf74f6 100644 --- a/Monal/Classes/MLNotificationManager.m +++ b/Monal/Classes/MLNotificationManager.m @@ -809,22 +809,47 @@ -(void) showLegacyNotificationForMessage:(MLMessage*) message withSound:(BOOL) s -(UNNotificationAttachment* _Nullable) createNotificationAttachmentForFileInfo:(NSDictionary*) info havingTypeHint:(UTType*) typeHint { NSError* error; - NSString* notificationAttachment = [[HelperTools getContainerURLForPathComponents:@[@"documentCache"]] path]; + NSString* attachmentDir = [[HelperTools getContainerURLForPathComponents:@[@"documentCache"]] path]; //use "tmp." prefix to make sure this file will be garbage collected should the ios notification attachment implementation leave it behind NSString* attachmentBasename = [NSString stringWithFormat:@"tmp.%@", info[@"cacheId"]]; + NSString* notificationAttachment = [attachmentDir stringByAppendingPathComponent:[attachmentBasename stringByAppendingPathExtensionForType:typeHint]]; //using stringByAppendingPathExtensionForType: does not produce playable audio notifications for audios sent by conversations, //but seems to work for other types //--> use info[@"fileExtension"] for audio files and stringByAppendingPathExtensionForType: for all other types if([typeHint conformsToType:UTTypeAudio]) notificationAttachment = [notificationAttachment stringByAppendingPathComponent:[attachmentBasename stringByAppendingPathExtension:info[@"fileExtension"]]]; - else - notificationAttachment = [notificationAttachment stringByAppendingPathComponent:[attachmentBasename stringByAppendingPathExtensionForType:typeHint]]; - DDLogVerbose(@"Preparing for notification attachment(%@): hardlinking downloaded file from '%@' to '%@'..", typeHint, info[@"cacheFile"], notificationAttachment); - error = [HelperTools hardLinkOrCopyFile:info[@"cacheFile"] to:notificationAttachment]; - if(error) + UIImage* image = nil; + if([info[@"mimeType"] hasPrefix:@"image/svg"]) + { + NSString* pngAttachment = [attachmentDir stringByAppendingPathComponent:[attachmentBasename stringByAppendingPathExtensionForType:UTTypePNG]]; + if(@available(iOS 16.0, macCatalyst 16.0, *)) + { + DDLogVerbose(@"Preparing for notification attachment(%@): converting downloaded file from svg at '%@' to png at '%@'...", typeHint, info[@"cacheFile"], pngAttachment); + image = [HelperTools renderUIImageFromSVGURL:[NSURL fileURLWithPath:info[@"cacheFile"]]]; + //the uiimage is somehow mirrored at the X-axis when received by appex --> mirror it back + if([HelperTools isAppExtension]) + { + DDLogDebug(@"We are in appex: mirroring UNNotificationAttachment image on Y axis..."); + image = [HelperTools mirrorImageOnXAxis:image]; + } + } + if(image != nil) + { + [UIImagePNGRepresentation(image) writeToFile:pngAttachment atomically:YES]; + typeHint = UTTypePNG; + notificationAttachment = pngAttachment; + } + } + //fallback if svg extraction failed OR it wasn't an SVG image in the first place + if(image == nil) { - DDLogError(@"Could not hardlink cache file to notification image temp file!"); - return nil; + DDLogVerbose(@"Preparing for notification attachment(%@): hardlinking downloaded file from '%@' to '%@'...", typeHint, info[@"cacheFile"], notificationAttachment); + error = [HelperTools hardLinkOrCopyFile:info[@"cacheFile"] to:notificationAttachment]; + if(error) + { + DDLogError(@"Could not hardlink cache file to notification image temp file!"); + return nil; + } } [HelperTools configureFileProtectionFor:notificationAttachment]; UNNotificationAttachment* attachment = [UNNotificationAttachment attachmentWithIdentifier:info[@"cacheId"] URL:[NSURL fileURLWithPath:notificationAttachment] options:@{UNNotificationAttachmentOptionsTypeHintKey:typeHint} error:&error]; diff --git a/Monal/Classes/SwiftHelpers.swift b/Monal/Classes/SwiftHelpers.swift index 862460151b..8e76fbc569 100644 --- a/Monal/Classes/SwiftHelpers.swift +++ b/Monal/Classes/SwiftHelpers.swift @@ -14,6 +14,9 @@ import CocoaLumberjackSwiftLogBackend import LibMonalRustSwiftBridge import Combine +//needed to render SVG to UIImage +import SwiftUI +import SVGView //import some defines in MLConstants.h into swift let kAppGroup = HelperTools.getObjcDefinedValue(.kAppGroup) @@ -270,6 +273,24 @@ public class SwiftHelpers: NSObject { HelperTools.handleRustPanic(withText: text, andBacktrace:backtrace) }); } + + //this is wrapped by HelperTools.renderUIImage(fromSVGURL) / [HelperTools renderUIImageFromSVGURL:] + //because MLChatImageCell wasn't able to import the monalxmpp-Swift bridging header somehow (but importing HelperTools works just fine) + @available(iOS 16.0, macCatalyst 16.0, *) + @objc(_renderUIImageFromSVGURL:) + public static func _renderUIImageFromSVG(url: URL?) -> UIImage? { + guard let url = url else { + return nil + } + guard let svgView = SVGParser.parse(contentsOf: url)?.toSwiftUI() else { + return nil + } + var image: UIImage? = nil + HelperTools.dispatchAsync(false, reentrantOn: DispatchQueue.main) { + image = ImageRenderer(content:svgView.scaledToFit().frame(width: 320, height: 200)).uiImage + } + return image + } } @objcMembers diff --git a/Monal/Classes/chatViewController.m b/Monal/Classes/chatViewController.m index 9a49abc771..6f5ee37c08 100644 --- a/Monal/Classes/chatViewController.m +++ b/Monal/Classes/chatViewController.m @@ -2629,37 +2629,37 @@ -(UISwipeActionsConfiguration*) tableView:(UITableView*) tableView trailingSwipe } -(MLBaseCell*) fileTransferCellCheckerWithInfo:(NSDictionary*)info direction:(BOOL)inDirection tableView:(UITableView*)tableView andMsg:(MLMessage*)row{ - MLBaseCell *cell = nil; - //don't crash on svg images not supported by UIImage - //TODO: use webview to display SVG - if([info[@"mimeType"] hasPrefix:@"image/"] && ![info[@"mimeType"] hasPrefix:@"image/svg"]) + MLBaseCell* cell = nil; + //svg to UIImage conversion is only supported on ios >= 16 + //--> this shows just a "picture.fill" placeholder for SVGs on ios < 16 + if(cell == nil && [info[@"mimeType"] hasPrefix:@"image/"]) { - MLChatImageCell* imageCell = (MLChatImageCell *)[self messageTableCellWithIdentifier:@"image" andInbound:inDirection fromTable:tableView]; + MLChatImageCell* imageCell = (MLChatImageCell*)[self messageTableCellWithIdentifier:@"image" andInbound:inDirection fromTable:tableView]; [imageCell initCellWithMLMessage:row]; cell = imageCell; } - else if([info[@"mimeType"] hasPrefix:@"video/"]) + if(cell == nil && [info[@"mimeType"] hasPrefix:@"video/"]) { - MLFileTransferVideoCell* videoCell = (MLFileTransferVideoCell *) [self messageTableCellWithIdentifier:@"fileTransferVideo" andInbound:inDirection fromTable:tableView]; + MLFileTransferVideoCell* videoCell = (MLFileTransferVideoCell*)[self messageTableCellWithIdentifier:@"fileTransferVideo" andInbound:inDirection fromTable:tableView]; NSString* videoStr = info[@"cacheFile"]; NSString* videoFileName = info[@"filename"]; [videoCell avplayerConfigWithUrlStr:videoStr andMimeType:info[@"mimeType"] fileName:videoFileName andVC:self]; cell = videoCell; } - else if([info[@"mimeType"] hasPrefix:@"audio/"]) + if(cell == nil && [info[@"mimeType"] hasPrefix:@"audio/"]) { //we may wan to make a new kind later but for now this is perfectly functional - MLFileTransferVideoCell* audioCell = (MLFileTransferVideoCell *) [self messageTableCellWithIdentifier:@"fileTransferAudio" andInbound:inDirection fromTable:tableView]; + MLFileTransferVideoCell* audioCell = (MLFileTransferVideoCell*)[self messageTableCellWithIdentifier:@"fileTransferAudio" andInbound:inDirection fromTable:tableView]; NSString *audioStr = info[@"cacheFile"]; NSString *audioFileName = info[@"filename"]; [audioCell avplayerConfigWithUrlStr:audioStr andMimeType:info[@"mimeType"] fileName:audioFileName andVC:self]; cell = audioCell; } - else + if(cell == nil) { - MLFileTransferTextCell* textCell = (MLFileTransferTextCell *) [self messageTableCellWithIdentifier:@"fileTransferText" andInbound:inDirection fromTable:tableView]; + MLFileTransferTextCell* textCell = (MLFileTransferTextCell*)[self messageTableCellWithIdentifier:@"fileTransferText" andInbound:inDirection fromTable:tableView]; NSString *fileSizeStr = info[@"size"]; long long fileSizeLongLongValue = fileSizeStr.longLongValue; diff --git a/Monal/Monal.xcodeproj/project.pbxproj b/Monal/Monal.xcodeproj/project.pbxproj index ef8825afb7..493d25f3f6 100644 --- a/Monal/Monal.xcodeproj/project.pbxproj +++ b/Monal/Monal.xcodeproj/project.pbxproj @@ -160,6 +160,7 @@ 84C1CD522A8F617F007076ED /* MLStreamRedirect.m in Sources */ = {isa = PBXBuildFile; fileRef = 84C1CD512A8F617F007076ED /* MLStreamRedirect.m */; }; 84C1CD542A8F6196007076ED /* MLStreamRedirect.h in Headers */ = {isa = PBXBuildFile; fileRef = 84C1CD532A8F6196007076ED /* MLStreamRedirect.h */; }; 84D31CE628653B83006D7926 /* WebRTCClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84D31CE528653B83006D7926 /* WebRTCClient.swift */; }; + 84E231F32C16A9CE00735FB7 /* SVGView in Frameworks */ = {isa = PBXBuildFile; productRef = 84E231F22C16A9CE00735FB7 /* SVGView */; }; 84E55E7D2964424E003E191A /* ActiveChatsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 261A6284176C156500059090 /* ActiveChatsViewController.m */; }; 84E55E8029644279003E191A /* ActiveChatsViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = 84E55E7F2964426D003E191A /* ActiveChatsViewController.h */; }; 84F194CE2C101A3E00F0A994 /* PromiseKit in Frameworks */ = {isa = PBXBuildFile; productRef = 84F194CD2C101A3E00F0A994 /* PromiseKit */; }; @@ -807,6 +808,7 @@ 84F194CE2C101A3E00F0A994 /* PromiseKit in Frameworks */, BE8B63D2491B1E5582965A8F /* Pods_monalxmpp.framework in Frameworks */, 849ADF412BACF0360009BCD7 /* CocoaLumberjackSwift in Frameworks */, + 84E231F32C16A9CE00735FB7 /* SVGView in Frameworks */, 849ADF432BACF0360009BCD7 /* CocoaLumberjackSwiftLogBackend in Frameworks */, 8414AE002A7ABC4300EFFCCC /* LibMonalRustSwiftBridge in Frameworks */, ); @@ -1591,6 +1593,7 @@ 849ADF402BACF0360009BCD7 /* CocoaLumberjackSwift */, 849ADF422BACF0360009BCD7 /* CocoaLumberjackSwiftLogBackend */, 84F194CD2C101A3E00F0A994 /* PromiseKit */, + 84E231F22C16A9CE00735FB7 /* SVGView */, ); productName = monalxmpp; productReference = 26CC579223A0867400ABB92A /* monalxmpp.framework */; @@ -1744,6 +1747,7 @@ 849ADF3D2BACF0360009BCD7 /* XCRemoteSwiftPackageReference "cocoalumberjack" */, 84F194CC2C101A3E00F0A994 /* XCRemoteSwiftPackageReference "PromiseKit" */, 84F194CF2C15197200F0A994 /* XCRemoteSwiftPackageReference "FrameUp" */, + 84E231F12C16A9CE00735FB7 /* XCRemoteSwiftPackageReference "SVGView" */, ); productRefGroup = 19C28FACFE9D520D11CA2CBB /* Products */; projectDirPath = ""; @@ -4630,6 +4634,14 @@ minimumVersion = 3.8.5; }; }; + 84E231F12C16A9CE00735FB7 /* XCRemoteSwiftPackageReference "SVGView" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/exyte/SVGView"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.0.6; + }; + }; 84F194CC2C101A3E00F0A994 /* XCRemoteSwiftPackageReference "PromiseKit" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/mxcl/PromiseKit"; @@ -4689,6 +4701,11 @@ package = 849ADF3D2BACF0360009BCD7 /* XCRemoteSwiftPackageReference "cocoalumberjack" */; productName = CocoaLumberjackSwiftLogBackend; }; + 84E231F22C16A9CE00735FB7 /* SVGView */ = { + isa = XCSwiftPackageProductDependency; + package = 84E231F12C16A9CE00735FB7 /* XCRemoteSwiftPackageReference "SVGView" */; + productName = SVGView; + }; 84F194CD2C101A3E00F0A994 /* PromiseKit */ = { isa = XCSwiftPackageProductDependency; package = 84F194CC2C101A3E00F0A994 /* XCRemoteSwiftPackageReference "PromiseKit" */; diff --git a/Monal/opensource.html b/Monal/opensource.html index 400482f49d..6260f3837d 100644 --- a/Monal/opensource.html +++ b/Monal/opensource.html @@ -104,6 +104,32 @@
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

+
+ SVGView: SVG parser and renderer written in SwiftUI
+ https://github.com/exyte/SVGView
+ MIT License
+
+ Copyright (c) 2020 exyte <info@exyte.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE.
+
+
FrameUP: Reframing SwiftUI Views. A collection of tools to help with layout.
https://github.com/ryanlintott/FrameUp
From d4c4d2bf79092b75a6c9e8e3c0145de777ae7818 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Mon, 10 Jun 2024 10:56:44 +0200 Subject: [PATCH 137/143] Remove deprecated UIGraphicsBeginImageContextWithOptions Replace it with UIGraphicsImageRenderer. --- Monal/Classes/HelperTools.m | 110 ++++++++++++----------------- Monal/Classes/MLImageManager.m | 24 +++---- Monal/Classes/SwiftuiHelpers.swift | 1 - 3 files changed, 55 insertions(+), 80 deletions(-) diff --git a/Monal/Classes/HelperTools.m b/Monal/Classes/HelperTools.m index f720791c79..14bcf544ff 100644 --- a/Monal/Classes/HelperTools.m +++ b/Monal/Classes/HelperTools.m @@ -1167,20 +1167,17 @@ +(UIImage* _Nullable) rotateImage:(UIImage* _Nullable) image byRadians:(CGFloat) CGAffineTransform t = CGAffineTransformMakeRotation(rotation); CGRect sizeRect = (CGRect) {.size = image.size}; CGRect destRect = CGRectApplyAffineTransform(sizeRect, t); - CGSize destinationSize = destRect.size; - //Draw image - UIGraphicsBeginImageContext(destinationSize); - CGContextRef context = UIGraphicsGetCurrentContext(); - CGContextTranslateCTM(context, destinationSize.width / 2.0f, destinationSize.height / 2.0f); - CGContextRotateCTM(context, rotation); - [image drawInRect:CGRectMake(-image.size.width / 2.0f, -image.size.height / 2.0f, image.size.width, image.size.height)]; - - //Save image (and save space) - image = nil; - image = UIGraphicsGetImageFromCurrentImageContext(); - UIGraphicsEndImageContext(); - return image; + return [[[UIGraphicsImageRenderer alloc] initWithSize:destRect.size] imageWithActions:^(UIGraphicsImageRendererContext* _Nonnull rendererContext) { + CGContextRef context = rendererContext.CGContext; + + //Move the origin to the middle of the image to apply the transformation + CGContextTranslateCTM(context, destRect.size.width / 2.0f, destRect.size.height / 2.0f); + CGContextRotateCTM(context, rotation); + + //Draw the original image into the transformed context + [image drawInRect:CGRectMake(-image.size.width / 2.0f, -image.size.height / 2.0f, image.size.width, image.size.height)]; + }]; } +(UIImage* _Nullable) mirrorImageOnXAxis:(UIImage* _Nullable) image @@ -1188,27 +1185,21 @@ +(UIImage* _Nullable) mirrorImageOnXAxis:(UIImage* _Nullable) image if(image == nil) return nil; - //Create a new image context - UIGraphicsBeginImageContextWithOptions(image.size, NO, image.scale); - CGContextRef context = UIGraphicsGetCurrentContext(); - - //Move the origin to the middle of the image to apply the transformation - CGContextTranslateCTM(context, image.size.width / 2, image.size.height / 2); - - //Apply the x-axis mirroring transform - CGContextScaleCTM(context, 1.0, -1.0); - - //Move the origin back to the bottom left corner - CGContextTranslateCTM(context, -image.size.width / 2, -image.size.height / 2); - - //Draw the original image into the transformed context - [image drawInRect:CGRectMake(0, 0, image.size.width, image.size.height)]; - - //Save image (and save space) - image = nil; - image = UIGraphicsGetImageFromCurrentImageContext(); - UIGraphicsEndImageContext(); - return image; + return [[[UIGraphicsImageRenderer alloc] initWithSize:image.size] imageWithActions:^(UIGraphicsImageRendererContext* _Nonnull rendererContext) { + CGContextRef context = rendererContext.CGContext; + + //Move the origin to the middle of the image to apply the transformation + CGContextTranslateCTM(context, image.size.width / 2, image.size.height / 2); + + //Apply the y-axis mirroring transform + CGContextScaleCTM(context, 1.0, -1.0); + + //Move the origin back to the bottom left corner + CGContextTranslateCTM(context, -image.size.width / 2, -image.size.height / 2); + + //Draw the original image into the transformed context + [image drawInRect:CGRectMake(0, 0, image.size.width, image.size.height)]; + }]; } +(UIImage* _Nullable) mirrorImageOnYAxis:(UIImage* _Nullable) image @@ -1216,45 +1207,34 @@ +(UIImage* _Nullable) mirrorImageOnYAxis:(UIImage* _Nullable) image if(image == nil) return nil; - //Create a new image context - UIGraphicsBeginImageContextWithOptions(image.size, NO, image.scale); - CGContextRef context = UIGraphicsGetCurrentContext(); - - //Move the origin to the middle of the image to apply the transformation - CGContextTranslateCTM(context, image.size.width / 2, image.size.height / 2); - - //Apply the y-axis mirroring transform - CGContextScaleCTM(context, -1.0, 1.0); - - //Move the origin back to the bottom left corner - CGContextTranslateCTM(context, -image.size.width / 2, -image.size.height / 2); - - //Draw the original image into the transformed context - [image drawInRect:CGRectMake(0, 0, image.size.width, image.size.height)]; - - //Save image (and save space) - image = nil; - image = UIGraphicsGetImageFromCurrentImageContext(); - UIGraphicsEndImageContext(); - return image; + return [[[UIGraphicsImageRenderer alloc] initWithSize:image.size] imageWithActions:^(UIGraphicsImageRendererContext* _Nonnull rendererContext) { + CGContextRef context = rendererContext.CGContext; + + //Move the origin to the middle of the image to apply the transformation + CGContextTranslateCTM(context, image.size.width / 2, image.size.height / 2); + + //Apply the y-axis mirroring transform + CGContextScaleCTM(context, -1.0, 1.0); + + //Move the origin back to the bottom left corner + CGContextTranslateCTM(context, -image.size.width / 2, -image.size.height / 2); + + //Draw the original image into the transformed context + [image drawInRect:CGRectMake(0, 0, image.size.width, image.size.height)]; + }]; } +(UIImage*) imageWithNotificationBadgeForImage:(UIImage*) image { - UIImage* finalImage; UIImage* badge = [[UIImage systemImageNamed:@"circle.fill"] imageWithTintColor:UIColor.redColor]; - UIGraphicsBeginImageContext(CGSizeMake(image.size.width, image.size.height)); - CGRect imgSize = CGRectMake(0, 0, image.size.width, image.size.height); CGRect dotSize = CGRectMake(image.size.width - 7, 0, 7, 7); - [image drawInRect:imgSize]; - [badge drawInRect:dotSize blendMode:kCGBlendModeNormal alpha:1.0]; - - finalImage = UIGraphicsGetImageFromCurrentImageContext(); - UIGraphicsEndImageContext(); - - return finalImage; + + return [[[UIGraphicsImageRenderer alloc] initWithSize:image.size] imageWithActions:^(UIGraphicsImageRendererContext* _Nonnull rendererContext) { + [image drawInRect:imgSize]; + [badge drawInRect:dotSize blendMode:kCGBlendModeNormal alpha:1.0]; + }]; } +(UIImageView*) buttonWithNotificationBadgeForImage:(UIImage*) image hasNotification:(bool) hasNotification withTapHandler: (UITapGestureRecognizer*) handler diff --git a/Monal/Classes/MLImageManager.m b/Monal/Classes/MLImageManager.m index 07215a3288..72dbe6c8d0 100644 --- a/Monal/Classes/MLImageManager.m +++ b/Monal/Classes/MLImageManager.m @@ -38,20 +38,16 @@ +(MLImageManager*) sharedInstance //this mehod should *only* be used in the mainapp due to memory requirements for large images +(UIImage*) circularImage:(UIImage*) image { - UIImage* composedImage; - UIGraphicsBeginImageContextWithOptions(image.size, NO, 0); - - UIBezierPath* clipPath = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(0, 0, image.size.width, image.size.height)]; - [clipPath addClip]; - - // Flip coordinates before drawing image as UIKit and CoreGraphics have inverted coordinate system - CGContextTranslateCTM(UIGraphicsGetCurrentContext(), 0, image.size.height); - CGContextScaleCTM(UIGraphicsGetCurrentContext(), 1, -1); - - CGContextDrawImage(UIGraphicsGetCurrentContext(), CGRectMake(0, 0, image.size.width, image.size.height), image.CGImage); - composedImage = UIGraphicsGetImageFromCurrentImageContext(); - UIGraphicsEndImageContext(); - return composedImage; + return [[[UIGraphicsImageRenderer alloc] initWithSize:image.size] imageWithActions:^(UIGraphicsImageRendererContext* _Nonnull rendererContext) { + UIBezierPath* clipPath = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(0, 0, image.size.width, image.size.height)]; + [clipPath addClip]; + + //Flip coordinates before drawing image as UIKit and CoreGraphics have inverted coordinate system + CGContextTranslateCTM(rendererContext.CGContext, 0, image.size.height); + CGContextScaleCTM(rendererContext.CGContext, 1, -1); + + CGContextDrawImage(rendererContext.CGContext, CGRectMake(0, 0, image.size.width, image.size.height), image.CGImage); + }]; } +(UIImage*) image:(UIImage*) image withMucOverlay:(UIImage*) overlay diff --git a/Monal/Classes/SwiftuiHelpers.swift b/Monal/Classes/SwiftuiHelpers.swift index a370d1a8e3..1afde7b542 100644 --- a/Monal/Classes/SwiftuiHelpers.swift +++ b/Monal/Classes/SwiftuiHelpers.swift @@ -43,7 +43,6 @@ extension Binding { ) } } - extension Binding { func bytecount(mappedTo: Double) -> Binding where Value == UInt { Binding( From 11ffdb99e4388c199389a7aeffa35e47b3b4da81 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Mon, 10 Jun 2024 11:05:43 +0200 Subject: [PATCH 138/143] Simplify UIHandler handling in MLMucProcessor --- Monal/Classes/MLMucProcessor.m | 136 ++++++++++----------------------- 1 file changed, 42 insertions(+), 94 deletions(-) diff --git a/Monal/Classes/MLMucProcessor.m b/Monal/Classes/MLMucProcessor.m index a3787d4a29..8d88804ff5 100644 --- a/Monal/Classes/MLMucProcessor.m +++ b/Monal/Classes/MLMucProcessor.m @@ -198,23 +198,7 @@ -(void) handleSentMessage:(NSNotification*) notification callUiHandlerFor = msg.toUser; if(callUiHandlerFor != nil) - { - monal_id_block_t uiHandler = [self getUIHandlerForMuc:callUiHandlerFor]; - if(uiHandler) - { - //remove handler (it will only be called once) - [self removeUIHandlerForMuc:callUiHandlerFor]; - - DDLogInfo(@"Calling UI handler for muc %@...", callUiHandlerFor); - dispatch_async(dispatch_get_main_queue(), ^{ - uiHandler(@{ - @"success": @YES, - @"muc": callUiHandlerFor, - @"account": self->_account - }); - }); - } - } + [self callSuccessUIHandlerForMuc:callUiHandlerFor]; } -(BOOL) isCreating:(NSString*) room @@ -638,21 +622,7 @@ -(void) configureMuc:(NSString*) roomJid withMandatoryOptions:(NSDictionary*) ma @"roomJid": [NSString stringWithFormat:@"%@", roomJid], })); - monal_id_block_t uiHandler = [self getUIHandlerForMuc:iqNode.fromUser]; - if(uiHandler) - { - //remove handler (it will only be called once) - [self removeUIHandlerForMuc:iqNode.fromUser]; - - DDLogInfo(@"Calling UI handler for muc %@...", iqNode.fromUser); - dispatch_async(dispatch_get_main_queue(), ^{ - uiHandler(@{ - @"success": @YES, - @"muc": iqNode.fromUser, - @"account": self->_account - }); - }); - } + [self callSuccessUIHandlerForMuc:iqNode.fromUser]; if(joinOnSuccess) { @@ -915,21 +885,7 @@ -(void) handleStatusCodes:(XMPPStanza*) node [_account sendIq:discoInfo withHandler:$newHandler(self, handleMembersList, $ID(type))]; } - monal_id_block_t uiHandler = [self getUIHandlerForMuc:node.fromUser]; - if(uiHandler) - { - //remove handler (it will only be called once) - [self removeUIHandlerForMuc:node.fromUser]; - - DDLogInfo(@"Calling UI handler for muc %@...", node.fromUser); - dispatch_async(dispatch_get_main_queue(), ^{ - uiHandler(@{ - @"success": @YES, - @"muc": node.fromUser, - @"account": self->_account - }); - }); - } + [self callSuccessUIHandlerForMuc:node.fromUser]; //MAYBE TODO: send out notification indicating we joined that room @@ -1110,24 +1066,12 @@ -(void) destroyRoom:(NSString*) room } DDLogInfo(@"Successfully destroyed room '%@' on account %@", room, account); - monal_id_block_t uiHandler = [self getUIHandlerForMuc:room]; - if(uiHandler) + if([self getUIHandlerForMuc:room] != nil) { - //remove handler (it will only be called once) - [self removeUIHandlerForMuc:room]; - - DDLogInfo(@"Calling UI handler for muc %@...", room); - dispatch_async(dispatch_get_main_queue(), ^{ - uiHandler(@{ - @"success": @YES, - @"muc": room, - @"account": self->_account, - @"callback": ^{ - //don't even keep our bookmark in this case - [self deleteMuc:room withBookmarksUpdate:YES keepBuddylistEntry:NO]; - }, - }); - }); + [self callSuccessUIHandlerForMuc:room withCallback:^{ + //don't even keep our bookmark in this case + [self deleteMuc:room withBookmarksUpdate:YES keepBuddylistEntry:NO]; + }]; } else { @@ -1328,21 +1272,7 @@ -(void) setAffiliation:(NSString*) affiliation ofUser:(NSString*) jid inMuc:(NSS return; } DDLogInfo(@"Successfully changed affiliation of '%@' in '%@' to '%@'", jid, roomJid, affiliation); - monal_id_block_t uiHandler = [self getUIHandlerForMuc:iqNode.fromUser]; - if(uiHandler) - { - //remove handler (it will only be called once) - [self removeUIHandlerForMuc:iqNode.fromUser]; - - DDLogInfo(@"Calling UI handler for muc %@...", iqNode.fromUser); - dispatch_async(dispatch_get_main_queue(), ^{ - uiHandler(@{ - @"success": @YES, - @"muc": iqNode.fromUser, - @"account": self->_account - }); - }); - } + [self callSuccessUIHandlerForMuc:iqNode.fromUser]; $$ -(void) changeNameOfMuc:(NSString*) room to:(NSString*) name @@ -1393,21 +1323,7 @@ -(void) publishAvatar:(UIImage* _Nullable) image forMuc:(NSString*) room return; } DDLogInfo(@"Successfully published avatar for muc: %@", iqNode.fromUser); - monal_id_block_t uiHandler = [self getUIHandlerForMuc:iqNode.fromUser]; - if(uiHandler) - { - //remove handler (it will only be called once) - [self removeUIHandlerForMuc:iqNode.fromUser]; - - DDLogInfo(@"Calling UI handler for muc %@...", iqNode.fromUser); - dispatch_async(dispatch_get_main_queue(), ^{ - uiHandler(@{ - @"success": @YES, - @"muc": iqNode.fromUser, - @"account": self->_account - }); - }); - } + [self callSuccessUIHandlerForMuc:iqNode.fromUser]; $$ $$instance_handler(handleDiscoResponseInvalidation, account.mucProcessor, $$ID(xmpp*, account), $$ID(NSString*, roomJid)) @@ -1775,6 +1691,38 @@ -(void) handleError:(NSString*) description forMuc:(NSString*) room withNode:(XM [HelperTools postError:description withNode:node andAccount:_account andIsSevere:isSevere]; } +-(void) callSuccessUIHandlerForMuc:(NSString*) room withCallback:(monal_void_block_t) callback +{ + monal_id_block_t uiHandler = [self getUIHandlerForMuc:room]; + if(uiHandler) + { + //remove handler (it will only be called once) + [self removeUIHandlerForMuc:room]; + + DDLogInfo(@"Calling UI handler for muc %@...", room); + dispatch_async(dispatch_get_main_queue(), ^{ + if(callback != nil) + uiHandler(@{ + @"success": @YES, + @"muc": room, + @"account": self->_account, + @"callback": callback, + }); + else + uiHandler(@{ + @"success": @YES, + @"muc": room, + @"account": self->_account, + }); + }); + } +} + +-(void) callSuccessUIHandlerForMuc:(NSString*) room +{ + +} + -(void) updateBookmarks { DDLogVerbose(@"Updating bookmarks on account %@", _account); From f7eea5b975510c4e7585ae91cb9ca9699046aecc Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Mon, 10 Jun 2024 12:52:27 +0200 Subject: [PATCH 139/143] Cleanup connectionProperties, fixes #1056 --- Monal/Classes/ActiveChatsViewController.m | 4 +- .../MLBlockedUsersTableViewController.m | 7 +- Monal/Classes/MLContact.m | 2 +- Monal/Classes/MLIQProcessor.m | 37 ++--- Monal/Classes/MLMessageProcessor.m | 2 +- Monal/Classes/MLServerDetails.m | 16 +- Monal/Classes/MLXMPPConnection.h | 17 +-- Monal/Classes/MLXMPPConnection.m | 5 +- Monal/Classes/xmpp.m | 140 +++++------------- 9 files changed, 76 insertions(+), 154 deletions(-) diff --git a/Monal/Classes/ActiveChatsViewController.m b/Monal/Classes/ActiveChatsViewController.m index 38bcf37b88..c09d3861bb 100755 --- a/Monal/Classes/ActiveChatsViewController.m +++ b/Monal/Classes/ActiveChatsViewController.m @@ -454,7 +454,7 @@ -(void) showWarningsIfNeeded if(![_mamWarningDisplayed containsObject:accountNo] && account.accountState >= kStateBound && account.connectionProperties.accountDiscoDone) { - if(!account.connectionProperties.supportsMam2) + if(![account.connectionProperties.accountDiscoFeatures containsObject:@"urn:xmpp:mam:2"]) { UIAlertController* messageAlert = [UIAlertController alertControllerWithTitle:[NSString stringWithFormat:NSLocalizedString(@"Account %@", @""), account.connectionProperties.identity.jid] message:NSLocalizedString(@"Your server does not support MAM (XEP-0313). That means you could frequently miss incoming messages!! You should switch your server or talk to the server admin to enable this!", @"") preferredStyle:UIAlertControllerStyleAlert]; [messageAlert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Close", @"") style:UIAlertActionStyleCancel handler:^(UIAlertAction* action __unused) { @@ -482,7 +482,7 @@ -(void) showWarningsIfNeeded if(![_pushWarningDisplayed containsObject:accountNo] && account.accountState >= kStateBound && account.connectionProperties.accountDiscoDone) { - if(!account.connectionProperties.supportsMam2) + if(![account.connectionProperties.accountDiscoFeatures containsObject:@"urn:xmpp:push:0"]) { UIAlertController* messageAlert = [UIAlertController alertControllerWithTitle:[NSString stringWithFormat:NSLocalizedString(@"Account %@", @""), account.connectionProperties.identity.jid] message:NSLocalizedString(@"Your server does not support PUSH (XEP-0357). That means you have to manually open the app to retrieve new incoming messages!! You should switch your server or talk to the server admin to enable this!", @"") preferredStyle:UIAlertControllerStyleAlert]; [messageAlert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Close", @"") style:UIAlertActionStyleCancel handler:^(UIAlertAction* action __unused) { diff --git a/Monal/Classes/MLBlockedUsersTableViewController.m b/Monal/Classes/MLBlockedUsersTableViewController.m index 0a28598d78..de6fe7d17e 100644 --- a/Monal/Classes/MLBlockedUsersTableViewController.m +++ b/Monal/Classes/MLBlockedUsersTableViewController.m @@ -87,13 +87,14 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N } - (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath { - return self.xmppAccount.connectionProperties.supportsBlocking; + return [self.xmppAccount.connectionProperties.serverDiscoFeatures containsObject:@"urn:xmpp:blocking"]; } - (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath { if (editingStyle == UITableViewCellEditingStyleDelete) { - if(!self.xmppAccount.connectionProperties.supportsBlocking) return; + if(![self.xmppAccount.connectionProperties.serverDiscoFeatures containsObject:@"urn:xmpp:blocking"]) + return; // unblock jid [[MLXMPPManager sharedInstance] block:NO fullJid:self.blockedJids[indexPath.row][@"fullBlockedJid"] onAccount:self.xmppAccount.accountNo]; @@ -103,7 +104,7 @@ - (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEd - (IBAction)addBlockButton:(id)sender { - if(!self.xmppAccount.connectionProperties.supportsBlocking) + if(![self.xmppAccount.connectionProperties.serverDiscoFeatures containsObject:@"urn:xmpp:blocking"]) { // show blocking is not supported alert UIAlertController* blockUnsuported = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"Blocking is not supported by the server", @"") message:nil preferredStyle:UIAlertControllerStyleAlert]; diff --git a/Monal/Classes/MLContact.m b/Monal/Classes/MLContact.m index 6bf237a76d..c9be560dd8 100644 --- a/Monal/Classes/MLContact.m +++ b/Monal/Classes/MLContact.m @@ -665,7 +665,7 @@ -(BOOL) toggleBlocked:(BOOL) block xmpp* account = [[MLXMPPManager sharedInstance] getConnectedAccountForID:self.accountId]; if(account == nil) return NO; - if(!account.connectionProperties.supportsBlocking) + if(![account.connectionProperties.serverDiscoFeatures containsObject:@"urn:xmpp:blocking"]) return NO; [[MLXMPPManager sharedInstance] block:block contact:self]; return YES; diff --git a/Monal/Classes/MLIQProcessor.m b/Monal/Classes/MLIQProcessor.m index 8aaa03660c..ce67ab5d39 100644 --- a/Monal/Classes/MLIQProcessor.m +++ b/Monal/Classes/MLIQProcessor.m @@ -115,7 +115,7 @@ +(void) processSetIq:(XMPPIQ*) iqNode forAccount:(xmpp*) account if([iqNode check:@"{urn:xmpp:blocking}block"] || [iqNode check:@"{urn:xmpp:blocking}unblock"]) { //make sure we don't process blocking updates not coming from our own account - if(account.connectionProperties.supportsBlocking && (iqNode.from == nil || [iqNode.fromUser isEqualToString:account.connectionProperties.identity.jid])) + if([account.connectionProperties.serverDiscoFeatures containsObject:@"urn:xmpp:blocking"] && (iqNode.from == nil || [iqNode.fromUser isEqualToString:account.connectionProperties.identity.jid])) { BOOL blockingUpdated = NO; // mark jid as unblocked @@ -407,7 +407,7 @@ +(BOOL) processRosterWithAccount:(xmpp*) account andIqNode:(XMPPIQ*) iqNode } NSSet* features = [NSSet setWithArray:[iqNode find:@"{http://jabber.org/protocol/disco#info}query/feature@var"]]; - account.connectionProperties.accountFeatures = features; + account.connectionProperties.accountDiscoFeatures = features; if( [iqNode check:@"{http://jabber.org/protocol/disco#info}query/identity"] && //xep-0163 support @@ -461,14 +461,12 @@ +(BOOL) processRosterWithAccount:(xmpp*) account andIqNode:(XMPPIQ*) iqNode if([features containsObject:@"urn:xmpp:push:0"]) { DDLogInfo(@"supports push"); - account.connectionProperties.supportsPush = YES; [account enablePush]; } if([features containsObject:@"urn:xmpp:mam:2"]) { DDLogInfo(@"supports mam:2"); - account.connectionProperties.supportsMam2 = YES; //query mam since last received stanza ID because we could not resume the smacks session //(we would not have landed here if we were able to resume the smacks session) @@ -513,7 +511,7 @@ +(BOOL) processRosterWithAccount:(xmpp*) account andIqNode:(XMPPIQ*) iqNode } NSSet* features = [NSSet setWithArray:[iqNode find:@"{http://jabber.org/protocol/disco#info}query/feature@var"]]; - account.connectionProperties.serverFeatures = features; + account.connectionProperties.serverDiscoFeatures = features; if([features containsObject:@"urn:xmpp:carbons:2"]) { @@ -527,17 +525,8 @@ +(BOOL) processRosterWithAccount:(xmpp*) account andIqNode:(XMPPIQ*) iqNode } } - if([features containsObject:@"urn:xmpp:ping"]) - account.connectionProperties.supportsPing = YES; - - if([features containsObject:@"urn:xmpp:extdisco:2"]) - account.connectionProperties.supportsExternalServiceDiscovery = YES; - if([features containsObject:@"urn:xmpp:blocking"]) - { - account.connectionProperties.supportsBlocking = YES; [account fetchBlocklist]; - } if(!account.connectionProperties.supportsHTTPUpload && [features containsObject:@"urn:xmpp:http:upload:0"]) { @@ -547,6 +536,10 @@ +(BOOL) processRosterWithAccount:(xmpp*) account andIqNode:(XMPPIQ*) iqNode account.connectionProperties.uploadSize = [[iqNode findFirst:@"{http://jabber.org/protocol/disco#info}query/\\{urn:xmpp:http:upload:0}result@max-file-size\\|int"] integerValue]; DDLogInfo(@"Upload max filesize: %lu", account.connectionProperties.uploadSize); } + + //query external services to learn stun/turn servers + if([features containsObject:@"urn:xmpp:extdisco:2"]) + [account queryExternalServicesOn:iqNode.fromUser]; $$ $$class_handler(handleServiceDiscoInfo, $$ID(xmpp*, account), $$ID(XMPPIQ*, iqNode)) @@ -563,6 +556,10 @@ +(BOOL) processRosterWithAccount:(xmpp*) account andIqNode:(XMPPIQ*) iqNode if([features containsObject:@"http://jabber.org/protocol/muc"]) account.connectionProperties.conferenceServers[iqNode.fromUser] = [iqNode findFirst:@"{http://jabber.org/protocol/disco#info}query"]; + + //query external services to learn stun/turn servers + if([features containsObject:@"urn:xmpp:extdisco:2"]) + [account queryExternalServicesOn:iqNode.fromUser]; $$ $$class_handler(handleServerDiscoItems, $$ID(xmpp*, account), $$ID(XMPPIQ*, iqNode)) @@ -576,8 +573,6 @@ +(BOOL) processRosterWithAccount:(xmpp*) account andIqNode:(XMPPIQ*) iqNode [discoInfo setiqTo:[item objectForKey:@"jid"]]; [discoInfo setDiscoInfoNode]; [account sendIq:discoInfo withHandler:$newHandler(self, handleServiceDiscoInfo)]; - - [account queryExternalServicesOn:[item objectForKey:@"jid"]]; } } $$ @@ -675,8 +670,11 @@ +(BOOL) processRosterWithAccount:(xmpp*) account andIqNode:(XMPPIQ*) iqNode $$ $$class_handler(handleBlocklist, $$ID(xmpp*, account), $$ID(XMPPIQ*, iqNode)) - if(!account.connectionProperties.supportsBlocking) + if(![account.connectionProperties.serverDiscoFeatures containsObject:@"urn:xmpp:blocking"]) + { + DDLogWarn(@"Ignoring blocklist update, server does not announce support for blocking!"); return; + } if([iqNode check:@"/"]) { @@ -698,8 +696,11 @@ +(BOOL) processRosterWithAccount:(xmpp*) account andIqNode:(XMPPIQ*) iqNode $$ $$class_handler(handleBlocked, $$ID(xmpp*, account), $$ID(XMPPIQ*, iqNode), $$ID(NSString*, blockedJid)) - if(!account.connectionProperties.supportsBlocking) + if(![account.connectionProperties.serverDiscoFeatures containsObject:@"urn:xmpp:blocking"]) + { + DDLogWarn(@"Ignoring block result, server does not announce support for blocking!"); return; + } if([iqNode check:@"/"]) { diff --git a/Monal/Classes/MLMessageProcessor.m b/Monal/Classes/MLMessageProcessor.m index 8ac9906a3c..51141e8708 100644 --- a/Monal/Classes/MLMessageProcessor.m +++ b/Monal/Classes/MLMessageProcessor.m @@ -475,7 +475,7 @@ +(MLMessage* _Nullable) processMessage:(XMPPMessage*) messageNode andOuterMessag { //ignore tombstones if not supported by server (someone probably faked them) if( - (!possiblyUnknownContact.isGroup && [account.connectionProperties.accountFeatures containsObject:@"urn:xmpp:message-retract:1#tombstone"]) || + (!possiblyUnknownContact.isGroup && [account.connectionProperties.accountDiscoFeatures containsObject:@"urn:xmpp:message-retract:1#tombstone"]) || (possiblyUnknownContact.isGroup && [[account.mucProcessor getRoomFeaturesForMuc:possiblyUnknownContact.contactJid] containsObject:@"urn:xmpp:message-retract:1#tombstone"]) ) { diff --git a/Monal/Classes/MLServerDetails.m b/Monal/Classes/MLServerDetails.m index 6b067c7c0a..0c75203035 100644 --- a/Monal/Classes/MLServerDetails.m +++ b/Monal/Classes/MLServerDetails.m @@ -93,7 +93,7 @@ -(void) checkServerCaps:(MLXMPPConnection*) connection [self.serverCaps addObject:@{ @"Title":NSLocalizedString(@"XEP-0191: Blocking Command", @""), @"Description":NSLocalizedString(@"XMPP protocol extension for communications blocking.", @""), - @"Color": connection.supportsBlocking ? SERVER_DETAILS_COLOR_OK : SERVER_DETAILS_COLOR_ERROR + @"Color": [connection.serverDiscoFeatures containsObject:@"urn:xmpp:blocking"] ? SERVER_DETAILS_COLOR_OK : SERVER_DETAILS_COLOR_ERROR }]; // supportsSM3 @@ -107,21 +107,21 @@ -(void) checkServerCaps:(MLXMPPConnection*) connection [self.serverCaps addObject:@{ @"Title":NSLocalizedString(@"XEP-0199: XMPP Ping", @""), @"Description":NSLocalizedString(@"XMPP protocol extension for sending application-level pings over XML streams.", @""), - @"Color": connection.supportsPing ? SERVER_DETAILS_COLOR_OK : SERVER_DETAILS_COLOR_ERROR + @"Color": [connection.serverDiscoFeatures containsObject:@"urn:xmpp:ping"] ? SERVER_DETAILS_COLOR_OK : SERVER_DETAILS_COLOR_ERROR }]; // supportsExternalServiceDiscovery [self.serverCaps addObject:@{ @"Title":NSLocalizedString(@"XEP-0215: External Service Discovery", @""), @"Description":NSLocalizedString(@"XMPP protocol extension for discovering services external to the XMPP network, like STUN or TURN servers needed for A/V calls.", @""), - @"Color": connection.supportsExternalServiceDiscovery ? SERVER_DETAILS_COLOR_OK : SERVER_DETAILS_COLOR_ERROR + @"Color": [connection.serverDiscoFeatures containsObject:@"urn:xmpp:extdisco:2"] ? SERVER_DETAILS_COLOR_OK : SERVER_DETAILS_COLOR_ERROR }]; // supportsRosterVersion [self.serverCaps addObject:@{ @"Title":NSLocalizedString(@"XEP-0237: Roster Versioning", @""), @"Description":NSLocalizedString(@"Defines a proposed modification to the XMPP roster protocol that enables versioning of rosters such that the server will not send the roster to the client if the roster has not been modified.", @""), - @"Color": connection.supportsRosterVersion ? SERVER_DETAILS_COLOR_OK : SERVER_DETAILS_COLOR_ERROR + @"Color": [connection.serverFeatures check:@"{urn:xmpp:features:rosterver}ver"] ? SERVER_DETAILS_COLOR_OK : SERVER_DETAILS_COLOR_ERROR }]; // usingCarbons2 @@ -135,21 +135,21 @@ -(void) checkServerCaps:(MLXMPPConnection*) connection [self.serverCaps addObject:@{ @"Title":NSLocalizedString(@"XEP-0313: Message Archive Management", @""), @"Description":NSLocalizedString(@"Access message archives on the server.", @""), - @"Color": connection.supportsMam2 ? SERVER_DETAILS_COLOR_OK : SERVER_DETAILS_COLOR_ERROR + @"Color": [connection.accountDiscoFeatures containsObject:@"urn:xmpp:mam:2"] ? SERVER_DETAILS_COLOR_OK : SERVER_DETAILS_COLOR_ERROR }]; // supportsClientState [self.serverCaps addObject:@{ @"Title":NSLocalizedString(@"XEP-0352: Client State Indication", @""), @"Description":NSLocalizedString(@"Indicate when a particular device is active or inactive. Saves battery.", @""), - @"Color": connection.supportsClientState ? SERVER_DETAILS_COLOR_OK : SERVER_DETAILS_COLOR_ERROR + @"Color": [connection.serverFeatures check:@"{urn:xmpp:csi:0}csi"] ? SERVER_DETAILS_COLOR_OK : SERVER_DETAILS_COLOR_ERROR }]; // supportsPush / pushEnabled [self.serverCaps addObject:@{ @"Title":NSLocalizedString(@"XEP-0357: Push Notifications", @""), @"Description":NSLocalizedString(@"Receive push notifications via Apple even when disconnected. Vastly improves reliability.", @""), - @"Color": connection.supportsPush ? (connection.pushEnabled ? SERVER_DETAILS_COLOR_OK : SERVER_DETAILS_COLOR_NON_IDEAL) : SERVER_DETAILS_COLOR_ERROR + @"Color": [connection.accountDiscoFeatures containsObject:@"urn:xmpp:push:0"] ? (connection.pushEnabled ? SERVER_DETAILS_COLOR_OK : SERVER_DETAILS_COLOR_NON_IDEAL) : SERVER_DETAILS_COLOR_ERROR }]; // supportsHTTPUpload @@ -163,7 +163,7 @@ -(void) checkServerCaps:(MLXMPPConnection*) connection [self.serverCaps addObject:@{ @"Title":NSLocalizedString(@"XEP-0379: Pre-Authenticated Roster Subscription", @""), @"Description":NSLocalizedString(@"Defines a protocol and URI scheme for pre-authenticated roster links that allow a third party to automatically obtain the user's presence subscription.", @""), - @"Color": connection.supportsRosterPreApproval ? SERVER_DETAILS_COLOR_OK : SERVER_DETAILS_COLOR_ERROR + @"Color": [connection.serverFeatures check:@"{urn:xmpp:features:pre-approval}sub"] ? SERVER_DETAILS_COLOR_OK : SERVER_DETAILS_COLOR_ERROR }]; // supportsSSDP diff --git a/Monal/Classes/MLXMPPConnection.h b/Monal/Classes/MLXMPPConnection.h index 8a6dce3592..2f22196597 100644 --- a/Monal/Classes/MLXMPPConnection.h +++ b/Monal/Classes/MLXMPPConnection.h @@ -13,6 +13,7 @@ NS_ASSUME_NONNULL_BEGIN @class MLContactSoftwareVersionInfo; +@class MLXMLNode; /** A class to hold the the identity, host, state and discovered properties of an xmpp connection @@ -29,8 +30,9 @@ NS_ASSUME_NONNULL_BEGIN */ //server details -@property (nonatomic, strong) NSSet* serverFeatures; -@property (nonatomic, strong) NSSet* accountFeatures; +@property (nonatomic, strong) MLXMLNode* serverFeatures; +@property (nonatomic, strong) NSSet* accountDiscoFeatures; +@property (nonatomic, strong) NSSet* serverDiscoFeatures; @property (nonatomic, strong) NSMutableArray* discoveredServices; @property (nonatomic, strong) NSMutableArray* discoveredStunTurnServers; @@ -43,26 +45,15 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, strong) NSString* _Nullable uploadServer; @property (nonatomic, assign) NSInteger uploadSize; -// client state -@property (nonatomic, assign) BOOL supportsClientState; -//message archive -@property (nonatomic, assign) BOOL supportsMam2; @property (nonatomic, assign) BOOL supportsSM3; -@property (nonatomic, assign) BOOL supportsPush; @property (nonatomic, assign) BOOL pushEnabled; @property (nonatomic, assign) BOOL supportsBookmarksCompat; @property (nonatomic, assign) BOOL usingCarbons2; -@property (nonatomic, assign) BOOL supportsRosterVersion; -@property (nonatomic, assign) BOOL supportsRosterPreApproval; @property (nonatomic, strong) NSString* serverIdentity; -@property (nonatomic, assign) BOOL supportsBlocking; -@property (nonatomic, assign) BOOL supportsPing; -@property (nonatomic, assign) BOOL supportsExternalServiceDiscovery; @property (nonatomic, assign) BOOL supportsPubSub; @property (nonatomic, assign) BOOL supportsPubSubMax; @property (nonatomic, assign) BOOL supportsModernPubSub; -@property (nonatomic, assign) BOOL supportsPreauthIbr; @property (nonatomic, assign) BOOL accountDiscoDone; diff --git a/Monal/Classes/MLXMPPConnection.m b/Monal/Classes/MLXMPPConnection.m index b832a4124e..3619d5fa42 100644 --- a/Monal/Classes/MLXMPPConnection.m +++ b/Monal/Classes/MLXMPPConnection.m @@ -7,6 +7,7 @@ // #import "MLXMPPConnection.h" +#import "MLXMLNode.h" @interface MLXMPPConnection () @@ -22,7 +23,9 @@ -(id) initWithServer:(MLXMPPServer*) server andIdentity:(MLXMPPIdentity*) identi self = [super init]; self.server = server; self.identity = identity; - self.serverFeatures = [NSSet new]; + self.serverFeatures = [MLXMLNode new]; + self.accountDiscoFeatures = [NSSet new]; + self.serverDiscoFeatures = [NSSet new]; self.conferenceServers = [NSMutableDictionary new]; self.discoveredServices = [NSMutableArray new]; self.discoveredStunTurnServers = [NSMutableArray new]; diff --git a/Monal/Classes/xmpp.m b/Monal/Classes/xmpp.m index a36fec53b6..2f9a58563b 100644 --- a/Monal/Classes/xmpp.m +++ b/Monal/Classes/xmpp.m @@ -48,7 +48,7 @@ @import AVFoundation; -#define STATE_VERSION 17 +#define STATE_VERSION 18 #define CONNECT_TIMEOUT 7.0 #define IQ_TIMEOUT 60.0 NSString* const kQueueID = @"queueID"; @@ -1063,7 +1063,7 @@ -(void) disconnectWithStreamError:(MLXMLNode* _Nullable) streamError andExplicit if(self.accountState>=kStateBound) [self->_sendQueue addOperations: @[[NSBlockOperation blockOperationWithBlock:^{ //disable push for this node - if(self.connectionProperties.supportsPush) + if([self.connectionProperties.accountDiscoFeatures containsObject:@"urn:xmpp:push:0"]) [self disablePush]; [self sendLastAck]; }]] waitUntilFinished:YES]; //block until finished because we are closing the xmpp stream directly afterwards @@ -2926,15 +2926,11 @@ -(void) handleFeaturesBeforeAuth:(MLXMLNode*) parsedStanza MLAssert(!([[DataLayer sharedInstance] isSasl2PinnedForAccount:self.accountNo] && [[DataLayer sharedInstance] isPlainActivatedForAccount:self.accountNo]), @"SASL2 pinned AND plain auth enabled, that should never happen!", @{@"account": self}); - if([parsedStanza check:@"{urn:xmpp:ibr-token:0}register"]) - { - DDLogInfo(@"Server supports Pre-Authenticated IBR"); - self.connectionProperties.supportsPreauthIbr = YES; - } - + if(![parsedStanza check:@"{urn:xmpp:ibr-token:0}register"]) + DDLogWarn(@"Server NOT supporting Pre-Authenticated IBR"); if(_registration) { - if(_registrationToken && self.connectionProperties.supportsPreauthIbr) + if(_registrationToken && [parsedStanza check:@"{urn:xmpp:ibr-token:0}register"]) { DDLogInfo(@"Registration: Calling submitRegToken"); [self submitRegToken:_registrationToken]; @@ -3111,26 +3107,15 @@ -(void) handleFeaturesBeforeAuth:(MLXMLNode*) parsedStanza -(void) handleFeaturesAfterAuth:(MLXMLNode*) parsedStanza { - if([parsedStanza check:@"{urn:xmpp:csi:0}csi"]) - { - DDLogInfo(@"Server supports CSI"); - self.connectionProperties.supportsClientState = YES; - } + self.connectionProperties.serverFeatures = parsedStanza; + + //this is set to NO if we fail to enable it if([parsedStanza check:@"{urn:xmpp:sm:3}sm"]) { DDLogInfo(@"Server supports SM3"); self.connectionProperties.supportsSM3 = YES; } - if([parsedStanza check:@"{urn:xmpp:features:rosterver}ver"]) - { - DDLogInfo(@"Server supports roster versioning"); - self.connectionProperties.supportsRosterVersion = YES; - } - if([parsedStanza check:@"{urn:xmpp:features:pre-approval}sub"]) - { - DDLogInfo(@"Server supports roster pre approval"); - self.connectionProperties.supportsRosterPreApproval = YES; - } + if([parsedStanza check:@"{http://jabber.org/protocol/caps}c@node"]) { DDLogInfo(@"Server identity: %@", [parsedStanza findFirst:@"{http://jabber.org/protocol/caps}c@node"]); @@ -3493,7 +3478,8 @@ -(void) realPersistState } [values setValue:[self.connectionProperties.serverFeatures copy] forKey:@"serverFeatures"]; - [values setValue:[self.connectionProperties.accountFeatures copy] forKey:@"accountFeatures"]; + [values setValue:[self.connectionProperties.serverDiscoFeatures copy] forKey:@"serverDiscoFeatures"]; + [values setValue:[self.connectionProperties.accountDiscoFeatures copy] forKey:@"accountDiscoFeatures"]; if(self.connectionProperties.uploadServer) [values setObject:self.connectionProperties.uploadServer forKey:@"uploadServer"]; @@ -3508,18 +3494,11 @@ -(void) realPersistState [values setObject:[NSNumber numberWithBool:self->_loggedInOnce] forKey:@"loggedInOnce"]; [values setObject:[NSNumber numberWithBool:self.connectionProperties.usingCarbons2] forKey:@"usingCarbons2"]; [values setObject:[NSNumber numberWithBool:self.connectionProperties.supportsBookmarksCompat] forKey:@"supportsBookmarksCompat"]; - [values setObject:[NSNumber numberWithBool:self.connectionProperties.supportsPush] forKey:@"supportsPush"]; [values setObject:[NSNumber numberWithBool:self.connectionProperties.pushEnabled] forKey:@"pushEnabled"]; - [values setObject:[NSNumber numberWithBool:self.connectionProperties.supportsClientState] forKey:@"supportsClientState"]; - [values setObject:[NSNumber numberWithBool:self.connectionProperties.supportsMam2] forKey:@"supportsMAM"]; [values setObject:[NSNumber numberWithBool:self.connectionProperties.supportsPubSub] forKey:@"supportsPubSub"]; [values setObject:[NSNumber numberWithBool:self.connectionProperties.supportsPubSubMax] forKey:@"supportsPubSubMax"]; [values setObject:[NSNumber numberWithBool:self.connectionProperties.supportsModernPubSub] forKey:@"supportsModernPubSub"]; [values setObject:[NSNumber numberWithBool:self.connectionProperties.supportsHTTPUpload] forKey:@"supportsHTTPUpload"]; - [values setObject:[NSNumber numberWithBool:self.connectionProperties.supportsPing] forKey:@"supportsPing"]; - [values setObject:[NSNumber numberWithBool:self.connectionProperties.supportsExternalServiceDiscovery] forKey:@"supportsExternalServiceDiscovery"]; - [values setObject:[NSNumber numberWithBool:self.connectionProperties.supportsRosterPreApproval] forKey:@"supportsRosterPreApproval"]; - [values setObject:[NSNumber numberWithBool:self.connectionProperties.supportsBlocking] forKey:@"supportsBlocking"]; [values setObject:[NSNumber numberWithBool:self.connectionProperties.accountDiscoDone] forKey:@"accountDiscoDone"]; [values setObject:[self->_inCatchup copy] forKey:@"inCatchup"]; [values setObject:[self->_mdsData copy] forKey:@"mdsData"]; @@ -3554,7 +3533,7 @@ -(void) realPersistState [[DataLayer sharedInstance] persistState:values forAccount:self.accountNo]; //debug output - DDLogVerbose(@"%@ --> persistState(saved at %@):\n\tisDoingFullReconnect=%@,\n\tlastHandledInboundStanza=%@,\n\tlastHandledOutboundStanza=%@,\n\tlastOutboundStanza=%@,\n\t#unAckedStanzas=%lu%s,\n\tstreamID=%@\n\tlastInteractionDate=%@\n\tpersistentIqHandlers=%@\n\tsupportsPush=%d\n\tsupportsHttpUpload=%d\n\tpushEnabled=%d\n\tsupportsPubSub=%d\n\tsupportsModernPubSub=%d\n\tsupportsPubSubMax=%d\n\tsupportsBlocking=%d\n\tsupportsClientState=%d\n\tsupportsBookmarksCompat=%d\n\taccountDiscoDone=%d\n\t_inCatchup=%@\n\tomemo.state=%@\n\thasSeenOmemoDeviceListAfterOwnDeviceid=%@\n", + DDLogVerbose(@"%@ --> persistState(saved at %@):\n\tisDoingFullReconnect=%@,\n\tlastHandledInboundStanza=%@,\n\tlastHandledOutboundStanza=%@,\n\tlastOutboundStanza=%@,\n\t#unAckedStanzas=%lu%s,\n\tstreamID=%@\n\tlastInteractionDate=%@\n\tpersistentIqHandlers=%@\n\tsupportsHttpUpload=%d\n\tpushEnabled=%d\n\tsupportsPubSub=%d\n\tsupportsModernPubSub=%d\n\tsupportsPubSubMax=%d\n\tsupportsBookmarksCompat=%d\n\taccountDiscoDone=%d\n\t_inCatchup=%@\n\tomemo.state=%@\n\thasSeenOmemoDeviceListAfterOwnDeviceid=%@\n", self.accountNo, values[@"stateSavedAt"], bool2str(self.isDoingFullReconnect), @@ -3565,14 +3544,11 @@ -(void) realPersistState self.streamID, self->_lastInteractionDate, persistentIqHandlerDescriptions, - self.connectionProperties.supportsPush, self.connectionProperties.supportsHTTPUpload, self.connectionProperties.pushEnabled, self.connectionProperties.supportsPubSub, self.connectionProperties.supportsModernPubSub, self.connectionProperties.supportsPubSubMax, - self.connectionProperties.supportsBlocking, - self.connectionProperties.supportsClientState, self.connectionProperties.supportsBookmarksCompat, self.connectionProperties.accountDiscoDone, self->_inCatchup, @@ -3657,7 +3633,8 @@ -(void) realReadState } self.connectionProperties.serverFeatures = [dic objectForKey:@"serverFeatures"]; - self.connectionProperties.accountFeatures = [dic objectForKey:@"accountFeatures"]; + self.connectionProperties.serverDiscoFeatures = [dic objectForKey:@"serverDiscoFeatures"]; + self.connectionProperties.accountDiscoFeatures = [dic objectForKey:@"accountDiscoFeatures"]; self.connectionProperties.discoveredServices = [[dic objectForKey:@"discoveredServices"] mutableCopy]; self.connectionProperties.discoveredStunTurnServers = [[dic objectForKey:@"discoveredStunTurnServers"] mutableCopy]; @@ -3685,30 +3662,12 @@ -(void) realReadState self.connectionProperties.supportsBookmarksCompat = compatNumber.boolValue; } - if([dic objectForKey:@"supportsPush"]) - { - NSNumber* pushNumber = [dic objectForKey:@"supportsPush"]; - self.connectionProperties.supportsPush = pushNumber.boolValue; - } - if([dic objectForKey:@"pushEnabled"]) { NSNumber* pushEnabled = [dic objectForKey:@"pushEnabled"]; self.connectionProperties.pushEnabled = pushEnabled.boolValue; } - if([dic objectForKey:@"supportsClientState"]) - { - NSNumber* csiNumber = [dic objectForKey:@"supportsClientState"]; - self.connectionProperties.supportsClientState = csiNumber.boolValue; - } - - if([dic objectForKey:@"supportsMAM"]) - { - NSNumber* mamNumber = [dic objectForKey:@"supportsMAM"]; - self.connectionProperties.supportsMam2 = mamNumber.boolValue; - } - if([dic objectForKey:@"supportsPubSub"]) { NSNumber* supportsPubSub = [dic objectForKey:@"supportsPubSub"]; @@ -3733,33 +3692,9 @@ -(void) realReadState self.connectionProperties.supportsHTTPUpload = supportsHTTPUpload.boolValue; } - if([dic objectForKey:@"supportsPing"]) - { - NSNumber* supportsPing = [dic objectForKey:@"supportsPing"]; - self.connectionProperties.supportsPing = supportsPing.boolValue; - } - - if([dic objectForKey:@"supportsExternalServiceDiscovery"]) - { - NSNumber* supportsExternalServiceDiscovery = [dic objectForKey:@"supportsExternalServiceDiscovery"]; - self.connectionProperties.supportsExternalServiceDiscovery = supportsExternalServiceDiscovery.boolValue; - } - if([dic objectForKey:@"lastInteractionDate"]) _lastInteractionDate = [dic objectForKey:@"lastInteractionDate"]; - if([dic objectForKey:@"supportsRosterPreApproval"]) - { - NSNumber* supportsRosterPreApproval = [dic objectForKey:@"supportsRosterPreApproval"]; - self.connectionProperties.supportsRosterPreApproval = supportsRosterPreApproval.boolValue; - } - - if([dic objectForKey:@"supportsBlocking"]) - { - NSNumber* supportsBlocking = [dic objectForKey:@"supportsBlocking"]; - self.connectionProperties.supportsBlocking = supportsBlocking.boolValue; - } - if([dic objectForKey:@"accountDiscoDone"]) { NSNumber* accountDiscoDone = [dic objectForKey:@"accountDiscoDone"]; @@ -3799,7 +3734,7 @@ -(void) realReadState } //debug output - DDLogVerbose(@"%@ --> readState(saved at %@):\n\tisDoingFullReconnect=%@,\n\tlastHandledInboundStanza=%@,\n\tlastHandledOutboundStanza=%@,\n\tlastOutboundStanza=%@,\n\t#unAckedStanzas=%lu%s,\n\tstreamID=%@,\n\tlastInteractionDate=%@\n\tpersistentIqHandlers=%@\n\tsupportsPush=%d\n\tsupportsHttpUpload=%d\n\tpushEnabled=%d\n\tsupportsPubSub=%d\n\tsupportsModernPubSub=%d\n\tsupportsPubSubMax=%d\n\tsupportsBlocking=%d\n\tsupportsClientSate=%d\n\tsupportsBookmarksCompat=%d\n\taccountDiscoDone=%d\n\t_inCatchup=%@\n\tomemo.state=%@\n\thasSeenOmemoDeviceListAfterOwnDeviceid=%@\n", + DDLogVerbose(@"%@ --> readState(saved at %@):\n\tisDoingFullReconnect=%@,\n\tlastHandledInboundStanza=%@,\n\tlastHandledOutboundStanza=%@,\n\tlastOutboundStanza=%@,\n\t#unAckedStanzas=%lu%s,\n\tstreamID=%@,\n\tlastInteractionDate=%@\n\tpersistentIqHandlers=%@\n\tsupportsHttpUpload=%d\n\tpushEnabled=%d\n\tsupportsPubSub=%d\n\tsupportsModernPubSub=%d\n\tsupportsPubSubMax=%d\n\tsupportsBookmarksCompat=%d\n\taccountDiscoDone=%d\n\t_inCatchup=%@\n\tomemo.state=%@\n\thasSeenOmemoDeviceListAfterOwnDeviceid=%@\n", self.accountNo, dic[@"stateSavedAt"], bool2str(self.isDoingFullReconnect), @@ -3810,14 +3745,11 @@ -(void) realReadState self.streamID, self->_lastInteractionDate, persistentIqHandlerDescriptions, - self.connectionProperties.supportsPush, self.connectionProperties.supportsHTTPUpload, self.connectionProperties.pushEnabled, self.connectionProperties.supportsPubSub, self.connectionProperties.supportsModernPubSub, self.connectionProperties.supportsPubSubMax, - self.connectionProperties.supportsBlocking, - self.connectionProperties.supportsClientState, self.connectionProperties.supportsBookmarksCompat, self.connectionProperties.accountDiscoDone, self->_inCatchup, @@ -3940,8 +3872,8 @@ -(void) bindResource:(NSString*) resource //--> all of this reasons imply that we had to start a new xmpp stream and our old cached disco data // and other state values are stale now //(smacks state will be reset/cleared later on if appropriate, no need to handle smacks here) - self.connectionProperties.serverFeatures = [NSSet new]; - self.connectionProperties.accountFeatures = [NSSet new]; + self.connectionProperties.serverDiscoFeatures = [NSSet new]; + self.connectionProperties.accountDiscoFeatures = [NSSet new]; self.connectionProperties.discoveredServices = [NSMutableArray new]; self.connectionProperties.discoveredStunTurnServers = [NSMutableArray new]; self.connectionProperties.discoveredAdhocCommands = [NSMutableDictionary new]; @@ -3949,23 +3881,14 @@ -(void) bindResource:(NSString*) resource self.connectionProperties.conferenceServers = [NSMutableDictionary new]; self.connectionProperties.supportsHTTPUpload = NO; self.connectionProperties.uploadServer = nil; - //self.connectionProperties.supportsClientState = NO; //already set by stream feature parsing - self.connectionProperties.supportsMam2 = NO; //self.connectionProperties.supportsSM3 = NO; //already set by stream feature parsing - self.connectionProperties.supportsPush = NO; self.connectionProperties.pushEnabled = NO; self.connectionProperties.supportsBookmarksCompat = NO; self.connectionProperties.usingCarbons2 = NO; - //self.connectionProperties.supportsRosterVersion = NO; //already set by stream feature parsing - //self.connectionProperties.supportsRosterPreApproval = NO; //already set by stream feature parsing //self.connectionProperties.serverIdentity = @""; //already set by stream feature parsing - self.connectionProperties.supportsBlocking = NO; - self.connectionProperties.supportsPing = NO; - self.connectionProperties.supportsExternalServiceDiscovery = NO; self.connectionProperties.supportsPubSub = NO; self.connectionProperties.supportsPubSubMax = NO; self.connectionProperties.supportsModernPubSub = NO; - //self.connectionProperties.supportsPreauthIbr = NO; //already set by stream feature parsing self.connectionProperties.accountDiscoDone = NO; //clear list of running mam queries @@ -4072,7 +3995,7 @@ -(void) fetchRoster { XMPPIQ* roster = [[XMPPIQ alloc] initWithType:kiqGetType]; NSString* rosterVer; - if(self.connectionProperties.supportsRosterVersion) + if([self.connectionProperties.serverFeatures check:@"{urn:xmpp:features:rosterver}ver"]) rosterVer = [[DataLayer sharedInstance] getRosterVersionForAccount:self.accountNo]; [roster setRosterRequest:rosterVer]; [self sendIq:roster withHandler:$newHandler(MLIQProcessor, handleRoster)]; @@ -4109,9 +4032,6 @@ -(void) initSession //--> no holes in our history can be caused by these offline messages in conjunction with mam catchup, // however all offline messages will be received twice (as offline message AND via mam catchup) - //query external services to learn stun/turn servers - [self queryExternalServicesOn:self.connectionProperties.identity.domain]; - //send own csi state (this must be done *after* presences to not delay/filter incoming presence flood needed to prime our database [self sendCurrentCSIState]; @@ -4150,8 +4070,11 @@ -(void) addReconnectionHandler:(MLHandler*) handler -(void) setBlocked:(BOOL) blocked forJid:(NSString* _Nonnull) blockedJid { - if(!self.connectionProperties.supportsBlocking) + if(![self.connectionProperties.serverDiscoFeatures containsObject:@"urn:xmpp:blocking"]) + { + DDLogWarn(@"Server does not support blocking..."); return; + } XMPPIQ* iqBlocked = [[XMPPIQ alloc] initWithType:kiqSetType]; @@ -4161,8 +4084,11 @@ -(void) setBlocked:(BOOL) blocked forJid:(NSString* _Nonnull) blockedJid -(void) fetchBlocklist { - if(!self.connectionProperties.supportsBlocking) + if(![self.connectionProperties.serverDiscoFeatures containsObject:@"urn:xmpp:blocking"]) + { + DDLogWarn(@"Server does not support blocking..."); return; + } XMPPIQ* iqBlockList = [[XMPPIQ alloc] initWithType:kiqGetType]; @@ -4298,9 +4224,9 @@ -(void) sendCurrentCSIState { [self dispatchOnReceiveQueue: ^{ //don't send anything before a resource is bound - if(self.accountState*) unread } //only send chatmarkers if requested by contact - BOOL assistedMDS = [self.connectionProperties.accountFeatures containsObject:@"urn:xmpp:mds:server-assist:0"] && lastMarkableMessage == lastUnreadMessage; + BOOL assistedMDS = [self.connectionProperties.accountDiscoFeatures containsObject:@"urn:xmpp:mds:server-assist:0"] && lastMarkableMessage == lastUnreadMessage; if(lastMarkableMessage != nil) { XMPPMessage* displayedNode = [[XMPPMessage alloc] initToContact:contact]; From a9fb4bddb63638de692a88cc285ffeee2d11e380 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Mon, 10 Jun 2024 13:27:01 +0200 Subject: [PATCH 140/143] Support SVG avatars, too --- Monal/Classes/HelperTools.h | 1 + Monal/Classes/HelperTools.m | 6 +++++ Monal/Classes/MLPubSubProcessor.m | 42 +++++++++++++++++++++++-------- Monal/Classes/SwiftHelpers.swift | 18 +++++++++++++ 4 files changed, 57 insertions(+), 10 deletions(-) diff --git a/Monal/Classes/HelperTools.h b/Monal/Classes/HelperTools.h index 2f01185235..fa953819fa 100644 --- a/Monal/Classes/HelperTools.h +++ b/Monal/Classes/HelperTools.h @@ -93,6 +93,7 @@ void swizzle(Class c, SEL orig, SEL new); +(NSString* _Nullable) xml2candidate:(MLXMLNode*) xml withInitiator:(BOOL) initiator; +(UIImage* _Nullable) renderUIImageFromSVGURL:(NSURL* _Nullable) url API_AVAILABLE(ios(16.0), macosx(13.0)); //means: API_AVAILABLE(ios(16.0), maccatalyst(16.0)) ++(UIImage* _Nullable) renderUIImageFromSVGData:(NSData* _Nullable) data API_AVAILABLE(ios(16.0), macosx(13.0)); //means: API_AVAILABLE(ios(16.0), maccatalyst(16.0)) +(void) busyWaitForOperationQueue:(NSOperationQueue*) queue; +(id) getObjcDefinedValue:(MLDefinedIdentifier) identifier; +(NSRunLoop*) getExtraRunloopWithIdentifier:(MLRunLoopIdentifier) identifier; diff --git a/Monal/Classes/HelperTools.m b/Monal/Classes/HelperTools.m index 14bcf544ff..d271e2ac8a 100644 --- a/Monal/Classes/HelperTools.m +++ b/Monal/Classes/HelperTools.m @@ -534,6 +534,12 @@ +(UIImage* _Nullable) renderUIImageFromSVGURL:(NSURL* _Nullable) url API_AVAI return [SwiftHelpers _renderUIImageFromSVGURL:url]; } +//this wrapper is needed, because MLChatImageCell can't import our monalxmpp-Swift bridging header, but importing HelperTools is okay ++(UIImage* _Nullable) renderUIImageFromSVGData:(NSData* _Nullable) data API_AVAILABLE(ios(16.0), macosx(13.0)) //means: API_AVAILABLE(ios(16.0), maccatalyst(16.0)) +{ + return [SwiftHelpers _renderUIImageFromSVGData:data]; +} + +(void) busyWaitForOperationQueue:(NSOperationQueue*) queue { //apparently setting someQueue.suspended = YES does return before the queue is actually suspended diff --git a/Monal/Classes/MLPubSubProcessor.m b/Monal/Classes/MLPubSubProcessor.m index 7dc4527be3..57c765bb7b 100644 --- a/Monal/Classes/MLPubSubProcessor.m +++ b/Monal/Classes/MLPubSubProcessor.m @@ -67,7 +67,8 @@ @implementation MLPubSubProcessor { for(NSString* entry in data) { - NSString* avatarHash = [data[entry] findFirst:@"{urn:xmpp:avatar:metadata}metadata/info@id"]; + MLXMLNode* metadata = [data[entry] findFirst:@"{urn:xmpp:avatar:metadata}metadata/info"]; + NSString* avatarHash = [metadata findFirst:@"/@id"]; if(!avatarHash) //the user disabled his avatar { DDLogInfo(@"User '%@' disabled his avatar", jid); @@ -89,13 +90,13 @@ @implementation MLPubSubProcessor } //only allow a maximum of 72KiB of image data when in appex due to appex memory limits //--> ignore metadata elements bigger than this size and only hande them once not in appex anymore - NSUInteger avatarByteSize = [[data[entry] findFirst:@"{urn:xmpp:avatar:metadata}metadata/info@bytes|int"] unsignedIntegerValue]; + NSUInteger avatarByteSize = [[metadata findFirst:@"/@bytes|int"] unsignedIntegerValue]; if(![HelperTools isAppExtension] || avatarByteSize < 128 * 1024) - [account.pubsub fetchNode:@"urn:xmpp:avatar:data" from:jid withItemsList:@[avatarHash] andHandler:$newHandler(self, handleAvatarFetchResult)]; + [account.pubsub fetchNode:@"urn:xmpp:avatar:data" from:jid withItemsList:@[avatarHash] andHandler:$newHandler(self, handleAvatarFetchResult, $ID(metadata))]; else { DDLogWarn(@"Not loading avatar image of '%@' because it is too big to be handled in appex (%lu bytes), rescheduling it to be fetched in mainapp", jid, (unsigned long)avatarByteSize); - [account addReconnectionHandler:$newHandler(self, fetchAvatarAgain, $ID(jid), $ID(avatarHash))]; + [account addReconnectionHandler:$newHandler(self, fetchAvatarAgain, $ID(jid), $ID(avatarHash), $ID(metadata))]; } } break; //we only want to process the first item (this should also be the only item) @@ -117,17 +118,17 @@ @implementation MLPubSubProcessor $$ //this handler will simply retry the fetchNode for urn:xmpp:avatar:data if in mainapp -$$class_handler(fetchAvatarAgain, $$ID(xmpp*, account), $$ID(NSString*, jid), $$ID(NSString*, avatarHash)) +$$class_handler(fetchAvatarAgain, $$ID(xmpp*, account), $$ID(NSString*, jid), $$ID(NSString*, avatarHash), $$ID(MLXMLNode*, metadata)) if([HelperTools isAppExtension]) { DDLogWarn(@"Not loading avatar image of '%@' because we are still in appex, rescheduling it again!", jid); - [account addReconnectionHandler:$newHandler(self, fetchAvatarAgain, $ID(jid), $ID(avatarHash))]; + [account addReconnectionHandler:$newHandler(self, fetchAvatarAgain, $ID(jid), $ID(avatarHash), $ID(metadata))]; } else - [account.pubsub fetchNode:@"urn:xmpp:avatar:data" from:jid withItemsList:@[avatarHash] andHandler:$newHandler(self, handleAvatarFetchResult)]; + [account.pubsub fetchNode:@"urn:xmpp:avatar:data" from:jid withItemsList:@[avatarHash] andHandler:$newHandler(self, handleAvatarFetchResult, $ID(metadata))]; $$ -$$class_handler(handleAvatarFetchResult, $$ID(xmpp*, account), $$ID(NSString*, jid), $$BOOL(success), $_ID(XMPPIQ*, errorIq), $_ID(XMPPIQ*, errorReason), $_ID((NSDictionary*), data)) +$$class_handler(handleAvatarFetchResult, $$ID(xmpp*, account), $$ID(NSString*, jid), $$BOOL(success), $_ID(XMPPIQ*, errorIq), $_ID(XMPPIQ*, errorReason), $_ID((NSDictionary*), data), $$ID(MLXMLNode*, metadata)) //ignore errors here (e.g. simply don't update the avatar image) //(this should never happen if other clients and servers behave properly) if(!success) @@ -139,7 +140,28 @@ @implementation MLPubSubProcessor for(NSString* avatarHash in data) { //this should be small enough to not crash the appex when loading the image from file later on but large enough to have excellent quality - UIImage* image = [UIImage imageWithData:[data[avatarHash] findFirst:@"{urn:xmpp:avatar:data}data#|base64"]]; + NSData* avatarData = [data[avatarHash] findFirst:@"{urn:xmpp:avatar:data}data#|base64"]; + UIImage* image = nil; + if([[metadata findFirst:@"/@type"] hasPrefix:@"image/svg"]) + { + if(@available(iOS 16.0, macCatalyst 16.0, *)) + { + image = [HelperTools renderUIImageFromSVGData:avatarData]; + //the uiimage is somehow mirrored at the X-axis when received by appex --> mirror it back + if([HelperTools isAppExtension]) + { + DDLogDebug(@"We are in appex: mirroring UNNotificationAttachment image on Y axis..."); + image = [HelperTools mirrorImageOnXAxis:image]; + } + } + } + else + image = [UIImage imageWithData:avatarData]; + if(image == nil) + { + DDLogWarn(@"Failed to load avatar of %@", jid); + return; + } //this upper limit is roughly 1.4MiB memory (600x600 with 4 byte per pixel) if(![HelperTools isAppExtension] || image.size.width * image.size.height < 600 * 600) { @@ -156,7 +178,7 @@ @implementation MLPubSubProcessor else { DDLogWarn(@"Not loading avatar image of '%@' because it is too big to be processed in appex (%lux%lu pixels), rescheduling it to be fetched in mainapp", jid, (unsigned long)image.size.width, (unsigned long)image.size.height); - [account addReconnectionHandler:$newHandler(self, fetchAvatarAgain, $ID(jid), $ID(avatarHash))]; + [account addReconnectionHandler:$newHandler(self, fetchAvatarAgain, $ID(jid), $ID(avatarHash), $ID(metadata))]; } } $$ diff --git a/Monal/Classes/SwiftHelpers.swift b/Monal/Classes/SwiftHelpers.swift index 8e76fbc569..45c106455e 100644 --- a/Monal/Classes/SwiftHelpers.swift +++ b/Monal/Classes/SwiftHelpers.swift @@ -291,6 +291,24 @@ public class SwiftHelpers: NSObject { } return image } + + //this is wrapped by HelperTools.renderUIImage(fromSVGURL) / [HelperTools renderUIImageFromSVGURL:] + //because MLChatImageCell wasn't able to import the monalxmpp-Swift bridging header somehow (but importing HelperTools works just fine) + @available(iOS 16.0, macCatalyst 16.0, *) + @objc(_renderUIImageFromSVGData:) + public static func _renderUIImageFromSVG(data: Data?) -> UIImage? { + guard let data = data else { + return nil + } + guard let svgView = SVGParser.parse(data: data)?.toSwiftUI() else { + return nil + } + var image: UIImage? = nil + HelperTools.dispatchAsync(false, reentrantOn: DispatchQueue.main) { + image = ImageRenderer(content:svgView.scaledToFit().frame(width: 320, height: 200)).uiImage + } + return image + } } @objcMembers From 869f07ab170897dab4f95919da9ee47ada8b909e Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Mon, 10 Jun 2024 13:29:35 +0200 Subject: [PATCH 141/143] Increase resolution of SVGs when rendered in main app --- Monal/Classes/MLNotificationManager.m | 6 ------ Monal/Classes/MLPubSubProcessor.m | 8 -------- Monal/Classes/SwiftHelpers.swift | 17 +++++++++++++++-- 3 files changed, 15 insertions(+), 16 deletions(-) diff --git a/Monal/Classes/MLNotificationManager.m b/Monal/Classes/MLNotificationManager.m index 32b1cf74f6..762165ba87 100644 --- a/Monal/Classes/MLNotificationManager.m +++ b/Monal/Classes/MLNotificationManager.m @@ -826,12 +826,6 @@ -(UNNotificationAttachment* _Nullable) createNotificationAttachmentForFileInfo:( { DDLogVerbose(@"Preparing for notification attachment(%@): converting downloaded file from svg at '%@' to png at '%@'...", typeHint, info[@"cacheFile"], pngAttachment); image = [HelperTools renderUIImageFromSVGURL:[NSURL fileURLWithPath:info[@"cacheFile"]]]; - //the uiimage is somehow mirrored at the X-axis when received by appex --> mirror it back - if([HelperTools isAppExtension]) - { - DDLogDebug(@"We are in appex: mirroring UNNotificationAttachment image on Y axis..."); - image = [HelperTools mirrorImageOnXAxis:image]; - } } if(image != nil) { diff --git a/Monal/Classes/MLPubSubProcessor.m b/Monal/Classes/MLPubSubProcessor.m index 57c765bb7b..d96977bf9c 100644 --- a/Monal/Classes/MLPubSubProcessor.m +++ b/Monal/Classes/MLPubSubProcessor.m @@ -145,15 +145,7 @@ @implementation MLPubSubProcessor if([[metadata findFirst:@"/@type"] hasPrefix:@"image/svg"]) { if(@available(iOS 16.0, macCatalyst 16.0, *)) - { image = [HelperTools renderUIImageFromSVGData:avatarData]; - //the uiimage is somehow mirrored at the X-axis when received by appex --> mirror it back - if([HelperTools isAppExtension]) - { - DDLogDebug(@"We are in appex: mirroring UNNotificationAttachment image on Y axis..."); - image = [HelperTools mirrorImageOnXAxis:image]; - } - } } else image = [UIImage imageWithData:avatarData]; diff --git a/Monal/Classes/SwiftHelpers.swift b/Monal/Classes/SwiftHelpers.swift index 45c106455e..0ad13c020c 100644 --- a/Monal/Classes/SwiftHelpers.swift +++ b/Monal/Classes/SwiftHelpers.swift @@ -287,7 +287,13 @@ public class SwiftHelpers: NSObject { } var image: UIImage? = nil HelperTools.dispatchAsync(false, reentrantOn: DispatchQueue.main) { - image = ImageRenderer(content:svgView.scaledToFit().frame(width: 320, height: 200)).uiImage + if HelperTools.isAppExtension() { + image = ImageRenderer(content:svgView.scaledToFit().frame(width: 320, height: 200)).uiImage + DDLogDebug("We are in appex: mirroring SVG image on Y axis..."); + image = HelperTools.mirrorImage(onXAxis:image) + } else { + image = ImageRenderer(content:svgView.scaledToFit().frame(width: 1280, height: 960)).uiImage + } } return image } @@ -305,7 +311,14 @@ public class SwiftHelpers: NSObject { } var image: UIImage? = nil HelperTools.dispatchAsync(false, reentrantOn: DispatchQueue.main) { - image = ImageRenderer(content:svgView.scaledToFit().frame(width: 320, height: 200)).uiImage + //the uiimage is somehow mirrored at the X-axis when received by appex --> mirror it back + if HelperTools.isAppExtension() { + image = ImageRenderer(content:svgView.scaledToFit().frame(width: 320, height: 200)).uiImage + DDLogDebug("We are in appex: mirroring SVG image on Y axis..."); + image = HelperTools.mirrorImage(onXAxis:image) + } else { + image = ImageRenderer(content:svgView.scaledToFit().frame(width: 1280, height: 960)).uiImage + } } return image } From f0dae09cdb818b7dafe2f83a7e27a60997c39fec Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Thu, 13 Jun 2024 17:27:47 +0200 Subject: [PATCH 142/143] --- 908 --- 6.4.0rc2 From 9d32275f13a53e9d593aaf646a96e116cca6329d Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Sat, 15 Jun 2024 06:29:35 +0200 Subject: [PATCH 143/143] Call real callSuccessUIHandlerForMuc function, fixes #1080 --- Monal/Classes/MLMucProcessor.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Monal/Classes/MLMucProcessor.m b/Monal/Classes/MLMucProcessor.m index 8d88804ff5..5d41ca388c 100644 --- a/Monal/Classes/MLMucProcessor.m +++ b/Monal/Classes/MLMucProcessor.m @@ -1720,7 +1720,7 @@ -(void) callSuccessUIHandlerForMuc:(NSString*) room withCallback:(monal_void_blo -(void) callSuccessUIHandlerForMuc:(NSString*) room { - + return [self callSuccessUIHandlerForMuc:room withCallback:nil]; } -(void) updateBookmarks