diff --git a/.github/workflows/beta.build-push.yml b/.github/workflows/beta.build-push.yml index 5a9a93f77e..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,28 +70,45 @@ 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@v3 + - uses: actions/upload-artifact@v4 with: name: monal-catalyst path: Monal/build/app/Monal.zip if-no-files-found: error - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: name: monal-ios path: Monal/build/ipa/Monal.ipa if-no-files-found: error - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: name: monal-catalyst-dsym path: Monal/build/macos_Monal.xcarchive/dSYMs if-no-files-found: error - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: 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 diff --git a/.github/workflows/develop-push.yml b/.github/workflows/develop-push.yml index 19f1cf75a6..63c62120ca 100644 --- a/.github/workflows/develop-push.yml +++ b/.github/workflows/develop-push.yml @@ -64,23 +64,28 @@ jobs: run: xcrun notarytool submit ./Monal/build/app/Monal.alpha.zip --wait --team-id S8D843U34Y --key "/Users/ci/appstoreconnect/apiKey.p8" --key-id "$(cat /Users/ci/appstoreconnect/apiKeyId.txt)" --issuer "$(cat /Users/ci/appstoreconnect/apiIssuerId.txt)" - name: Update monal homebrew alpha repo run: scripts/updateAlphaHomebrew.sh - #- uses: actions/upload-artifact@v3 - # with: - # name: monal-catalyst - # path: "Monal/build/app/Monal.alpha.tar" - # if-no-files-found: error - #- uses: actions/upload-artifact@v3 - # with: - # name: monal-ios - # path: "Monal/build/ipa/Monal.alpha.ipa" - # if-no-files-found: error - #- uses: actions/upload-artifact@v3 - # with: + - uses: actions/upload-artifact@v4 + with: + name: monal-catalyst + path: "Monal/build/app/Monal.alpha.tar" + if-no-files-found: error + - uses: actions/upload-artifact@v4 + with: + name: monal-ios + path: "Monal/build/ipa/Monal.alpha.ipa" + if-no-files-found: error + # - uses: actions/upload-artifact@v4 + # with: # name: monal-catalyst-dsym # path: Monal/build/macos_Monal.xcarchive/dSYMs # if-no-files-found: error - #- uses: actions/upload-artifact@v3 - # with: + # - uses: actions/upload-artifact@v4 + # with: # name: monal-ios-dsym # path: Monal/build/ios_Monal.xcarchive/dSYMs # if-no-files-found: error + # - name: Update translations + # run: | + # chmod +x ./scripts/updateLocalization.sh + # chmod +x ./scripts/xliff_extractor.py + # ./scripts/updateLocalization.sh NOCOMMIT diff --git a/.github/workflows/stable.build-push.yml b/.github/workflows/stable.build-push.yml index fc380aa429..251e3234cb 100644 --- a/.github/workflows/stable.build-push.yml +++ b/.github/workflows/stable.build-push.yml @@ -76,41 +76,41 @@ jobs: - name: Extract version number and changelog from newest merge commit id: releasenotes run: | - buildNumber=$(git tag --sort="v:refname" |grep "Build_iOS" | tail -n1 | sed 's/Build_iOS_//g') + buildNumber=$(git tag --sort="v:refname" | grep "Build_iOS" | tail -n1 | sed 's/Build_iOS_//g') echo "tag=Build_iOS_$buildNumber" >> "$GITHUB_OUTPUT" - echo "name=$(git log -n 1 --merges --pretty=format:%s)" >> "$GITHUB_OUTPUT" + echo "name=$(git log -n 1 --merges --pretty=format:%s | sed -E 's/^\s*([^\s]+)\s+\(([^\s]+)\)$/\1 (Build '$buildNumber', PR \2)/g')" >> "$GITHUB_OUTPUT" echo "notes=$(git log -n 1 --merges --pretty=format:%b)" >> "$GITHUB_OUTPUT" - name: Release uses: softprops/action-gh-release@v2 with: - name: Release ${{ steps.releasenotes.outputs.name }} - tag_name: ${{ steps.releasenotes.outputs.tag }} + name: "Release ${{ steps.releasenotes.outputs.name }}" + tag_name: "${{ steps.releasenotes.outputs.tag }}" target_commitish: stable generate_release_notes: false - body: ${{ steps.releasenotes.outputs.notes }} + body: "${{ steps.releasenotes.outputs.notes }}" files: | ./Monal/build/ipa/Monal.ipa ./Monal/build/app/Monal.zip fail_on_unmatched_files: true token: ${{ secrets.GITHUB_TOKEN }} draft: false - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: name: monal-catalyst-pkg path: Monal/build/app/Monal.pkg if-no-files-found: error - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: name: monal-ios path: Monal/build/ipa/Monal.ipa if-no-files-found: error - - uses: actions/upload-artifact@v3 - with: - name: monal-catalyst-dsym - path: Monal/build/macos_Monal.xcarchive/dSYMs - if-no-files-found: error - - uses: actions/upload-artifact@v3 - with: - name: monal-ios-dsym - path: Monal/build/ios_Monal.xcarchive/dSYMs - if-no-files-found: error + # - uses: actions/upload-artifact@v4 + # with: + # name: monal-catalyst-dsym + # path: Monal/build/macos_Monal.xcarchive/dSYMs + # if-no-files-found: error + # - uses: actions/upload-artifact@v4 + # with: + # name: monal-ios-dsym + # path: Monal/build/ios_Monal.xcarchive/dSYMs + # if-no-files-found: error diff --git a/.github/workflows/update-translations.yml b/.github/workflows/update-translations.yml new file mode 100644 index 0000000000..a628f9ec12 --- /dev/null +++ b/.github/workflows/update-translations.yml @@ -0,0 +1,33 @@ +# build a new beta release and push it to apple +name: update-translations + +# Controls when the action will run. +on: + workflow_dispatch: + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + # This workflow contains a single job called "build" + updateTranslations: + # The type of runner that the job will run on + runs-on: self-hosted + env: + APP_NAME: "Monal" + APP_DIR: "Monal.app" + BUILD_TYPE: "Beta" + EXPORT_OPTIONS_CATALYST_APPSTORE: "../scripts/exportOptions/Stable_Catalyst_ExportOptions.plist" + EXPORT_OPTIONS_CATALYST_APP_EXPORT: "../scripts/exportOptions/Beta_Catalyst_ExportOptions.plist" + EXPORT_OPTIONS_IOS: "../scripts/exportOptions/Beta_iOS_ExportOptions.plist" + # 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 diff --git a/Monal/Alpha.Monal.macos.entitlements b/Monal/Alpha.Monal.macos.entitlements index ee88dc993e..9134ea69aa 100644 --- a/Monal/Alpha.Monal.macos.entitlements +++ b/Monal/Alpha.Monal.macos.entitlements @@ -4,6 +4,8 @@ aps-environment production + com.apple.developer.usernotifications.filtering + com.apple.developer.usernotifications.communication com.apple.security.app-sandbox @@ -20,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/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/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..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) { @@ -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/AddContactMenu.swift b/Monal/Classes/AddContactMenu.swift index fc63decb89..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 @@ -136,7 +136,7 @@ struct AddContactMenu: View { } showLoadingOverlay(overlay, headline: NSLocalizedString("Adding...", comment: "")) account.checkJidType(jid, withCompletion: { type, errorMsg in - if(type == "account") { + if type == "account" { hideLoadingOverlay(overlay) let contact = MLContact.createContact(fromJid: jid, andAccountNo: account.accountNo) self.newContact = contact @@ -144,19 +144,17 @@ struct AddContactMenu: View { //import omemo fingerprints as manually trusted, if requested 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 success : Bool = (data as! NSDictionary)["success"] as! Bool; + } else if type == "muc" { + 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!")) - } - }, forMuc: jid) - account.joinMuc(jid) + } } else { hideLoadingOverlay(overlay) errorAlert(title: Text("Error"), message: Text(errorMsg ?? "Undefined error")) @@ -168,14 +166,14 @@ struct AddContactMenu: View { let account = self.connectedAccounts[selectedAccount] let splitJid = HelperTools.splitJid(account.connectionProperties.identity.jid) Form { - if(connectedAccounts.isEmpty) { + if connectedAccounts.isEmpty { Text("Please make sure at least one account has connected before trying to add a contact or channel.") .foregroundColor(.secondary) } else { Section(header:Text("Contact and Group/Channel Jids are usually in the format: name@domain.tld")) { - if(connectedAccounts.count > 1) { + if connectedAccounts.count > 1 { Picker("Use account", selection: $selectedAccount) { ForEach(Array(self.connectedAccounts.enumerated()), id: \.element) { idx, account in Text(account.connectionProperties.identity.jid).tag(idx) @@ -196,15 +194,15 @@ struct AddContactMenu: View { toAdd = toAdd.replacingOccurrences(of: " ", with: "") } } - if(scannedFingerprints != nil && scannedFingerprints!.count > 0) { + if scannedFingerprints != nil && scannedFingerprints!.count > 0 { Section(header: Text("A contact was scanned through the QR code scanner")) { - Toggle(isOn: $importScannedFingerprints, label: { + Toggle(isOn: $importScannedFingerprints) { Text("Import and trust OMEMO fingerprints from QR code") - }) + } } } Section { - if(scannedFingerprints != nil) { + if scannedFingerprints != nil { Button(action: { toAdd = "" importScannedFingerprints = true @@ -217,10 +215,10 @@ struct AddContactMenu: View { Button(action: { showAlert = toAddEmptyAlert || toAddInvalidAlert - if(!showAlert) { + 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...")) + if jidComponents["host"] == nil || jidComponents["host"]!.isEmpty { + errorAlert(title: Text("Error"), message: Text("Something went wrong while parsing your input...")) showAlert = true return } @@ -287,7 +285,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/BackgroundSettings.swift b/Monal/Classes/BackgroundSettings.swift index bbb0985c9f..839e78ef1f 100644 --- a/Monal/Classes/BackgroundSettings.swift +++ b/Monal/Classes/BackgroundSettings.swift @@ -2,14 +2,10 @@ // 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. // -import SwiftUI -import UniformTypeIdentifiers -import monalxmpp - @ViewBuilder func title(contact: ObservableKVOWrapper?) -> some View { if let contact = contact { @@ -27,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)) } @@ -39,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( @@ -62,32 +57,44 @@ 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(Text("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(Text("Change Background Image")) .sheet(isPresented:$showingImagePicker) { ImagePicker(image:$inputImage) } - //>= ios16 + //>= ios 16 /* PhotosPicker(selection:$selectedItem, matching:.images, photoLibrary:.shared()) { if let inputImage = inputImage { @@ -133,8 +140,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/ChannelMemberList.swift b/Monal/Classes/ChannelMemberList.swift index a3cbd0444d..f303bb6c7b 100644 --- a/Monal/Classes/ChannelMemberList.swift +++ b/Monal/Classes/ChannelMemberList.swift @@ -6,51 +6,67 @@ // Copyright © 2024 monal-im.org. All rights reserved. // -import SwiftUI -import monalxmpp import OrderedCollections struct ChannelMemberList: View { - @State private var channelMembers: 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" } } - _channelMembers = State(wrappedValue: nickSet) + participants.sort { + (mucAffiliationToInt($0.value), $0.key) < (mucAffiliationToInt($1.value), $1.key) + } } + var body: some View { List { - Section(header: Text(self.channel.obj.contactDisplayName)) { - ForEach(self.channelMembers.sorted(by: <), id: \.self.key) { - member in + Section(header: Text("\(self.channel.contactDisplayName as String) (affiliation: \(mucAffiliationToString(ownAffiliation)))")) { + ForEach(participants.keys, id: \.self) { participant_key in ZStack(alignment: .topLeading) { HStack(alignment: .center) { - Text(member.key) + Text(participant_key) Spacer() - if member.value == "owner" { - Text(NSLocalizedString("Owner", comment: "")) - } else if member.value == "admin" { - Text(NSLocalizedString("Admin", comment: "")) - } else { - Text(NSLocalizedString("Member", comment: "")) - } + Text(mucAffiliationToString(participants[participant_key])) } } } } } - .navigationBarTitle("Channel Members", displayMode: .inline) + .navigationBarTitle(Text("Channel Participants"), 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() + } + } + } } } 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/ContactDetails.swift b/Monal/Classes/ContactDetails.swift index 1f3af26879..6b16fc8798 100644 --- a/Monal/Classes/ContactDetails.swift +++ b/Monal/Classes/ContactDetails.swift @@ -6,15 +6,13 @@ // 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 + @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 @@ -24,31 +22,249 @@ 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 _contact = StateObject(wrappedValue: contact) self.account = MLXMPPManager.sharedInstance().getConnectedAccount(forID: contact.accountId)! + } + private func updateRoleAndAffiliation() { 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 = "participant" + 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 + success = true // < dismiss entire view on close + } + + private func showImagePicker() { +#if targetEnvironment(macCatalyst) + let picker = DocumentPickerViewController( + supportedTypes: [UTType.image], + onPick: { url in + if let imageData = try? Data(contentsOf: url) { + if let loadedImage = UIImage(data: imageData) { + self.inputImage = loadedImage + } + } + }, + onDismiss: { + //do nothing on dismiss + } + ) + UIApplication.shared.windows.first?.rootViewController?.present(picker, animated: true) +#else + showingImagePicker = true +#endif + } + var body: some View { Form { Section { - 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") ? Text("Change Group Avatar") : Text("Change Channel Avatar")) + .onTapGesture { + 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") ? Text("Remove Group Avatar") : Text("Remove Channel Avatar")) + .applyClosure { view in + if #available(iOS 15, *) { + view + .symbolRenderingMode(.palette) + .foregroundStyle(.white, .red) + } else { + view.foregroundColor(.red) + } + } + }) + .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") ? Text("Change Group Avatar") : Text("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") ? Text("Group Avatar") : Text("Channel Avatar")) + } + } else { + view.accessibilityLabel(Text("Avatar")) + } + } + .frame(width: 150, height: 150, alignment: .center) + .shadow(radius: 7) + .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: { + 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) + } + } + ) + ] + ) + } + + 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, let statusMessage = contact.statusMessage as String?, statusMessage.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 +275,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 +299,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,39 +348,36 @@ 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" ? 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) } - Toggle("Pin Chat", isOn: Binding(get: { + Toggle(isOn: Binding(get: { contact.isPinned }, set: { contact.obj.togglePinnedChat($0) - })) -// Button(contact.isPinned ? "Unpin Chat" : "Pin Chat") { -// contact.obj.togglePinnedChat(!contact.isPinned); -// } - - 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") - } + })) { + Text("Pin Chat") } + #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") } @@ -172,7 +385,7 @@ struct ContactDetails: View { } #endif - if(!contact.isGroup && !contact.isSelfChat) { + if !contact.isGroup && !contact.isSelfChat { NavigationLink(destination: LazyClosureView(ContactResources(contact: contact))) { Text("Resources") } @@ -187,22 +400,38 @@ struct ContactDetails: View { } } - NavigationLink(destination: LazyClosureView(BackgroundSettings(contact:contact, delegate:delegate))) { + NavigationLink(destination: LazyClosureView(BackgroundSettings(contact:contact))) { 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" { + if ["owner", "admin"].contains(ownAffiliation) { + NavigationLink(destination: LazyClosureView(MemberList(mucContact:contact))) { + Text("Channel Participants") + } + } else { + NavigationLink(destination: LazyClosureView(ChannelMemberList(mucContact:contact))) { + Text("Channel Participants") + } + } + } } .listStyle(.plain) 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 { @@ -229,12 +458,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 { @@ -256,6 +485,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() } ) @@ -266,8 +497,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") @@ -295,11 +526,51 @@ 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: { + 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 + } + 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) + } + } + ) + ] + ) + } + } + } + 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") @@ -330,8 +601,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 }) { @@ -358,19 +628,42 @@ 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 { + 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) } } - } else { - view + })) + } + .onChange(of:inputImage) { _ in + 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 + 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() + } } } } 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/ContactEntry.swift b/Monal/Classes/ContactEntry.swift index 9ff3eac03f..ce3ccea0c9 100644 --- a/Monal/Classes/ContactEntry.swift +++ b/Monal/Classes/ContactEntry.swift @@ -6,11 +6,25 @@ // Copyright © 2023 monal-im.org. All rights reserved. // -import SwiftUI - -struct ContactEntry: View { - let contact : ObservableKVOWrapper - +struct ContactEntry: View { + let contact: ObservableKVOWrapper + let selfnotesPrefix: Bool + @ViewBuilder let additionalContent: () -> AdditionalContent + + 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 { ZStack(alignment: .topLeading) { HStack(alignment: .center) { @@ -18,8 +32,15 @@ struct ContactEntry: View { .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) + if selfnotesPrefix { + Text(contact.contactDisplayName as String) + } else { + Text(contact.contactDisplayNameWithoutSelfnotesPrefix as String) + } + additionalContent() + Text(contact.contactJid as String) + .foregroundColor(Color(UIColor.secondaryLabel)) + .font(.footnote) } } } diff --git a/Monal/Classes/ContactPicker.swift b/Monal/Classes/ContactPicker.swift index a58279fde0..d180d0dd4b 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,51 +25,71 @@ 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) } } } } struct ContactPicker: View { - @Environment(\.presentationMode) private var presentationMode - - @State var contacts: OrderedSet> + typealias completionType = (OrderedSet>)->Void let account: xmpp - @Binding var selectedContacts: OrderedSet> - let existingMembers: OrderedSet> + @Environment(\.presentationMode) private var presentationMode + @Binding var returnedContacts: OrderedSet> + @State var selectedContacts: OrderedSet> @State var searchText = "" + @State var isEditingSearchInput = false + let allowRemoval: Bool + let completion: completionType? - @State var isEditingSearchInput: Bool = false - - init(account: xmpp, selectedContacts: Binding>>) { - self.init(account: account, selectedContacts: selectedContacts, existingMembers: OrderedSet()) + 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, selectedContacts: Binding>>, existingMembers: OrderedSet>) { + + init(_ account: xmpp, binding returnedContacts: Binding>>, allowRemoval: Bool = true) { self.account = account - self._selectedContacts = selectedContacts - self.existingMembers = existingMembers - + self.allowRemoval = allowRemoval + self.completion = nil + _selectedContacts = State(wrappedValue:OrderedSet()) + _returnedContacts = returnedContacts + buildPreselectedContacts(returnedContacts.wrappedValue) + } + + private mutating func buildPreselectedContacts(_ source: OrderedSet>) { + //build currently selected list of contacts + var contactsTmp: OrderedSet> = OrderedSet() + for contact in source { + contactsTmp.append(contact) + } + _selectedContacts = State(wrappedValue:contactsTmp) + } + + private var allContacts: OrderedSet> { + //build list of all possible contacts on this account (excluding selfchat and other mucs) var contactsTmp: OrderedSet> = OrderedSet() for contact in DataLayer.sharedInstance().possibleGroupMembers(forAccount: account.accountNo) { contactsTmp.append(ObservableKVOWrapper(contact)) } - _contacts = State(wrappedValue: contactsTmp) + return contactsTmp } 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 +100,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 +127,14 @@ struct ContactPicker: View { } } .listStyle(.inset) - .navigationBarTitle("Contact Selection", displayMode: .inline) - .navigationBarBackButtonHidden(true) - .toolbar { - ToolbarItem(placement: .navigationBarLeading) { - Button("Back", action: { - self.presentationMode.wrappedValue.dismiss() - }) + .navigationBarTitle(Text("Contact Selection"), displayMode: .inline) + .onDisappear { + returnedContacts.removeAll() + for contact in selectedContacts { + returnedContacts.append(contact) + } + if let completion = completion { + completion(returnedContacts) } } } diff --git a/Monal/Classes/ContactRequestsMenu.swift b/Monal/Classes/ContactRequestsMenu.swift index ebce742ba8..040d3283af 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: () -> () @@ -88,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 d88db79a82..12e9938a72 100644 --- a/Monal/Classes/CreateGroupMenu.swift +++ b/Monal/Classes/CreateGroupMenu.swift @@ -6,29 +6,20 @@ // Copyright © 2022 monal-im.org. All rights reserved. // -import MobileCoreServices -import UniformTypeIdentifiers -import SwiftUI -import monalxmpp 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 @@ -47,14 +38,14 @@ struct CreateGroupMenu: View { var body: some View { Form { - if(connectedAccounts.isEmpty) { + if connectedAccounts.isEmpty { Text("Please make sure at least one account has connected before trying to create new group.") .foregroundColor(.secondary) } else { Section() { - Picker("Use account", selection: $selectedAccount) { + Picker(selection: $selectedAccount, label: Text("Use account")) { ForEach(Array(self.connectedAccounts.enumerated()), id: \.element) { idx, account in Text(account.connectionProperties.identity.jid).tag(account as xmpp?) } @@ -66,53 +57,57 @@ struct CreateGroupMenu: View { .autocapitalization(.none) .addClearButton(isEditing: isEditingGroupName, text:$groupName) - NavigationLink(destination: LazyClosureView(ContactPicker(account: self.selectedAccount!, selectedContacts: $selectedContacts)), label: { - 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.")) + return + } showLoadingOverlay(overlay, headline: NSLocalizedString("Creating Group", comment: "")) - let roomJid = self.selectedAccount!.mucProcessor.createGroup(nil) - if(roomJid == nil) { - let groupContact = MLContact.createContact(fromJid: roomJid!, andAccountNo: self.selectedAccount!.accountNo) + guard let roomJid = self.selectedAccount!.mucProcessor.createGroup(generatedJid) else { + //room already existing in our local bookmarks --> just open it + //this should never happen since we randomly generated a jid above hideLoadingOverlay(overlay) + let groupContact = MLContact.createContact(fromJid: generatedJid, andAccountNo: self.selectedAccount!.accountNo) self.delegate.dismissWithoutAnimation() if let activeChats = self.appDelegate.activeChats { activeChats.presentChat(with:groupContact) } - } else { - self.selectedAccount!.mucProcessor.addUIHandler({data in - let success : Bool = (data as! NSDictionary)["success"] as! Bool; - if(success) { - self.selectedAccount!.mucProcessor.changeName(ofMuc: roomJid!, to: self.groupName) - for user in self.selectedContacts { - self.selectedAccount!.mucProcessor.setAffiliation("member", ofUser: user.contactJid, inMuc: roomJid!) - self.selectedAccount!.mucProcessor.inviteUser(user.contactJid, inMuc: roomJid!) - } - let groupContact = MLContact.createContact(fromJid: roomJid!, andAccountNo: self.selectedAccount!.accountNo) - hideLoadingOverlay(overlay) - self.delegate.dismissWithoutAnimation() - if let activeChats = self.appDelegate.activeChats { - activeChats.presentChat(with:groupContact) - } - } else { - hideLoadingOverlay(overlay) - errorAlert(title: Text("Error creating group!")) - } - }, forMuc: roomJid!) + return } + 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 { + self.selectedAccount!.mucProcessor.setAffiliation("member", ofUser: user.contactJid, inMuc: roomJid) + self.selectedAccount!.mucProcessor.inviteUser(user.contactJid, inMuc: roomJid) + } + let groupContact = MLContact.createContact(fromJid: roomJid, andAccountNo: self.selectedAccount!.accountNo) + hideLoadingOverlay(overlay) + self.delegate.dismissWithoutAnimation() + if let activeChats = self.appDelegate.activeChats { + activeChats.presentChat(with:groupContact) + } + } else { + hideLoadingOverlay(overlay) + errorAlert(title: Text("Error creating group!"), message: Text(data["errorMessage"] as! String)) + } + }, forMuc: roomJid) }, label: { 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 +117,7 @@ struct CreateGroupMenu: View { })) } .addLoadingOverlay(overlay) - .navigationBarTitle("Create new group", displayMode: .inline) + .navigationBarTitle(Text("Create new group"), displayMode: .inline) .navigationViewStyle(.stack) } } diff --git a/Monal/Classes/DataLayer.h b/Monal/Classes/DataLayer.h index b030a79a5f..2fe0fbec21 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 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 4de33cc7a3..e22fb7deef 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]]) @@ -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; }]; @@ -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 andJid:buddyName 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 andJid:(NSString*) jid 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 buddy_name=? AND stanzaid!='' AND stanzaid=?;" andArguments:@[accountNo, jid, stanzaId]]; if([found count]) { DDLogVerbose(@"stanzaid provided and could be found: %@", found); - return YES; + return found[0]; } } @@ -1318,7 +1318,7 @@ -(BOOL) hasMessageForStanzaId:(NSString*) stanzaId orMessageID:(NSString*) messa // 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"); @@ -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/DebugView.swift b/Monal/Classes/DebugView.swift index ea12caff8a..7fe70c6121 100644 --- a/Monal/Classes/DebugView.swift +++ b/Monal/Classes/DebugView.swift @@ -71,6 +71,12 @@ struct LogFilesView: View { } } } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .applyClosure { view in + if #available(iOS 15, *) { + view.background(.interpolatedWindowBackground) + } + } .alert(isPresented: $showingDBExportFailedAlert) { Alert(title: Text("Database Export Failed"), message: Text("Failed to export the database, please check the logfile for errors and try again."), dismissButton: .default(Text("Close"))) } @@ -94,7 +100,9 @@ struct UDPConfigView: View { Text("The UDP logger allows you to livestream the log to the configured IP. Please use a secure key when streaming over the internet!\n[Learn how to receive the log stream](https://github.com/monal-im/Monal/wiki/Introduction-to-Monal-Logging#stream-the-log).") Form { Section(header: Text("UDP Logger Configuration")) { - Toggle("Enable", isOn: $defaultDB.udpLoggerEnabled) + Toggle(isOn: $defaultDB.udpLoggerEnabled) { + Text("Enable") + } LabeledContent("Logserver IP:") { TextField("Logserver IP", text: $defaultDB.udpLoggerHostname, prompt: Text("Required")) } @@ -115,44 +123,56 @@ struct UDPConfigView: View { Text("UDP Logging UI not supported on iOS < 16").foregroundColor(.red) } } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .applyClosure { view in + if #available(iOS 15, *) { + view.background(.interpolatedWindowBackground) + } + } } } struct CrashTestingView: View { var body: some View { - VStack(alignment:.leading, spacing: 25) { - Text("This allows you to forcefully crash the app using several different methods to test the crash handling.") - - Group { - Button("Try to call unknown handler method") { - DispatchQueue.global(qos: .default).async(execute: { + VStack(alignment:.leading, spacing: 25) { + Text("This allows you to forcefully crash the app using several different methods to test the crash handling.") + + Group { + Button("Try to call unknown handler method") { + DispatchQueue.global(qos: .default).async(execute: { + HelperTools.flushLogs(withTimeout: 0.100) + let handler = MLHandler(delegate: self, handlerName: "IDontKnowThis", andBoundArguments: [:]) + handler.call(withArguments: nil) + }) + } + Button("Bad Access Crash") { HelperTools.flushLogs(withTimeout: 0.100) - let handler = MLHandler(delegate: self, handlerName: "IDontKnowThis", andBoundArguments: [:]) - handler.call(withArguments: nil) - }) - } - Button("Bad Access Crash") { - HelperTools.flushLogs(withTimeout: 0.100) - let delegate: AnyClass? = NSClassFromString("MonalAppDelegate") - print(delegate.unsafelyUnwrapped.audiovisualTypes()) - - } - Button("Assertion Crash") { - HelperTools.flushLogs(withTimeout: 0.100) - assert(false) - } - Button("Fatal Error Crash") { - HelperTools.flushLogs(withTimeout: 0.100) - fatalError("fatalError_example") - } - Button("Nil Crash") { - HelperTools.flushLogs(withTimeout: 0.100) - let crasher:Int? = nil - print(crasher!) + let delegate: AnyClass? = NSClassFromString("MonalAppDelegate") + print(delegate.unsafelyUnwrapped.audiovisualTypes()) + + } + Button("Assertion Crash") { + HelperTools.flushLogs(withTimeout: 0.100) + assert(false) + } + Button("Fatal Error Crash") { + HelperTools.flushLogs(withTimeout: 0.100) + fatalError("fatalError_example") + } + Button("Nil Crash") { + HelperTools.flushLogs(withTimeout: 0.100) + let crasher:Int? = nil + print(crasher!) + } + }.foregroundColor(.red) + Spacer() + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .applyClosure { view in + if #available(iOS 15, *) { + view.background(.interpolatedWindowBackground) } - }.foregroundColor(.red) - Spacer() - } + } } } @@ -177,6 +197,7 @@ struct DebugView: View { Text("Crash Testing") } } + .frame(maxWidth: .infinity, maxHeight: .infinity) .padding() .addLoadingOverlay(overlay) .onChange(of: isReconnecting) { _ in 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/EditGroupSubject.swift b/Monal/Classes/EditGroupSubject.swift index 7dd8e816ec..f90e1eee7c 100644 --- a/Monal/Classes/EditGroupSubject.swift +++ b/Monal/Classes/EditGroupSubject.swift @@ -6,15 +6,13 @@ // Copyright © 2024 monal-im.org. All rights reserved. // -import SwiftUI - 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 +26,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 +36,6 @@ struct EditGroupSubject: View { view } } - .addClearButton(isEditing: isEditingSubject, text:$subject) } } } diff --git a/Monal/Classes/GeneralSettings.swift b/Monal/Classes/GeneralSettings.swift new file mode 100644 index 0000000000..8466de48ff --- /dev/null +++ b/Monal/Classes/GeneralSettings.swift @@ -0,0 +1,444 @@ +// +// GeneralSettings.swift +// Monal +// +// Created by Vaidik Dubey on 22/03/24. +// 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{ + 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("useInlineSafari") + var useInlineSafari: 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") + } + } + + 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")) + .onAppear { + generalSettingsDefaultsDB.hasSeenPrivacySettings = true + } + } +} + +struct UserInterfaceSettings: View { + @ObservedObject var generalSettingsDefaultsDB = GeneralSettingsDefaultsDB() + + var body: some View { + Form { + Section(header: Text("Previews")) { + SettingsToggle(isOn: $generalSettingsDefaultsDB.showGeoLocation) { + Text("Show inline geo location") + Text("Received geo locations are shared with Apple's Maps App.") + } + SettingsToggle(isOn: $generalSettingsDefaultsDB.showURLPreview) { + Text("Show URL previews") + Text("The operator of the webserver providing that URL may see your IP address.") + } + 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")) { + 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")) { + 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) + } + } + } + .navigationBarTitle(Text("User Interface"), displayMode: .inline) + } +} + +struct SecuritySettings: View { + @ObservedObject var generalSettingsDefaultsDB = GeneralSettingsDefaultsDB() + + var body: some View { + Form { + Section(header: Text("Encryption")) { + 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, *) { + 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 \ +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. +""" + ) + } + } + + 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")) { + SettingsToggle(isOn: $generalSettingsDefaultsDB.autodeleteAllMessagesAfter3Days) { + Text("Autodelete all messages after 3 days") + } + } + } + .navigationBarTitle(Text("Security"), displayMode: .inline) + } +} + +struct PrivacySettings: View { + @ObservedObject var generalSettingsDefaultsDB = GeneralSettingsDefaultsDB() + + var body: some View { + Form { + Section(header: Text("Activity indications")) { + SettingsToggle(isOn: $generalSettingsDefaultsDB.sendReceivedMarkers) { + Text("Send message received") + Text("Let your contacts know if you received a message.") + } + SettingsToggle(isOn: $generalSettingsDefaultsDB.sendDisplayedMarkers) { + Text("Send message displayed state") + Text("Let your contacts know if you read a message.") + } + SettingsToggle(isOn: $generalSettingsDefaultsDB.sendLastChatState) { + Text("Send typing notifications") + Text("Let your contacts know if you are typing a message.") + } + SettingsToggle(isOn: $generalSettingsDefaultsDB.sendLastUserInteraction) { + Text("Send last interaction time") + Text("Let your contacts know when you last opened the app.") + } + } + + Section(header: Text("Interactions")) { + SettingsToggle(isOn: $generalSettingsDefaultsDB.allowNonRosterContacts) { + Text("Accept incoming messages from strangers") + Text("Allow contacts not in your contact list to contact you.") + } + 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")) { + SettingsToggle(isOn: $generalSettingsDefaultsDB.allowVersionIQ) { + Text("Publish version") + Text("Allow contacts in your contact list to query your Monal and iOS versions.") + } + 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.") + } + } + } + .navigationBarTitle(Text("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("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(Text("Notifications"), displayMode: .inline) + } +} + +struct AttachmentSettings: View { + @ObservedObject var generalSettingsDefaultsDB = GeneralSettingsDefaultsDB() + + var body: some View { + Form { + Section(header: Text("General File Transfer Settings")) { + SettingsToggle(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/GroupDetailsEdit.swift b/Monal/Classes/GroupDetailsEdit.swift deleted file mode 100644 index 36fb1b39b9..0000000000 --- a/Monal/Classes/GroupDetailsEdit.swift +++ /dev/null @@ -1,104 +0,0 @@ -// -// GroupDetailsEdit.swift -// Monal -// -// Created by Friedrich Altheide on 23.02.24. -// Copyright © 2024 monal-im.org. All rights reserved. -// - -import SwiftUI -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 - @StateObject private var overlay = LoadingOverlayState() - private let account: xmpp - private let ownAffiliation: String? - - 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.obj.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.obj.mucType == "group" { - Text("Group description") - } else { - Text("Channel description") - } - Spacer() - } - } - .sheet(isPresented: $showingSheetEditSubject) { - LazyClosureView(EditGroupSubject(contact: contact)) - } - } - } - .addLoadingOverlay(overlay) - .navigationTitle((contact.obj.mucType == "group") ? "Edit group" : "Edit channel") - .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/HelperTools.h b/Monal/Classes/HelperTools.h index fb02f2877d..fa953819fa 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; @@ -86,6 +92,8 @@ 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)) ++(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; @@ -101,8 +109,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; @@ -177,6 +188,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 0054d9e159..d271e2ac8a 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 @@ -519,6 +528,18 @@ +(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]; +} + +//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 @@ -526,7 +547,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]; @@ -578,6 +599,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]; @@ -648,9 +671,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 @@ -1138,24 +1163,88 @@ +(void) handleUploadItemProvider:(NSItemProvider*) provider withCompletionHandle } #pragma clang diagnostic pop -+(UIImage*) imageWithNotificationBadgeForImage:(UIImage*) image { - UIImage* finalImage; - UIImage* badge = [[UIImage systemImageNamed:@"circle.fill"] imageWithTintColor:UIColor.redColor]; +//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); + + 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 +{ + if(image == nil) + return nil; + + 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 +{ + if(image == nil) + return nil; + + 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)]; + }]; +} - UIGraphicsBeginImageContext(CGSizeMake(image.size.width, image.size.height)); ++(UIImage*) imageWithNotificationBadgeForImage:(UIImage*) image +{ + UIImage* badge = [[UIImage systemImageNamed:@"circle.fill"] imageWithTintColor:UIColor.redColor]; 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 { ++(UIImageView*) buttonWithNotificationBadgeForImage:(UIImage*) image hasNotification:(bool) hasNotification withTapHandler: (UITapGestureRecognizer*) handler +{ UIImageView* result; if(hasNotification) result = [[UIImageView alloc] initWithImage:[self imageWithNotificationBadgeForImage:image]]; @@ -1980,16 +2069,20 @@ +(NSSet*) getOwnFeatureSet @"jabber:x:conference", @"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", @"urn:xmpp:message-retract:1", @"urn:xmpp:message-correct:0", ] 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 @@ -2708,4 +2801,13 @@ +(BOOL) isIP:(NSString*) host return NO; } ++(NSURLSession*) createEphemeralURLSession +{ + NSURLSessionConfiguration* sessionConfig = [NSURLSessionConfiguration ephemeralSessionConfiguration]; + if(@available(iOS 16.1, macCatalyst 16.1, *)) + if([[HelperTools defaultsDB] boolForKey: @"useDnssecForAllConnections"]) + sessionConfig.requiresDNSSECValidation = YES; + return [NSURLSession sessionWithConfiguration:sessionConfig]; +} + @end 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/LoadingOverlay.swift b/Monal/Classes/LoadingOverlay.swift index bb8f0d4f62..89fbbbe801 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 @@ -38,7 +35,8 @@ struct LoadingOverlay: ViewModifier { state.description.font(.footnote) ProgressView() } - .frame(width: 250, height: 100) + .padding(20) + .frame(minWidth: 250, minHeight: 100) .background(Color.secondary.colorInvert()) .cornerRadius(20) .transaction { transaction in transaction.animation = nil} @@ -61,6 +59,10 @@ func showLoadingOverlay(_ overlay: LoadingOverlayState, headli overlay.enabled = true //only rerender ui once (not sure if this optimization is really needed, if this is missing, use @Published for member vars of state class) overlay.objectWillChange.send() + //make sure to really draw the overlay on race conditions + DispatchQueue.main.asyncAfter(deadline: .now() + 0.250) { + overlay.objectWillChange.send() + } } } @@ -71,6 +73,10 @@ func showLoadingOverlay(_ overlay: LoadingOverlayState, headli overlay.enabled = true //only rerender ui once (not sure if this optimization is really needed, if this is missing, use @Published for member vars of state class) overlay.objectWillChange.send() + //make sure to really draw the overlay on race conditions + DispatchQueue.main.asyncAfter(deadline: .now() + 0.250) { + overlay.objectWillChange.send() + } } } @@ -81,6 +87,10 @@ func hideLoadingOverlay(_ overlay: LoadingOverlayState) { overlay.enabled = false //only rerender ui once (not sure if this optimization is really needed, if this is missing, use @Published for member vars of state class) overlay.objectWillChange.send() + //make sure to really draw the overlay on race conditions + DispatchQueue.main.asyncAfter(deadline: .now() + 0.250) { + overlay.objectWillChange.send() + } } } 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; 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/MLCall.m b/Monal/Classes/MLCall.m index 03705f57ea..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 { @@ -410,7 +417,18 @@ -(BOOL) isConnected -(void) setAudioSession:(AVAudioSession*) audioSession { @synchronized(self) { - if(audioSession != nil) + if(audioSession == _audioSession) + { + DDLogWarn(@"Trying to activate same audio session a second time, ignoring..."); + return; + } + 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), @@ -1105,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:@{ @@ -1264,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/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/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/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/MLConstants.h b/Monal/Classes/MLConstants.h index 9abd0df732..b6c26bd941 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, @@ -165,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/MLContact.h b/Monal/Classes/MLContact.h index 49aa660b8c..80a2b90b11 100644 --- a/Monal/Classes/MLContact.h +++ b/Monal/Classes/MLContact.h @@ -50,13 +50,15 @@ 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 */ @property (nonatomic, readonly) NSString* nickName; @property (nonatomic, strong) NSString* nickNameView; +@property (nonatomic, strong) NSString* fullNameView; /** xmpp state text @@ -90,6 +92,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 4999d65ae9..c9be560dd8 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; @@ -27,10 +28,13 @@ NSString* const kSubRemove = @"remove"; NSString* const kAskSubscribe = @"subscribe"; +static NSMutableDictionary* _singletonCache; + @interface MLContact () { NSInteger _unreadCount; monal_void_block_t _cancelNickChange; + monal_void_block_t _cancelFullNameChange; UIImage* _avatar; } @property (nonatomic, assign) BOOL isSelfChat; @@ -67,6 +71,11 @@ @interface MLContact () @implementation MLContact ++(void) initialize +{ + _singletonCache = [NSMutableDictionary new]; +} + +(MLContact*) makeDummyContact:(int) type { if(type == 1) @@ -185,10 +194,8 @@ +(NSString*) ownDisplayNameForAccount:(xmpp*) account return nilDefault(displayName, @""); } -+(MLContact*) createContactFromJid:(NSString*) jid andAccountNo:(NSNumber*) accountNo ++(MLContact*) createContactFromDatabaseWithJid:(NSString*) jid andAccountNo:(NSNumber*) accountNo { - 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 @@ -221,14 +228,41 @@ +(MLContact*) createContactFromJid:(NSString*) jid andAccountNo:(NSNumber*) acco return [self contactFromDictionary:contactDict]; } ++(MLContact*) createContactFromJid:(NSString*) jid andAccountNo:(NSNumber*) accountNo +{ + MLAssert(jid != nil, @"jid must not be nil"); + MLAssert(accountNo != nil && accountNo.intValue >= 0, @"accountNo must not be nil and > 0"); + + 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]; + } + + MLContact* retval = [self createContactFromDatabaseWithJid:jid andAccountNo:accountNo]; + + _singletonCache[cacheKey] = [[WeakContainer alloc] initWithObj:retval]; + return retval; + } +} + -(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]; + [[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; } @@ -269,7 +303,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) { @@ -294,7 +329,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 @@ -303,6 +338,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; @@ -336,11 +376,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); @@ -363,6 +408,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, @""); @@ -370,6 +425,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 +433,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 +444,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 @@ -406,6 +489,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]; @@ -577,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/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 diff --git a/Monal/Classes/MLFileTransferDataCell.m b/Monal/Classes/MLFileTransferDataCell.m index 7bfc50b9af..60866e23f7 100644 --- a/Monal/Classes/MLFileTransferDataCell.m +++ b/Monal/Classes/MLFileTransferDataCell.m @@ -34,7 +34,7 @@ -(void)awakeFromNib -(void)layoutSubviews { - if([MLFiletransfer isFileforHistoryIdInTransfer:self.messageDBId]) + if([MLFiletransfer isFileForHistoryIdInTransfer:self.messageDBId]) { [self.loadingView setHidden:NO]; [self.loadingView startAnimating]; diff --git a/Monal/Classes/MLFiletransfer.h b/Monal/Classes/MLFiletransfer.h index e49e990800..391a6b2cb0 100644 --- a/Monal/Classes/MLFiletransfer.h +++ b/Monal/Classes/MLFiletransfer.h @@ -27,7 +27,7 @@ NS_ASSUME_NONNULL_BEGIN +(void) uploadFile:(NSURL*) fileUrl onAccount:(xmpp*) account withEncryption:(BOOL) encrypted andCompletion:(void (^)(NSString* _Nullable url, NSString* _Nullable mimeType, NSNumber* _Nullable size, NSError* _Nullable error)) completion; +(void) uploadUIImage:(UIImage*) image onAccount:(xmpp*) account withEncryption:(BOOL) encrypted andCompletion:(void (^)(NSString* _Nullable url, NSString* _Nullable mimeType, NSNumber* _Nullable size, NSError* _Nullable error)) completion; +(void) hardlinkFileForMessage:(MLMessage*) msg; -+(BOOL) isFileforHistoryIdInTransfer:(NSNumber*) historyId; ++(BOOL) isFileForHistoryIdInTransfer:(NSNumber*) historyId; +(NSString*) getMimeTypeOfOriginalFile:(NSString*) file; @end diff --git a/Monal/Classes/MLFiletransfer.m b/Monal/Classes/MLFiletransfer.m index 6527f8ee0e..804d565f94 100644 --- a/Monal/Classes/MLFiletransfer.m +++ b/Monal/Classes/MLFiletransfer.m @@ -67,7 +67,7 @@ +(void) checkMimeTypeAndSizeForHistoryID:(NSNumber*) historyId } //make sure we don't check or download this twice @synchronized(_currentlyTransfering) { - if([_currentlyTransfering containsObject:historyId]) + if([self isFileForHistoryIdInTransfer:historyId]) { DDLogDebug(@"Already checking/downloading this content, ignoring"); return; @@ -77,11 +77,22 @@ +(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, *)) + if([[HelperTools defaultsDB] boolForKey: @"useDnssecForAllConnections"]) + request.requiresDNSSECValidation = YES; request.HTTPMethod = @"HEAD"; request.cachePolicy = NSURLRequestReturnCacheDataElseLoad; - NSURLSession* session = [NSURLSession sharedSession]; - [[session dataTaskWithRequest:request completionHandler:^(NSData* _Nullable data __unused, NSURLResponse* _Nullable response, NSError* _Nullable error __unused) { + NSURLSession* session = [HelperTools createEphemeralURLSession]; + [[session dataTaskWithRequest:request completionHandler:^(NSData* _Nullable data __unused, NSURLResponse* _Nullable response, NSError* _Nullable error) { + if(error != nil) + { + DDLogError(@"Failed to fetch headers of %@ at %@: %@", msg, url, error); + //check done, remove from "currently checking/downloading list" and set error + [self setErrorType:NSLocalizedString(@"Download error", @"") andErrorText:[NSString stringWithFormat:NSLocalizedString(@"Failed to fetch download metadata: %@", @""), error] forMessageId:msg.messageId]; + [self markAsComplete:historyId]; + return; + } NSDictionary* headers = ((NSHTTPURLResponse*)response).allHeaderFields; NSString* mimeType = [[headers objectForKey:@"Content-Type"] lowercaseString]; NSNumber* contentLength = [headers objectForKey:@"Content-Length"] ? [NSNumber numberWithInt:([[headers objectForKey:@"Content-Length"] intValue])] : @(-1); @@ -154,7 +165,7 @@ +(void) downloadFileForHistoryID:(NSNumber*) historyId andForceDownload:(BOOL) f //make sure we don't check or download this twice (but only do this if the download is not forced anyway) @synchronized(_currentlyTransfering) { - if(!forceDownload && [_currentlyTransfering containsObject:historyId]) + if(!forceDownload && [self isFileForHistoryIdInTransfer:historyId]) { DDLogDebug(@"Already checking/downloading this content, ignoring"); return; @@ -167,20 +178,20 @@ +(void) downloadFileForHistoryID:(NSNumber*) historyId andForceDownload:(BOOL) f NSURLComponents* urlComponents = [NSURLComponents componentsWithString:msg.messageText]; if(!urlComponents) { - DDLogError(@"url components decoding failed"); + DDLogError(@"url components decoding failed for %@", msg); [self setErrorType:NSLocalizedString(@"Download error", @"") andErrorText:NSLocalizedString(@"Failed to decode download link", @"") forMessageId:msg.messageId]; [self markAsComplete:historyId]; 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) { if(error) { - DDLogError(@"File download failed: %@", error); - [self setErrorType:NSLocalizedString(@"Download error", @"") andErrorText:NSLocalizedString(@"Failed to download file", @"") forMessageId:msg.messageId]; + DDLogError(@"File download for %@ failed: %@", msg, error); + [self setErrorType:NSLocalizedString(@"Download error", @"") andErrorText:[NSString stringWithFormat:NSLocalizedString(@"Failed to download file: %@", @""), error] forMessageId:msg.messageId]; [self markAsComplete:historyId]; return; } @@ -206,7 +217,7 @@ +(void) downloadFileForHistoryID:(NSNumber*) historyId andForceDownload:(BOOL) f DDLogInfo(@"Decrypting encrypted filetransfer stored at '%@'...", location); if(urlComponents.fragment.length < 88) { - DDLogError(@"File download failed: %@", error); + DDLogError(@"File download for %@ failed: %@", msg, error); [self setErrorType:NSLocalizedString(@"Download error", @"") andErrorText:NSLocalizedString(@"Failed to decode encrypted link", @"") forMessageId:msg.messageId]; [self markAsComplete:historyId]; return; @@ -223,7 +234,7 @@ +(void) downloadFileForHistoryID:(NSNumber*) historyId andForceDownload:(BOOL) f NSData* decryptedData = [AESGcm decrypt:encryptedData withKey:key andIv:iv withAuth:nil]; if(decryptedData == nil) { - DDLogError(@"File download decryption failed"); + DDLogError(@"File download decryption failed for %@", msg); [self setErrorType:NSLocalizedString(@"Download error", @"") andErrorText:NSLocalizedString(@"Failed to decrypt download", @"") forMessageId:msg.messageId]; [self markAsComplete:historyId]; return; @@ -231,7 +242,7 @@ +(void) downloadFileForHistoryID:(NSNumber*) historyId andForceDownload:(BOOL) f [decryptedData writeToFile:cacheFile options:NSDataWritingAtomic error:&error]; if(error) { - DDLogError(@"File download failed: %@", error); + DDLogError(@"File download for %@ failed: %@", msg, error); [self setErrorType:NSLocalizedString(@"Download error", @"") andErrorText:NSLocalizedString(@"Failed to write decrypted download into cache directory", @"") forMessageId:msg.messageId]; [self markAsComplete:historyId]; return; @@ -241,7 +252,7 @@ +(void) downloadFileForHistoryID:(NSNumber*) historyId andForceDownload:(BOOL) f } else { - DDLogError(@"Failed to decrypt file (iv, key, data length checks failed)"); + DDLogError(@"Failed to decrypt file (iv, key, data length checks failed) for %@", msg); [self setErrorType:NSLocalizedString(@"Download error", @"") andErrorText:NSLocalizedString(@"Failed to decrypt filetransfer", @"") forMessageId:msg.messageId]; [self markAsComplete:historyId]; return; @@ -255,8 +266,8 @@ +(void) downloadFileForHistoryID:(NSNumber*) historyId andForceDownload:(BOOL) f error = [HelperTools hardLinkOrCopyFile:[location path] to:cacheFile]; if(error) { - DDLogError(@"File download failed: %@", error); - [self setErrorType:NSLocalizedString(@"Download error", @"") andErrorText:NSLocalizedString(@"Failed to copy downloaded file into cache directory", @"") forMessageId:msg.messageId]; + DDLogError(@"File download for %@ failed: %@", msg, error); + [self setErrorType:NSLocalizedString(@"Download error", @"") andErrorText:[NSString stringWithFormat:NSLocalizedString(@"Failed to copy downloaded file into cache directory: %@", @""), error] forMessageId:msg.messageId]; [self markAsComplete:historyId]; return; } @@ -818,7 +829,7 @@ +(void) setErrorType:(NSString*) errorType andErrorText:(NSString*) errorText fo //make sure we don't upload the same tmpfile twice (should never happen anyways) @synchronized(_currentlyTransfering) { - if([_currentlyTransfering containsObject:file]) + if([self isFileAtPathInTransfer:file]) { error = [NSError errorWithDomain:@"MonalError" code:0 userInfo:@{NSLocalizedDescriptionKey: NSLocalizedString(@"Already uploading this content, ignoring", @"")}]; DDLogError(@"Already uploading this content, ignoring %@", file); @@ -875,7 +886,7 @@ +(void) setErrorType:(NSString*) errorType andErrorText:(NSString*) errorText fo @"data":fileData, @"fileName":userFacingFilename, @"contentType":sendMimeType - } andCompletion:^(NSString *url, NSError *error) { + } andCompletion:^(NSString* url, NSError* error) { if(error) { [_fileManager removeItemAtPath:file error:nil]; //remove temporary file @@ -914,7 +925,7 @@ +(void) setErrorType:(NSString*) errorType andErrorText:(NSString*) errorText fo [_fileManager moveItemAtPath:file toPath:cacheFile error:&error]; if(error) { - NSError* error = [NSError errorWithDomain:@"MonalError" code:0 userInfo:@{NSLocalizedDescriptionKey: NSLocalizedString(@"Failed to uploaded file to file cache directory", @"")}]; + NSError* error = [NSError errorWithDomain:@"MonalError" code:0 userInfo:@{NSLocalizedDescriptionKey: NSLocalizedString(@"Failed to move uploaded file to file cache directory", @"")}]; [_fileManager removeItemAtPath:file error:nil]; //remove temporary file [self markAsComplete:file]; DDLogError(@"File upload failed: %@", error); @@ -947,10 +958,17 @@ +(void) markAsComplete:(id) obj [[NSNotificationCenter defaultCenter] postNotificationName:kMonalFiletransfersIdle object:self]; } -+(BOOL) isFileforHistoryIdInTransfer:(NSNumber*) historyId ++(BOOL) isFileForHistoryIdInTransfer:(NSNumber*) historyId { if([_currentlyTransfering containsObject:historyId]) return YES; return NO; } + ++(BOOL) isFileAtPathInTransfer:(NSString*) path +{ + if([_currentlyTransfering containsObject:path]) + return YES; + return NO; +} @end diff --git a/Monal/Classes/MLHTTPRequest.m b/Monal/Classes/MLHTTPRequest.m index a12ca5805b..de50109ffb 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,9 @@ +(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, *)) + if([[HelperTools defaultsDB] boolForKey: @"useDnssecForAllConnections"]) + theRequest.requiresDNSSECValidation = YES; [theRequest setHTTPMethod:verb]; NSData* dataToSubmit = postedData; @@ -70,8 +73,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/MLIQProcessor.m b/Monal/Classes/MLIQProcessor.m index fcb64a0337..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)) @@ -561,8 +554,12 @@ +(BOOL) processRosterWithAccount:(xmpp*) account andIqNode:(XMPPIQ*) iqNode DDLogInfo(@"Upload max filesize: %lu", account.connectionProperties.uploadSize); } - if(!account.connectionProperties.conferenceServer && [features containsObject:@"http://jabber.org/protocol/muc"]) - account.connectionProperties.conferenceServer = iqNode.fromUser; + 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/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..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 @@ -262,6 +258,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]; 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/MLMessageProcessor.m b/Monal/Classes/MLMessageProcessor.m index 9f11a9e598..51141e8708 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!"); @@ -346,16 +345,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; @@ -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, }]; @@ -477,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"]) ) { @@ -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 andJid:buddyName 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/MLMucProcessor.h b/Monal/Classes/MLMucProcessor.h index 2e06ecf3d2..d8fd57c84b 100644 --- a/Monal/Classes/MLMucProcessor.h +++ b/Monal/Classes/MLMucProcessor.h @@ -26,7 +26,9 @@ NS_ASSUME_NONNULL_BEGIN -(void) leave:(NSString*) room withBookmarksUpdate:(BOOL) updateBookmarks keepBuddylistEntry:(BOOL) keepBuddylistEntry; //muc management methods --(NSString* _Nullable) createGroup:(NSString* _Nullable) node; +-(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 7a698f4b6c..5d41ca388c 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 @9 @interface MLMucProcessor() { @@ -33,11 +33,13 @@ @interface MLMucProcessor() NSMutableDictionary* _roomFeatures; NSMutableDictionary* _creating; NSMutableDictionary* _joining; + NSMutableSet* _destroying; NSMutableSet* _firstJoin; + NSMutableDictionary* _changingName; 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 @@ -75,7 +77,9 @@ -(id) initWithAccount:(xmpp*) account _roomFeatures = [NSMutableDictionary new]; _creating = [NSMutableDictionary new]; _joining = [NSMutableDictionary new]; + _destroying = [NSMutableSet new]; _firstJoin = [NSMutableSet new]; + _changingName = [NSMutableDictionary new]; _uiHandler = [NSMutableDictionary new]; _lastPing = [NSDate date]; _noUpdateBookmarks = [NSMutableSet new]; @@ -83,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; } @@ -107,7 +112,9 @@ -(void) setInternalState:(NSDictionary*) state _roomFeatures = [state[@"roomFeatures"] mutableCopy]; _creating = [state[@"creating"] mutableCopy]; _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]; @@ -122,7 +129,9 @@ -(NSDictionary*) getInternalState @"roomFeatures": [_roomFeatures copy], @"creating": [_creating copy], @"joining": [_joining copy], + @"destroying": [_destroying copy], @"firstJoin": [_firstJoin copy], + @"changingName": [_changingName copy], @"lastPing": _lastPing, @"noUpdateBookmarks": [_noUpdateBookmarks copy], @"hasFetchedBookmarks": @(_hasFetchedBookmarks), @@ -140,6 +149,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]; @@ -173,6 +184,23 @@ -(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) + [self callSuccessUIHandlerForMuc:callUiHandlerFor]; +} + -(BOOL) isCreating:(NSString*) room { @synchronized(_stateLockObject) { @@ -187,10 +215,37 @@ -(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 @synchronized(_stateLockObject) { + DDLogVerbose(@"Adding ui handler for muc: %@", room); _uiHandler[room] = handler; } } @@ -290,25 +345,49 @@ -(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) + { + 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 + 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 - [[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 @@ -415,6 +494,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); @@ -429,6 +513,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); @@ -448,6 +533,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]; @@ -461,6 +547,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]; @@ -475,12 +562,13 @@ -(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]; [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 @@ -504,6 +592,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); @@ -519,6 +608,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]; @@ -532,6 +622,8 @@ -(void) configureMuc:(NSString*) roomJid withMandatoryOptions:(NSDictionary*) ma @"roomJid": [NSString stringWithFormat:@"%@", roomJid], })); + [self callSuccessUIHandlerForMuc:iqNode.fromUser]; + if(joinOnSuccess) { //group is now properly configured and we are joined, but all the code handling a proper join was not run @@ -563,6 +655,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; } @@ -577,6 +674,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; } @@ -597,6 +699,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 @@ -621,6 +728,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] + }]; } } } @@ -637,6 +749,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; } @@ -650,6 +767,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; } @@ -711,8 +833,21 @@ -(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]; + + DDLogDebug(@"Updating muc contact..."); + [[MLNotificationQueue currentQueue] postNotificationName:kMonalContactRefresh object:_account userInfo:@{ + @"contact": [MLContact createContactFromJid:node.fromUser andAccountNo:_account.accountNo] + }]; + } } } else @@ -750,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 @@ -844,17 +965,33 @@ -(void) handleStatusCodes:(XMPPStanza*) node DDLogError(@"Got room create idle timeout but not creating group, ignoring: %@", room); return; } + DDLogWarn(@"Timeout while creating muc '%@'...", room); [self removeRoomFromCreating:room]; [self deleteMuc:room withBookmarksUpdate:NO keepBuddylistEntry:NO]; [self handleError:[NSString stringWithFormat:NSLocalizedString(@"Could not create group '%@': timeout", @""), room] forMuc:room withNode:nil andIsSevere:YES]; $$ --(NSString* _Nullable) createGroup:(NSString* _Nullable) node +-(NSString* _Nullable) generateMucJid { - if(node == nil) - node = [self generateSpeakableGroupNode]; + NSString* mucServer = nil; + for(NSString* jid in _account.connectionProperties.conferenceServers) + { + if([_account.connectionProperties.conferenceServers[jid] check:@"identity"]) + { + mucServer = jid; + break; + } + } + if(mucServer == nil) + return nil; + NSString* node = [self generateSpeakableGroupNode]; node = [node stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceAndNewlineCharacterSet].lowercaseString; - NSString* room = [[NSString stringWithFormat:@"%@@%@", node, _account.connectionProperties.conferenceServer] lowercaseString]; + NSString* room = [[NSString stringWithFormat:@"%@@%@", node, mucServer] lowercaseString]; + return room; +} + +-(NSString* _Nullable) createGroup:(NSString*) room +{ if([[DataLayer sharedInstance] isBuddyMuc:room forAccount:_account.accountNo]) { DDLogWarn(@"Cannot create muc already existing in our buddy list, checking if we are still joined and join if needed..."); @@ -892,6 +1029,58 @@ -(NSString* _Nullable) createGroup:(NSString* _Nullable) node 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); + if([self getUIHandlerForMuc:room] != nil) + { + [self callSuccessUIHandlerForMuc:room withCallback:^{ + //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]; @@ -1052,13 +1241,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 @@ -1082,10 +1272,12 @@ -(void) setAffiliation:(NSString*) affiliation ofUser:(NSString*) jid inMuc:(NSS return; } DDLogInfo(@"Successfully changed affiliation of '%@' in '%@' to '%@'", jid, roomJid, affiliation); + [self callSuccessUIHandlerForMuc:iqNode.fromUser]; $$ -(void) changeNameOfMuc:(NSString*) room to:(NSString*) name { + [self incrementNameChange:room]; [self configureMuc:room withMandatoryOptions:@{ @"muc#roomconfig_roomname": name, } andOptionalOptions:@{} deletingMucOnError:NO andJoiningMucOnSuccess:NO]; @@ -1131,9 +1323,11 @@ -(void) publishAvatar:(UIImage* _Nullable) image forMuc:(NSString*) room return; } DDLogInfo(@"Successfully published avatar for muc: %@", iqNode.fromUser); + [self callSuccessUIHandlerForMuc:iqNode.fromUser]; $$ $$instance_handler(handleDiscoResponseInvalidation, account.mucProcessor, $$ID(xmpp*, account), $$ID(NSString*, roomJid)) + [self decrementNameChange:roomJid]; DDLogInfo(@"Removing muc '%@' from _joining...", roomJid); [self removeRoomFromJoining:roomJid]; $$ @@ -1154,6 +1348,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) @@ -1168,6 +1363,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 @@ -1183,6 +1379,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) @@ -1201,6 +1398,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 @@ -1220,6 +1418,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; } @@ -1267,12 +1466,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]; } } @@ -1468,14 +1670,10 @@ -(void) handleError:(NSString*) description forMuc:(NSString*) room withNode:(XM //remove handler (it will only be called once) [self removeUIHandlerForMuc:room]; - if(node == nil) - { - DDLogInfo(@"Could not extract UI error message. node == nil"); - return; - } - - //prepare data - NSString* message = [HelperTools extractXMPPError:node withDescription:description]; + //prepare data + NSString* message = description; + if(node != nil) + message = [HelperTools extractXMPPError:node withDescription:description]; NSDictionary* data = @{ @"success": @NO, @"muc": room, @@ -1493,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 +{ + return [self callSuccessUIHandlerForMuc:room withCallback:nil]; +} + -(void) updateBookmarks { DDLogVerbose(@"Updating bookmarks on account %@", _account); diff --git a/Monal/Classes/MLNotificationManager.m b/Monal/Classes/MLNotificationManager.m index b424b87b70..762165ba87 100644 --- a/Monal/Classes/MLNotificationManager.m +++ b/Monal/Classes/MLNotificationManager.m @@ -809,22 +809,41 @@ -(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"]) { - DDLogError(@"Could not hardlink cache file to notification image temp file!"); - return nil; + 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"]]]; + } + 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) + { + 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/MLOMEMO.m b/Monal/Classes/MLOMEMO.m index d3e327dbe5..752696b827 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 @@ -1062,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; } 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)); diff --git a/Monal/Classes/MLPubSubProcessor.m b/Monal/Classes/MLPubSubProcessor.m index d06615ecde..d96977bf9c 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"); @@ -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,20 @@ @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]; + } + 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 +170,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/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/MLResizingTextView.m b/Monal/Classes/MLResizingTextView.m index 8b0a2288bf..9513833591 100644 --- a/Monal/Classes/MLResizingTextView.m +++ b/Monal/Classes/MLResizingTextView.m @@ -7,6 +7,7 @@ // #import "MLResizingTextView.h" +#import "HelperTools.h" @implementation MLResizingTextView @@ -17,7 +18,8 @@ - (void) layoutSubviews if (!CGSizeEqualToSize(self.bounds.size, [self intrinsicContentSize])) { [self invalidateIntrinsicContentSize]; } - [self becomeFirstResponder]; + if([[HelperTools defaultsDB] boolForKey: @"showKeyboardOnChatOpen"]) + [self becomeFirstResponder]; } - (CGSize)intrinsicContentSize 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) { diff --git a/Monal/Classes/MLServerDetails.m b/Monal/Classes/MLServerDetails.m index 2b079b5f53..0c75203035 100644 --- a/Monal/Classes/MLServerDetails.m +++ b/Monal/Classes/MLServerDetails.m @@ -16,6 +16,7 @@ @interface MLServerDetails () @property (nonatomic, strong) MLContactSoftwareVersionInfo* serverVersion; @property (nonatomic, strong) NSMutableArray* serverCaps; +@property (nonatomic, strong) NSMutableArray* mucServers; @property (nonatomic, strong) NSMutableArray* stunTurnServers; @property (nonatomic, strong) NSMutableArray* srvRecords; @property (nonatomic, strong) NSMutableArray* tlsVersions; @@ -30,6 +31,7 @@ @implementation MLServerDetails enum MLServerDetailsSections { SERVER_VERSION_SECTION, SUPPORTED_SERVER_XEPS_SECTION, + MUC_SERVERS_SECTION, VOIP_SECTION, SRV_RECORS_SECTION, TLS_SECTION, @@ -52,6 +54,7 @@ -(void) viewWillAppear:(BOOL) animated { [super viewWillAppear:animated]; self.serverCaps = [NSMutableArray new]; + self.mucServers = [NSMutableArray new]; self.stunTurnServers = [NSMutableArray new]; self.srvRecords = [NSMutableArray new]; self.tlsVersions = [NSMutableArray new]; @@ -63,6 +66,7 @@ -(void) viewWillAppear:(BOOL) animated self.serverVersion = self.xmppAccount.connectionProperties.serverVersion; [self checkServerCaps:self.xmppAccount.connectionProperties]; + [self checkMucServers:self.xmppAccount.connectionProperties]; [self convertSRVRecordsToReadable]; [self checkTLSVersions:self.xmppAccount.connectionProperties]; [self checkSASLMethods:self.xmppAccount.connectionProperties]; @@ -89,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 @@ -103,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 @@ -131,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 @@ -159,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 @@ -171,6 +175,23 @@ -(void) checkServerCaps:(MLXMPPConnection*) connection }]; } +-(void) checkMucServers:(MLXMPPConnection*) connection +{ + DDLogVerbose(@"Checking muc servers: %@", connection.conferenceServers); + //yes, checkMucServers: is plural, but for now, our connectionProperties only store one single muc server (the first one encountered) + if(connection.conferenceServers.count == 0) + { + [self.mucServers addObject:@{@"Title": NSLocalizedString(@"None", @""), @"Description":NSLocalizedString(@"This server does not provide any MUC servers.", @""), @"Color":SERVER_DETAILS_COLOR_ERROR}]; + return; + } + for(NSString* jid in connection.conferenceServers) + { + NSDictionary* entry = [connection.conferenceServers[jid] findFirst:@"identity@@"]; + [self.mucServers addObject:@{@"Title": [NSString stringWithFormat:NSLocalizedString(@"Server: %@", @""), jid], @"Description": [NSString stringWithFormat:NSLocalizedString(@"%@ (type '%@', category '%@')", @""), entry[@"name"], entry[@"type"], entry[@"category"]], @"Color": [@"text" isEqualToString:entry[@"type"]] ? SERVER_DETAILS_COLOR_OK : SERVER_DETAILS_COLOR_NONE}]; + } + DDLogVerbose(@"Extracted muc server entries: %@", self.mucServers); +} + -(void) checkStunServers:(NSMutableArray*) stunTurnServers { for(NSDictionary* service in stunTurnServers) @@ -214,7 +235,7 @@ -(void) convertSRVRecordsToReadable NSString* prio = [srvEntry objectForKey:@"priority"]; // Check if entry is currently in use - NSString* entryColor = @"None"; + NSString* entryColor = SERVER_DETAILS_COLOR_NONE; if([self.xmppAccount.connectionProperties.server.connectServer isEqualToString:hostname] && self.xmppAccount.connectionProperties.server.connectPort == port && self.xmppAccount.connectionProperties.server.isDirectTLS == [[srvEntry objectForKey:@"isSecure"] boolValue]) @@ -236,8 +257,8 @@ -(void) convertSRVRecordsToReadable -(void) checkTLSVersions:(MLXMPPConnection*) connection { DDLogVerbose(@"connection uses tls version: %@", connection.tlsVersion); - [self.tlsVersions addObject:@{@"Title": NSLocalizedString(@"TLS 1.2", @""), @"Description":NSLocalizedString(@"Older, slower, but still secure TLS version", @""), @"Color":([@"1.2" isEqualToString:connection.tlsVersion] ? SERVER_DETAILS_COLOR_OK : @"None")}]; - [self.tlsVersions addObject:@{@"Title": NSLocalizedString(@"TLS 1.3", @""), @"Description":NSLocalizedString(@"Newest TLS version which is faster than TLS 1.2", @""), @"Color":([@"1.3" isEqualToString:connection.tlsVersion] ? SERVER_DETAILS_COLOR_OK : @"None")}]; + [self.tlsVersions addObject:@{@"Title": NSLocalizedString(@"TLS 1.2", @""), @"Description":NSLocalizedString(@"Older, slower, but still secure TLS version", @""), @"Color":([@"1.2" isEqualToString:connection.tlsVersion] ? SERVER_DETAILS_COLOR_OK : SERVER_DETAILS_COLOR_NONE)}]; + [self.tlsVersions addObject:@{@"Title": NSLocalizedString(@"TLS 1.3", @""), @"Description":NSLocalizedString(@"Newest TLS version which is faster than TLS 1.2", @""), @"Color":([@"1.3" isEqualToString:connection.tlsVersion] ? SERVER_DETAILS_COLOR_OK : SERVER_DETAILS_COLOR_NONE)}]; DDLogVerbose(@"tls versions: %@", self.tlsVersions); } @@ -262,7 +283,7 @@ -(void) checkSASLMethods:(MLXMPPConnection*) connection description = NSLocalizedString(@"Salted Challenge Response Authentication Mechanism using the given Hash Method additionally secured by Channel-Binding", @""); else if([method hasPrefix:@"SCRAM-"]) description = NSLocalizedString(@"Salted Challenge Response Authentication Mechanism using the given Hash Method", @""); - [self.saslMethods addObject:@{@"Title": [NSString stringWithFormat:NSLocalizedString(@"Method: %@", @""), method], @"Description":description, @"Color":(used ? SERVER_DETAILS_COLOR_OK : (!supported ? SERVER_DETAILS_COLOR_NON_IDEAL : @"None"))}]; + [self.saslMethods addObject:@{@"Title": [NSString stringWithFormat:NSLocalizedString(@"Method: %@", @""), method], @"Description":description, @"Color":(used ? SERVER_DETAILS_COLOR_OK : (!supported ? SERVER_DETAILS_COLOR_NON_IDEAL : SERVER_DETAILS_COLOR_NONE))}]; } } @@ -284,7 +305,7 @@ -(void) checkChannelBindingTypes:(MLXMPPConnection*) connection description = NSLocalizedString(@"Secure channel-binding defined for TLS1.3 and some TLS1.2 connections.", @""); else if([type isEqualToString:@"tls-server-end-point"]) description = NSLocalizedString(@"Weakest channel-binding type, not securing against stolen certs/keys, but detects wrongly issued certs.", @""); - [self.channelBindingTypes addObject:@{@"Title": [NSString stringWithFormat:NSLocalizedString(@"Type: %@", @""), type], @"Description":description, @"Color":(used ? SERVER_DETAILS_COLOR_OK : (!supported ? SERVER_DETAILS_COLOR_NON_IDEAL : @"None"))}]; + [self.channelBindingTypes addObject:@{@"Title": [NSString stringWithFormat:NSLocalizedString(@"Type: %@", @""), type], @"Description":description, @"Color":(used ? SERVER_DETAILS_COLOR_OK : (!supported ? SERVER_DETAILS_COLOR_NON_IDEAL : SERVER_DETAILS_COLOR_NONE))}]; } } @@ -301,6 +322,8 @@ -(NSInteger) tableView:(UITableView*) tableView numberOfRowsInSection:(NSInteger return 1; else if(section == SUPPORTED_SERVER_XEPS_SECTION) return (NSInteger)self.serverCaps.count; + else if(section == MUC_SERVERS_SECTION) + return (NSInteger)self.mucServers.count; else if(section == VOIP_SECTION) return (NSInteger)self.stunTurnServers.count; else if(section == SRV_RECORS_SECTION) @@ -335,7 +358,9 @@ -(UITableViewCell*) tableView:(UITableView*) tableView cellForRowAtIndexPath:(NS } else if(indexPath.section == SUPPORTED_SERVER_XEPS_SECTION) dic = [self.serverCaps objectAtIndex:(NSUInteger)indexPath.row]; - if(indexPath.section == VOIP_SECTION) + else if(indexPath.section == MUC_SERVERS_SECTION) + dic = [self.mucServers objectAtIndex:(NSUInteger)indexPath.row]; + else if(indexPath.section == VOIP_SECTION) dic = [self.stunTurnServers objectAtIndex:(NSUInteger)indexPath.row]; else if(indexPath.section == SRV_RECORS_SECTION) dic = [self.srvRecords objectAtIndex:(NSUInteger)indexPath.row]; @@ -390,8 +415,10 @@ -(NSString*) tableView:(UITableView*) tableView titleForHeaderInSection:(NSInteg return NSLocalizedString(@"This is the software running on your server.", @""); else if(section == SUPPORTED_SERVER_XEPS_SECTION) return NSLocalizedString(@"These are the modern XMPP capabilities Monal detected on your server after you have logged in.", @""); + else if(section == MUC_SERVERS_SECTION) + return NSLocalizedString(@"These are the MUC servers detected by Monal (blue entry used by Monal).", @""); else if(section == VOIP_SECTION) - return NSLocalizedString(@"These are STUN and TURN services announced by your server. (blue entries are used by monal)", @""); + return NSLocalizedString(@"These are STUN and TURN services announced by your server (blue entries are used by Monal).", @""); else if(section == SRV_RECORS_SECTION) return NSLocalizedString(@"These are SRV resource records found for your domain.", @""); else if(section == TLS_SECTION) diff --git a/Monal/Classes/MLSettingsTableViewController.m b/Monal/Classes/MLSettingsTableViewController.m index e022cb5047..a73297329e 100644 --- a/Monal/Classes/MLSettingsTableViewController.m +++ b/Monal/Classes/MLSettingsTableViewController.m @@ -32,9 +32,7 @@ }; enum SettingsAppRows { - PrivacySettingsRow, - NotificationsRow, - BackgroundsRow, + GeneralSettingsRow, SoundsRow, SettingsAppRowsCnt }; @@ -157,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; @@ -204,14 +223,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 +335,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; @@ -351,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(); @@ -367,16 +370,16 @@ -(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: #endif case SettingsAboutRowsCntORLogRow:{ - UIViewController* logView = [[SwiftuiInterface new] makeViewWithName:@"logView"]; + UIViewController* logView = [[SwiftuiInterface new] makeViewWithName:@"DebugView"]; [self showDetailViewController:logView sender:self]; break; } diff --git a/Monal/Classes/MLStream.m b/Monal/Classes/MLStream.m index 549fceab4e..4a2c6562b1 100644 --- a/Monal/Classes/MLStream.m +++ b/Monal/Classes/MLStream.m @@ -530,6 +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, *)) + 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 8d66b90af8..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]; }); } @@ -403,8 +405,12 @@ -(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, *)) + if([[HelperTools defaultsDB] boolForKey: @"useDnssecForAllConnections"]) + 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 +446,9 @@ -(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, *)) + if([[HelperTools defaultsDB] boolForKey: @"useDnssecForAllConnections"]) + responseRequest.requiresDNSSECValidation = YES; [responseRequest setHTTPMethod:@"POST"]; [responseRequest setValue:@"application/json" forHTTPHeaderField:@"Accept"]; @@ -448,7 +457,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 +476,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..5bda2b8461 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; @@ -20,16 +21,42 @@ -(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 { [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, *)) + if([[HelperTools defaultsDB] boolForKey: @"useDnssecForAllConnections"]) + nsrequest.requiresDNSSECValidation = YES; [self.webview loadRequest:nsrequest]; } self.navigationItem.largeTitleDisplayMode = UINavigationItemLargeTitleDisplayModeNever; 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; diff --git a/Monal/Classes/MLXMPPConnection.h b/Monal/Classes/MLXMPPConnection.h index b9b1878fdc..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,40 +30,30 @@ 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; @property (nonatomic, strong) NSMutableDictionary* discoveredAdhocCommands; @property (nonatomic, strong) MLContactSoftwareVersionInfo* _Nullable serverVersion; -@property (nonatomic, strong) NSString* _Nullable conferenceServer; +@property (nonatomic, strong) NSMutableDictionary* conferenceServers; @property (nonatomic, assign) BOOL supportsHTTPUpload; @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 a48857fb3e..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,10 @@ -(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]; self.discoveredAdhocCommands = [NSMutableDictionary new]; diff --git a/Monal/Classes/MLXMPPManager.m b/Monal/Classes/MLXMPPManager.m index 5b4a347d0c..f4095737a9 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" @@ -141,6 +142,28 @@ -(void) defaultSettings //anti spam/privacy setting, but default to yes (current behavior, conversations behavior etc.) [self upgradeBoolUserSettingsIfUnset:@"allowNonRosterContacts" toDefault:YES]; [self upgradeBoolUserSettingsIfUnset:@"allowCallsFromNonRosterContacts" toDefault:YES]; + + //mac catalyst will not show a soft-keyboard when setting focus, ios will + //--> only automatically set focus on macos and make this configurable +#if TARGET_OS_MACCATALYST + [self upgradeBoolUserSettingsIfUnset:@"showKeyboardOnChatOpen" toDefault:YES]; +#else + [self upgradeBoolUserSettingsIfUnset:@"showKeyboardOnChatOpen" toDefault:NO]; +#endif + +#ifdef IS_ALPHA + [self upgradeBoolUserSettingsIfUnset:@"useDnssecForAllConnections" toDefault:YES]; +#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 @@ -325,9 +348,9 @@ -(id) init }); nw_path_monitor_start(_path_monitor); - //trigger iq invalidations from a background thread because timeouts aren't time critical - //we use this to decrement the timeout value of an iq handler every second until it reaches zero - dispatch_async(dispatch_queue_create_with_target("im.monal.iqtimeouts", DISPATCH_QUEUE_SERIAL, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0)), ^{ + //trigger iq invalidations and idle timers from a background thread because timeouts aren't time critical + //we use this to decrement the timeout value of an iq handler / idle timer every second until it reaches zero + dispatch_async(dispatch_queue_create_with_target("im.monal.timeouts", DISPATCH_QUEUE_SERIAL, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0)), ^{ while(YES) { for(xmpp* account in [MLXMPPManager sharedInstance].connectedXMPP) [account updateIqHandlerTimeouts]; @@ -848,8 +871,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/MemberList.swift b/Monal/Classes/MemberList.swift index 472e10a80a..a00f412b35 100644 --- a/Monal/Classes/MemberList.swift +++ b/Monal/Classes/MemberList.swift @@ -6,227 +6,338 @@ // 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 ownAffiliation: String + @StateObject var muc: 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 online: Dictionary, Bool> + @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() + @StateObject private var overlay = LoadingOverlayState() init(mucContact: ObservableKVOWrapper) { - self.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)) { + account = MLXMPPManager.sharedInstance().getConnectedAccount(forID: mucContact.accountId)! as xmpp + _muc = StateObject(wrappedValue:mucContact) + _ownAffiliation = State(wrappedValue:"none") + _memberList = State(wrappedValue:OrderedSet>()) + _affiliations = State(wrappedValue:[:]) + _online = State(wrappedValue:[:]) + } + + func updateMemberlist() { + 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 } - affiliationTmp.updateValue((memberInfo["affiliation"] as? String) ?? "none", forKey: jid) + 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 + } else { + online[contact] = false + } } - _affiliation = State(wrappedValue: affiliationTmp) + 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 { + return performMucAction(account:self.account, mucJid:self.muc.contactJid, overlay:self.overlay, headlineView:headlineView, descriptionView:descriptionView, action:action) } - 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 { + //we don't want to set affiliation=none in channels using deletion swipe (this does not delete the user) + if self.muc.mucType == "channel" { return false } - if let contactAffiliation = self.affiliation[contact.contactJid] { - if self.ownAffiliation == "owner" { + if contact.contactJid == account.connectionProperties.identity.jid { + return false + } + 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 } - - 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") - }) + + func actionsAllowed(for contact:ObservableKVOWrapper) -> [String] { + 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"*/] + 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"] + } 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] + } } - 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")) - } else { - Text(NSLocalizedString("", comment: "muc affiliation")) + } else { + if ownAffiliation == "owner" { + 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"] + } 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] + } + } + } + } + //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",*/ "reinvite"] + } else { + 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) } - .onTapGesture(perform: { - if contact.obj.contactJid != self.account.connectionProperties.identity.jid { - self.selectedMember = contact.obj + } 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) } - }) - .deleteDisabled( - !ownUserHasAffiliationToRemove(contact: contact) - ) + } + } 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) + } } - .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") + ), 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 { + 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") + } } } - } - .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 { + + ForEach(memberList, id:\.self) { contact in + if !contact.isSelfChat { + HStack { HStack { - Spacer() - Image(uiImage: selectedMember.avatar) - .resizable() - .frame(width: 150, height: 150, alignment: .center) + ContactEntry(contact:contact) { + Text("Affiliation: \(mucAffiliationToString(affiliations[contact]))\(!(online[contact] ?? false) ? Text(" (offline)") : Text(""))") + //.foregroundColor(Color(UIColor.secondaryLabel)) + .font(.footnote) + } Spacer() } - HStack { - Spacer() - Text(selectedMember.contactDisplayName as String) - Spacer() + .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) + ) + + if ownAffiliation == "owner" || ownAffiliation == "admin" { + makePickerView(contact:contact) + .fixedSize() + .offset(x:8, y:0) } } - 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) + .applyClosure { view in + if !(online[contact] ?? false) { + view.opacity(0.5) + } else { + view } } + .deleteDisabled(!ownUserHasAffiliationToRemove(contact: contact)) } } - }) - } - .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) - } - self.selectedMember = nil - }) { - Text("Remove from group") + .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) + } + } + }) } - } 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() + .actionSheet(isPresented: $showActionSheet) { + ActionSheet( + title: actionSheetPrompt.title, + message: actionSheetPrompt.message, + buttons: [ + .cancel(), + .destructive( + Text("Yes"), + action: actionSheetPrompt.closure + ) + ] + ) } - } - - func makeOwner(_ selectedMember: ObservableKVOWrapper) -> some View { - return affiliationButton(selectedMember, affiliation: "owner", label: { - Text("Make owner") - }) - } - - 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") + .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() + } + } + } } +} - func block(_ selectedMember: ObservableKVOWrapper) -> AnyView { - if self.group.mucType != "group" { - return AnyView( - affiliationButton(selectedMember, affiliation: "outcast", label: { - Text("Block from group") - }) - ) - } else { - return AnyView(EmptyView()) - } +extension UIPickerView { + override open func didMoveToSuperview() { + super.didMoveToSuperview() + self.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) } } diff --git a/Monal/Classes/MonalAppDelegate.m b/Monal/Classes/MonalAppDelegate.m index e07a8c237f..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]; @@ -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}]; @@ -1023,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]; }); }); } @@ -1949,7 +1942,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,77 +1957,80 @@ -(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]; 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) + [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; } } diff --git a/Monal/Classes/NotificationSettings.swift b/Monal/Classes/NotificationDebugging.swift similarity index 77% rename from Monal/Classes/NotificationSettings.swift rename to Monal/Classes/NotificationDebugging.swift index 42e3135967..3dc7396466 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,9 +25,9 @@ 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) + 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) } @@ -70,29 +49,29 @@ 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) + 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) { 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) + 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) } } Section(header: Text("Pushserver Region").font(.title3)) { - Picker("Push Server", selection: $selectedPushServer) { + Picker(selection: $selectedPushServer, label: Text("Push Server")) { ForEach(self.availablePushServers.sorted(by: >), id: \.key) { pushServerFqdn, pushServerName in Text(pushServerName).tag(pushServerFqdn) } @@ -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/OmemoKeys.swift b/Monal/Classes/OmemoKeys.swift index 8621abc8dd..09eea4dd70 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 @@ -382,14 +380,14 @@ 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 }) .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) diff --git a/Monal/Classes/OmemoQrCodeView.swift b/Monal/Classes/OmemoQrCodeView.swift index fc880295aa..0ce34146bd 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 { @@ -59,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/PrivacySettings.swift b/Monal/Classes/PrivacySettings.swift deleted file mode 100644 index a7fb6754b2..0000000000 --- a/Monal/Classes/PrivacySettings.swift +++ /dev/null @@ -1,258 +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 -} - - -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") - } - } - 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("Notification privacy", selection: $privacyDefaultDB.notificationPrivacySetting) { - ForEach(NotificationPrivacySettingOption.allCases, id: \.self) { option in - Text(getNotificationPrivacyOption(option)).tag(option.rawValue) - } - } - Toggle("Enable encryption by default for new chats", isOn: $privacyDefaultDB.omemoDefaultOn) - Toggle("Autodelete all messages after 3 days", isOn: $privacyDefaultDB.autodeleteAllMessagesAfter3Days) - } - .navigationBarTitle("Privacy & security", displayMode: .inline) - } -} - -struct PublishingScreen: View { - @ObservedObject var privacyDefaultDB = PrivacyDefaultDB() - - var body: some View { - Form { - Toggle("Send last interaction time", isOn: $privacyDefaultDB.sendLastUserInteraction) - Toggle("Send typing notifications", isOn: $privacyDefaultDB.sendLastChatState) - Toggle("Send message received state", isOn: $privacyDefaultDB.sendReceivedMarkers) - Toggle("Send message displayed state", isOn: $privacyDefaultDB.sendDisplayedMarkers) - } - .navigationBarTitle("Publishing", displayMode: .inline) - } -} - -struct PreviewsScreen: View { - @ObservedObject var privacyDefaultDB = PrivacyDefaultDB() - - var body: some View { - Form { - Toggle("Show inline geo location", isOn: $privacyDefaultDB.showGeoLocation) - Toggle("Show URL previews", isOn: $privacyDefaultDB.showURLPreview) - } - .navigationBarTitle("Previews", displayMode: .inline) - } -} - -struct CommunicationScreen: View { - @ObservedObject var privacyDefaultDB = PrivacyDefaultDB() - - var body: some View { - Form { - Toggle("Allow contacts not in my contact list to contact me", isOn: $privacyDefaultDB.allowNonRosterContacts) - Toggle("Allow approved contacts to query my Monal and iOS version", isOn: $privacyDefaultDB.allowVersionIQ) - Toggle("Calls: Allow contacts not in my contact list to call me", isOn: $privacyDefaultDB.allowCallsFromNonRosterContacts) - Toggle("Calls: Allow P2P sessions", isOn: $privacyDefaultDB.webrtcAllowP2P) - Toggle("Calls: Allow TURN fallback to Monal-Servers", isOn: $privacyDefaultDB.webrtcUseFallbackTurn) - } - .navigationBarTitle("Communication", displayMode: .inline) - } -} - -struct MLAutoDownloadFiletransferSettingView: View { - @ObservedObject var privacyDefaultDB = PrivacyDefaultDB() - - var body: some View { - Form { - Section(header: Text("General File Transfer Settings")) { - Toggle("Auto-Download Media", isOn: $privacyDefaultDB.autodownloadFiletransfers) - } - - 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 upto : \(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 upto : \(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/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/RegisterAccount.swift b/Monal/Classes/RegisterAccount.swift index f3bc791d4b..90a7b7c9a8 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, *), HelperTools.defaultsDB().bool(forKey:"useDnssecForAllConnections") { + request.requiresDNSSECValidation = true; + } webView.load(request) } } @@ -71,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))") @@ -385,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) @@ -407,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: { diff --git a/Monal/Classes/RichAlert.swift b/Monal/Classes/RichAlert.swift index 4b2a12a329..d03e90d9f4 100644 --- a/Monal/Classes/RichAlert.swift +++ b/Monal/Classes/RichAlert.swift @@ -6,8 +6,8 @@ // Copyright © 2022 monal-im.org. All rights reserved. // -import SwiftUI import ViewExtractor +import FrameUp struct RichAlertView: ViewModifier where TitleContent: View, BodyContent: View, ButtonContent: View { @Binding public var isPresented: T? @@ -30,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) diff --git a/Monal/Classes/SwiftHelpers.swift b/Monal/Classes/SwiftHelpers.swift index ffd0887d55..0ad13c020c 100644 --- a/Monal/Classes/SwiftHelpers.swift +++ b/Monal/Classes/SwiftHelpers.swift @@ -10,9 +10,13 @@ @_exported import Foundation @_exported import CocoaLumberjackSwift @_exported import Logging +@_exported import PromiseKit 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) @@ -28,6 +32,14 @@ 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) +} + 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!) @@ -87,7 +99,7 @@ class KVOObserver: NSObject { } @dynamicMemberLookup -public class ObservableKVOWrapper: ObservableObject, Hashable, Equatable { +public class ObservableKVOWrapper: ObservableObject, Hashable, Equatable, CustomStringConvertible, Identifiable { public var obj: ObjType private var observedMembers: NSMutableSet = NSMutableSet() private var observers: [KVOObserver] = Array() @@ -154,17 +166,41 @@ 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 { 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) { @@ -226,6 +262,7 @@ public struct defaultsDB { } } + @objcMembers public class SwiftHelpers: NSObject { public static func initSwiftHelpers() { @@ -236,6 +273,55 @@ 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) { + 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 + } + + //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) { + //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 + } } @objcMembers diff --git a/Monal/Classes/SwiftuiHelpers.swift b/Monal/Classes/SwiftuiHelpers.swift index 3e60317783..1afde7b542 100644 --- a/Monal/Classes/SwiftuiHelpers.swift +++ b/Monal/Classes/SwiftuiHelpers.swift @@ -24,16 +24,12 @@ let monalGreen = Color(UIColor(red:128.0/255, green:203.0/255, blue:182.0/255, a let monalDarkGreen = Color(UIColor(red:20.0/255, green:138.0/255, blue:103.0/255, alpha:1.0)); //see https://stackoverflow.com/a/62207329/3528174 -public extension Color { -#if os(macOS) - static let background = Color(NSColor.windowBackgroundColor) - static let secondaryBackground = Color(NSColor.underPageBackgroundColor) - static let tertiaryBackground = Color(NSColor.controlBackgroundColor) -#else - static let background = Color(UIColor.systemBackground) - static let secondaryBackground = Color(UIColor.secondarySystemBackground) - static let tertiaryBackground = Color(UIColor.tertiarySystemBackground) -#endif +//and https://www.hackingwithswift.com/forums/100-days-of-swiftui/extending-shapestyle-for-adding-colors-instead-of-extending-color/12324 +public extension ShapeStyle where Self == Color { + static var interpolatedWindowBackground: Color { Color(UIColor { $0.userInterfaceStyle == .dark ? UIColor.systemBackground : UIColor.secondarySystemBackground }) } + static var background: Color { Color(UIColor.systemBackground) } + static var secondaryBackground: Color { Color(UIColor.secondarySystemBackground) } + static var tertiaryBackground: Color { Color(UIColor.tertiarySystemBackground) } } extension Binding { @@ -47,7 +43,6 @@ extension Binding { ) } } - extension Binding { func bytecount(mappedTo: Double) -> Binding where Value == UInt { Binding( @@ -70,6 +65,162 @@ class SheetDismisserProtocol: ObservableObject { } } +func getContactList(viewContact: (ObservableKVOWrapper?)) -> OrderedSet> { + if let contact = viewContact { + 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() + 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 [] + } +} + +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" { + 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") +} + +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 { + 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 { + 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) { + 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 @@ -208,7 +359,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) @@ -444,19 +595,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() @@ -481,9 +619,7 @@ 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 "logView": + case "DebugView": host.rootView = AnyView(UIKitWorkaround(DebugView())) case "WelcomeLogIn": host.rootView = AnyView(AddTopLevelNavigation(withDelegate:delegate, to:WelcomeLogIn(delegate:delegate))) @@ -495,39 +631,15 @@ 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() } 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/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/Classes/WelcomeLogIn.swift b/Monal/Classes/WelcomeLogIn.swift index 765852a813..d1c277a944 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,}$" @@ -113,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 { @@ -179,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() } })) } @@ -218,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) 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 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 1ed71c276e..6f5ee37c08 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 @@ -54,6 +55,8 @@ @interface chatViewController()* _localMLContactCache; BOOL _isRecording; + BOOL _isAtBottom; + monal_void_block_t _scrollToBottomTimer; } @property (nonatomic, strong) NSDateFormatter* destinationDateFormat; @@ -83,9 +86,7 @@ @interface chatViewController()_localMLContactCache) { [self->_localMLContactCache removeAllObjects]; } [self refreshData]; [self reloadTable]; - }); + }]; } -(void) openCallScreen:(id) sender @@ -636,8 +637,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(), ^{ @@ -801,11 +815,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]; } @@ -813,10 +828,25 @@ -(void) viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; #ifndef DISABLE_OMEMO - if(self.xmppAccount) + if(self.xmppAccount && [[DataLayer sharedInstance] isAccountEnabled:self.xmppAccount.accountNo]) { - BOOL omemoDeviceForContactFound = [self.xmppAccount.omemo knownDevicesForAddressName:self.contact.contactJid].count > 0; - if(!omemoDeviceForContactFound && self.contact.isEncrypted && [[DataLayer sharedInstance] isAccountEnabled:self.xmppAccount.accountNo]) + BOOL omemoDeviceForContactFound = NO; + if(!self.contact.isGroup) + omemoDeviceForContactFound = [self.xmppAccount.omemo knownDevicesForAddressName:self.contact.contactJid].count > 0; + else + { + omemoDeviceForContactFound = NO; + for(NSDictionary* participant in [[DataLayer sharedInstance] getMembersAndParticipantsOfMuc:self.contact.contactJid forAccountId:self.xmppAccount.accountNo]) + { + if(participant[@"participant_jid"]) + omemoDeviceForContactFound |= [self.xmppAccount.omemo knownDevicesForAddressName:participant[@"participant_jid"]].count > 0; + else if(participant[@"member_jid"]) + omemoDeviceForContactFound |= [self.xmppAccount.omemo knownDevicesForAddressName:participant[@"member_jid"]].count > 0; + if(omemoDeviceForContactFound) + break; + } + } + if(!omemoDeviceForContactFound && self.contact.isEncrypted) { if(!self.contact.isGroup && [[HelperTools splitJid:self.contact.contactJid][@"host"] isEqualToString:@"cheogram.com"]) { @@ -976,12 +1006,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(), ^{ @@ -1657,6 +1683,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]; @@ -1669,7 +1696,8 @@ -(MLMessage* _Nullable) addMessageto:(NSString *)to withMessage:(nonnull NSStrin withRowAnimation:UITableViewRowAnimationNone]; } } completion:^(BOOL finished) { - [self scrollToBottom]; + if(wasAtBottom) + [self scrollToBottomAnimated:NO]; }]; }); @@ -1702,6 +1730,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]; @@ -1738,7 +1768,7 @@ -(void) handleNewMessage:(NSNotification *)notification } [self->_messageTable endUpdates]; - [self scrollToBottom]; + [CATransaction commit]; if (self.searchController.isActive) @@ -1749,6 +1779,9 @@ -(void) handleNewMessage:(NSNotification *)notification } [self refreshCounter]; + + if(wasAtBottom) + [self scrollToBottomAnimated:YES]; }); } } @@ -1831,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 @@ -1885,20 +1916,41 @@ -(void) handleFiletransferMessageUpdate:(NSNotification*) notification } } --(void) scrollToBottom +-(void) scrollToBottomIfNeeded { - if(self.messageList.count == 0) return; - dispatch_async(dispatch_get_main_queue(), ^{ + if(_isAtBottom) + { + //DDLogVerbose(@"Scrolling to bottom because needed: %@", [NSThread callStackSymbols]); + [self scrollToBottomAnimated:NO]; + } +} + +-(void) scrollToBottomAnimated:(BOOL) animated +{ + if(self.messageList.count == 0) + return; + 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]; - // if(![self.messageTable.indexPathsForVisibleRows containsObject:path1]) - { - [self.messageTable scrollToRowAtIndexPath:path1 atScrollPosition:UITableViewScrollPositionTop animated:YES]; - } + [self.messageTable scrollToRowAtIndexPath:path1 atScrollPosition:UITableViewScrollPositionBottom animated:animated]; + self->_isAtBottom = YES; } - }); + [self refreshCounter]; + }; + 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 @@ -2546,15 +2598,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, @@ -2569,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; @@ -2650,23 +2710,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 @@ -2885,9 +2940,9 @@ -(void) commandFPressed:(UIKeyCommand*)keyCommand # pragma mark - Textview delegate functions -- (void)textViewDidBeginEditing:(UITextView *)textView +-(void) textViewDidBeginEditing:(UITextView*) textView { - [self scrollToBottom]; + [self scrollToBottomIfNeeded]; } - (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text @@ -2967,13 +3022,16 @@ -(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, *)) + if([[HelperTools defaultsDB] boolForKey: @"useDnssecForAllConnections"]) + 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; } @@ -2984,7 +3042,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; } @@ -2999,7 +3057,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; } @@ -3040,11 +3098,15 @@ -(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, *)) + 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"]; 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 @@ -3088,7 +3150,7 @@ - (void)keyboardWillDisappear:(NSNotification*) aNotification - (void)keyboardDidShow:(NSNotification*)aNotification { - //TODO grab animation info + //TODO grab animation info NSDictionary* info = [aNotification userInfo]; CGSize kbSize = [[info objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue].size; if(kbSize.height > 100) { //my inputbar +any other @@ -3098,10 +3160,8 @@ - (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) + [self scrollToBottomIfNeeded]; } - (void)keyboardDidHide:(NSNotification*)aNotification @@ -3116,6 +3176,7 @@ - (void)keyboardDidHide:(NSNotification*)aNotification - (void)keyboardWillShow:(NSNotification*)aNotification { + [self setChatInputHeightConstraints:NO]; //TODO grab animation info // UIEdgeInsets contentInsets = UIEdgeInsetsZero; diff --git a/Monal/Classes/xmpp.h b/Monal/Classes/xmpp.h index a34be781da..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; @@ -228,7 +229,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 55d4e3ca86..2f9a58563b 100644 --- a/Monal/Classes/xmpp.m +++ b/Monal/Classes/xmpp.m @@ -47,9 +47,8 @@ #import "AESGcm.h" @import AVFoundation; -@import WebRTC; -#define STATE_VERSION 15 +#define STATE_VERSION 18 #define CONNECT_TIMEOUT 7.0 #define IQ_TIMEOUT 60.0 NSString* const kQueueID = @"queueID"; @@ -198,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 @@ -717,7 +716,7 @@ -(BOOL) connectionTask -(BOOL) parseQueueFrozen { - return _parseQueue.suspended == YES; + return [_parseQueue isSuspended] == YES; } -(void) freezeParseQueue @@ -730,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 @@ -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 @@ -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}]; } } } @@ -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"]); @@ -3327,6 +3312,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, @@ -3492,12 +3478,14 @@ -(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"]; - if(self.connectionProperties.conferenceServer) - [values setObject:self.connectionProperties.conferenceServer forKey:@"conferenceServer"]; + + if(self.connectionProperties.conferenceServers) + [values setObject:self.connectionProperties.conferenceServers forKey:@"conferenceServers"]; [values setObject:[self.pubsub getInternalData] forKey:@"pubsubData"]; [values setObject:[self.mucProcessor getInternalState] forKey:@"mucState"]; @@ -3506,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"]; @@ -3543,14 +3524,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\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), @@ -3561,18 +3544,16 @@ -(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, - 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); } @@ -3594,10 +3575,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 @@ -3647,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]; @@ -3655,7 +3642,7 @@ -(void) realReadState self.connectionProperties.serverVersion = [dic objectForKey:@"serverVersion"]; self.connectionProperties.uploadServer = [dic objectForKey:@"uploadServer"]; - self.connectionProperties.conferenceServer = [dic objectForKey:@"conferenceServer"]; + self.connectionProperties.conferenceServers = [[dic objectForKey:@"conferenceServers"] mutableCopy]; if([dic objectForKey:@"loggedInOnce"]) { @@ -3675,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"]; @@ -3723,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"]; @@ -3782,8 +3727,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\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), @@ -3794,18 +3745,16 @@ -(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, - self.omemo.state + self.omemo.state, + bool2str(self.hasSeenOmemoDeviceListAfterOwnDeviceid) ); if(self.unAckedStanzas) for(NSDictionary* dic in self.unAckedStanzas) @@ -3822,7 +3771,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) @@ -3923,32 +3872,23 @@ -(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]; self.connectionProperties.serverVersion = nil; - self.connectionProperties.conferenceServer = nil; + 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 @@ -4055,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)]; @@ -4092,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]; @@ -4133,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]; @@ -4144,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]; @@ -4281,9 +4224,9 @@ -(void) sendCurrentCSIState { [self dispatchOnReceiveQueue: ^{ //don't send anything before a resource is bound - if(self.accountState*) 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.accountDiscoFeatures 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 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/Monal.xcodeproj/project.pbxproj b/Monal/Monal.xcodeproj/project.pbxproj index e8969e929d..493d25f3f6 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 */; }; @@ -160,8 +160,11 @@ 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 */; }; + 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 */; }; @@ -177,10 +180,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 */; }; @@ -306,7 +306,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 = ""; }; @@ -502,7 +502,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 = ""; }; @@ -592,6 +592,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 = ""; }; @@ -636,8 +640,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 +668,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 = ""; }; @@ -773,6 +774,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 */, @@ -803,8 +805,10 @@ buildActionMask = 2147483647; files = ( 849ADF3F2BACF0360009BCD7 /* CocoaLumberjack in Frameworks */, + 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 */, ); @@ -1002,11 +1006,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 = ""; @@ -1099,11 +1103,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"; @@ -1527,6 +1528,7 @@ C1F5C7AE2777638B0001F295 /* OrderedCollections */, C1E1EC7A286A025F0097EC74 /* SwiftSoup */, 841898A92957712000FEC77D /* ViewExtractor */, + 84F194D02C15197200F0A994 /* FrameUp */, ); productName = SworIM; productReference = 26080210110ABA4E005E194D /* Monal.app */; @@ -1590,6 +1592,8 @@ 849ADF3E2BACF0360009BCD7 /* CocoaLumberjack */, 849ADF402BACF0360009BCD7 /* CocoaLumberjackSwift */, 849ADF422BACF0360009BCD7 /* CocoaLumberjackSwiftLogBackend */, + 84F194CD2C101A3E00F0A994 /* PromiseKit */, + 84E231F22C16A9CE00735FB7 /* SVGView */, ); productName = monalxmpp; productReference = 26CC579223A0867400ABB92A /* monalxmpp.framework */; @@ -1733,6 +1737,7 @@ pa, ko, eu, + "es-AR", ); mainGroup = 29B97314FDCFA39411CA2CEA /* CustomTemplate */; packageReferences = ( @@ -1740,6 +1745,9 @@ C1E1EC79286A025F0097EC74 /* XCRemoteSwiftPackageReference "SwiftSoup" */, 841898A82957712000FEC77D /* XCRemoteSwiftPackageReference "ViewExtractor" */, 849ADF3D2BACF0360009BCD7 /* XCRemoteSwiftPackageReference "cocoalumberjack" */, + 84F194CC2C101A3E00F0A994 /* XCRemoteSwiftPackageReference "PromiseKit" */, + 84F194CF2C15197200F0A994 /* XCRemoteSwiftPackageReference "FrameUp" */, + 84E231F12C16A9CE00735FB7 /* XCRemoteSwiftPackageReference "SVGView" */, ); productRefGroup = 19C28FACFE9D520D11CA2CBB /* Products */; projectDirPath = ""; @@ -2045,9 +2053,8 @@ 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 */, - C15382622B89C38300EA83EC /* EditGroupName.swift in Sources */, C1E8A7F72B8E47C300760220 /* EditGroupSubject.swift in Sources */, 263DFAC32187D0E00038E716 /* MLLinkCell.m in Sources */, 3D65B78D27234B74005A30F4 /* ContactDetails.swift in Sources */, @@ -2066,7 +2073,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 */, @@ -2077,8 +2084,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 */, @@ -2289,6 +2294,7 @@ C132EA9B26C92E9000BB9A67 /* pa */, C15A4E5F279D2AC80055CD11 /* ko */, C1E856A828DECF5F00B104E9 /* eu */, + 84F194C52C0FE78B00F0A994 /* es-AR */, ); name = iosShare.storyboard; sourceTree = ""; @@ -2330,6 +2336,7 @@ C132EA9626C92DD900BB9A67 /* pa */, C15A4E5D279D2AC70055CD11 /* ko */, C1E856A728DECF5F00B104E9 /* eu */, + 84F194C42C0FE74500F0A994 /* es-AR */, ); name = Settings.storyboard; sourceTree = ""; @@ -2371,6 +2378,7 @@ C132EA9426C92DD900BB9A67 /* pa */, C15A4E5B279D2AC60055CD11 /* ko */, C1E856A628DECF5F00B104E9 /* eu */, + 84F194C32C0FE70900F0A994 /* es-AR */, ); name = Main.storyboard; sourceTree = ""; @@ -2411,6 +2419,7 @@ C132EA9926C92DDA00BB9A67 /* pa */, C15A4E60279D2AC80055CD11 /* ko */, C1E856A928DECF5F00B104E9 /* eu */, + 84F194C62C0FE79000F0A994 /* es-AR */, ); name = Localizable.strings; sourceTree = ""; @@ -2657,7 +2666,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 14.0; LLVM_LTO = YES; MACOSX_DEPLOYMENT_TARGET = 11.0; - MARKETING_VERSION = 6.3.0; + MARKETING_VERSION = 6.4.0; PRESERVE_DEAD_CODE_INITS_AND_TERMS = NO; SDKROOT = iphoneos; STRIP_INSTALLED_PRODUCT = NO; @@ -3024,7 +3033,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 14.0; LLVM_LTO = YES; MACOSX_DEPLOYMENT_TARGET = 11.0; - MARKETING_VERSION = 6.3.0; + MARKETING_VERSION = 6.4.0; PRESERVE_DEAD_CODE_INITS_AND_TERMS = NO; SDKROOT = iphoneos; STRIP_INSTALLED_PRODUCT = NO; @@ -3186,7 +3195,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 14.0; LLVM_LTO = YES; MACOSX_DEPLOYMENT_TARGET = 11.0; - MARKETING_VERSION = 6.3.0; + MARKETING_VERSION = 6.4.0; ONLY_ACTIVE_ARCH = YES; PRESERVE_DEAD_CODE_INITS_AND_TERMS = NO; RUN_CLANG_STATIC_ANALYZER = YES; @@ -3462,7 +3471,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 14.0; LLVM_LTO = YES; MACOSX_DEPLOYMENT_TARGET = 11.0; - MARKETING_VERSION = 6.3.0; + MARKETING_VERSION = 6.4.0; PRESERVE_DEAD_CODE_INITS_AND_TERMS = NO; RUN_CLANG_STATIC_ANALYZER = YES; SDKROOT = iphoneos; @@ -3815,7 +3824,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 14.0; LLVM_LTO = YES; MACOSX_DEPLOYMENT_TARGET = 11.0; - MARKETING_VERSION = 6.3.0; + MARKETING_VERSION = 6.4.0; PRESERVE_DEAD_CODE_INITS_AND_TERMS = NO; SDKROOT = iphoneos; STRIP_INSTALLED_PRODUCT = NO; @@ -4229,7 +4238,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 14.0; LLVM_LTO = NO; MACOSX_DEPLOYMENT_TARGET = 11.0; - MARKETING_VERSION = 6.3.0; + MARKETING_VERSION = 6.4.0; PRESERVE_DEAD_CODE_INITS_AND_TERMS = NO; RUN_CLANG_STATIC_ANALYZER = YES; SDKROOT = iphoneos; @@ -4625,6 +4634,30 @@ 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"; + requirement = { + kind = upToNextMajorVersion; + 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"; @@ -4668,6 +4701,21 @@ package = 849ADF3D2BACF0360009BCD7 /* XCRemoteSwiftPackageReference "cocoalumberjack" */; productName = CocoaLumberjackSwiftLogBackend; }; + 84E231F22C16A9CE00735FB7 /* SVGView */ = { + isa = XCSwiftPackageProductDependency; + package = 84E231F12C16A9CE00735FB7 /* XCRemoteSwiftPackageReference "SVGView" */; + productName = SVGView; + }; + 84F194CD2C101A3E00F0A994 /* PromiseKit */ = { + isa = XCSwiftPackageProductDependency; + package = 84F194CC2C101A3E00F0A994 /* XCRemoteSwiftPackageReference "PromiseKit" */; + productName = PromiseKit; + }; + 84F194D02C15197200F0A994 /* FrameUp */ = { + isa = XCSwiftPackageProductDependency; + package = 84F194CF2C15197200F0A994 /* XCRemoteSwiftPackageReference "FrameUp" */; + productName = FrameUp; + }; C1E1EC7A286A025F0097EC74 /* SwiftSoup */ = { isa = XCSwiftPackageProductDependency; package = C1E1EC79286A025F0097EC74 /* XCRemoteSwiftPackageReference "SwiftSoup" */; diff --git a/Monal/NotificationService/NotificationService.m b/Monal/NotificationService/NotificationService.m index 0eaf40f866..6bb79c6e57 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 @@ -478,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; diff --git a/Monal/localization/Base.lproj/Main.storyboard b/Monal/localization/Base.lproj/Main.storyboard index 16e176ab65..dd2ae0e236 100644 --- a/Monal/localization/Base.lproj/Main.storyboard +++ b/Monal/localization/Base.lproj/Main.storyboard @@ -216,9 +216,8 @@ - + - @@ -2537,7 +2536,6 @@ - @@ -2547,9 +2545,6 @@ - - - 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 @@ + 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/opensource.html b/Monal/opensource.html index 0bc30c1e50..6260f3837d 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,88 @@
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
+ 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 +294,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 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 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 | diff --git a/monal.doap b/monal.doap index 7fc3229462..7ece02a1f2 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 @@ -469,7 +469,7 @@ complete 6.0 - 6.0 + 0.6.0 XEP-0353: Jingle Message Initiation @@ -505,13 +505,6 @@ XEP-0368: SRV records for XMPP over TLS - - - - planned - XEP-0369: Mediated Information eXchange (MIX) - - @@ -562,7 +555,7 @@ complete 6.0 - 0.4.0 + 1.0.1 XEP-0388: Extensible SASL Profile @@ -616,7 +609,7 @@ complete 5.4 - 1.1.3 + 1.1.4 XEP-0402: PEP Native Bookmarks @@ -715,6 +708,21 @@ XEP-0480: SASL Upgrade Tasks + + + + planned + XEP-0484: Fast Authentication Streamlining Tokens + + + + + + complete + 5.0 + XEP-0486: MUC Avatars + + 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(), diff --git a/scripts/build.sh b/scripts/build.sh index 1d2823dff4..9bcb101369 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -102,7 +102,7 @@ xcrun xcodebuild \ -configuration $BUILD_TYPE \ -archivePath "build/ios_$APP_NAME.xcarchive" \ -allowProvisioningUpdates \ - clean archive + archive echo "" echo "*************************" diff --git a/scripts/updateLocalization.sh b/scripts/updateLocalization.sh index d5ad34b588..5b2cdf7b2e 100755 --- a/scripts/updateLocalization.sh +++ b/scripts/updateLocalization.sh @@ -9,7 +9,12 @@ if ! which bartycrouch > /dev/null; then echo "ERROR: BartyCrouch not installed, download it from https://github.com/Flinesoft/BartyCrouch" exit 1 fi - + +compile_swift="NO" +if [ "x$2" != "x" ]; then + compile_swift="$2" +fi + function pullCurrentState { #subshell to not leak from "cd $folder" ( @@ -58,33 +63,60 @@ function runBartycrouch { bartycrouch lint -x -w } +echo "" +echo "***************************************" +echo "* Initializing submodules *" +echo "***************************************" git submodule deinit --all -f git submodule update --init --recursive --remote pullCurrentState "$@" +if [ "$compile_swift" == "YES" ]; then + echo "" + echo "*******************************************" + echo "* Building rust packages & bridge *" + echo "*******************************************" + bash ../rust/build-rust.sh + + echo "" + echo "***************************************" + echo "* Installing macOS & iOS Pods *" + echo "***************************************" + pod install --repo-update +fi + +echo "" +echo "***************************************" +echo "* Removing unused strings *" +echo "***************************************" # update strings to remove everything that's now unused (that includes swiftui strings we'll readd below) cp .bartycrouch.toml .bartycrouch.toml.orig sed 's/additive = true/additive = false/g' .bartycrouch.toml > .bartycrouch.toml.new +rm .bartycrouch.toml mv .bartycrouch.toml.new .bartycrouch.toml runBartycrouch +rm .bartycrouch.toml mv .bartycrouch.toml.orig .bartycrouch.toml - # now restore original state for all languages but our base one (otherwise every swiftui translation will be deleted) mv "localization/external/Base.lproj/Localizable.strings" "localization/external/Base.lproj/Localizable.strings.updated" pullCurrentState "$@" mv "localization/external/Base.lproj/Localizable.strings.updated" "localization/external/Base.lproj/Localizable.strings" -# extract xliff file (has to be run multiple times, even if no error occured, don't ask me why) -# we use grep here to test for a dummy string to detect if our run succeeded +echo "" +echo "***************************************" +echo "* Extracting xliff files *" +echo "***************************************" if [ -e localization.tmp ]; then rm -rf localization.tmp fi +# extract xliff file (has to be run multiple times, even if no error occured, don't ask me why) +# we use grep here to test for a dummy string to detect if our run succeeded dummy="DON'T TRANSLATE: $(head /dev/urandom | LC_ALL=C tr -dc A-Za-z0-9 | head -c 8)" -echo "\nlet swiftuiTranslationRandomDummyString = Text(\"$dummy\")" >> Classes/SwiftuiHelpers.swift +#echo "\nlet swiftuiTranslationRandomDummyString = Text(\"$dummy\")" >> Classes/SwiftuiHelpers.swift x=$((1)) while [[ $x -lt 16 ]]; do echo "STARTING RUN $x..." - while ! xcrun xcodebuild -exportLocalizations -localizationPath localization.tmp -exportLanguage base SWIFT_EMIT_LOC_STRINGS=NO; do + while ! xcrun xcodebuild -workspace "Monal.xcworkspace" -scheme "Monal" -sdk iphoneos -configuration "Beta" -allowProvisioningUpdates -exportLocalizations -localizationPath localization.tmp -exportLanguage base SWIFT_EMIT_LOC_STRINGS="$compile_swift"; do echo "ERROR, TRYING AGAIN..." done echo "RUN $x SUCCEEDED, EXTRACTING STRINGS FROM XLIFF!" @@ -92,20 +124,27 @@ while [[ $x -lt 16 ]]; do ../scripts/xliff_extractor.py -x "localization.tmp/base.xcloc/Localized Contents/base.xliff" x=$((x+1)) done -rm -rf *A\ Document\ Being\ Saved\ By\ xcodebuild* if ! grep -q "$dummy" "localization/external/Base.lproj/Localizable.strings"; then echo "Could not extract dummy string after $x runs!" - exit 1 + #exit 1 fi awk "!/$dummy/" "localization/external/Base.lproj/Localizable.strings" > "localization/external/Base.lproj/Localizable.strings.new" mv "localization/external/Base.lproj/Localizable.strings.new" "localization/external/Base.lproj/Localizable.strings" +rm -rf *A\ Document\ Being\ Saved\ By\ xcodebuild* +echo "" +echo "*********************************************************" +echo "* Using batrycrouch to update all languages *" +echo "*********************************************************" runBartycrouch - if [ -e localization.tmp ]; then rm -rf localization.tmp fi +echo "" +echo "*******************************************" +echo "* Showing results as git diff *" +echo "*******************************************" for folder in "localization/external" "shareSheet-iOS/localization/external"; do #subshell to not leak from "cd $folder" ( @@ -123,6 +162,11 @@ for folder in "localization/external" "shareSheet-iOS/localization/external"; do ) done +echo "" +echo "***************************************" +echo "* Cleaning up submodules *" +echo "***************************************" git submodule deinit --all -f git submodule update --init --recursive + exit 0