diff --git a/.github/workflows/beta.build-push.yml b/.github/workflows/beta.build-push.yml index 5a9a93f77e..3ecabc36b4 100644 --- a/.github/workflows/beta.build-push.yml +++ b/.github/workflows/beta.build-push.yml @@ -75,22 +75,22 @@ jobs: 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 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..f8560de197 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 diff --git a/Monal/Classes/AddContactMenu.swift b/Monal/Classes/AddContactMenu.swift index fc63decb89..313570b9d8 100644 --- a/Monal/Classes/AddContactMenu.swift +++ b/Monal/Classes/AddContactMenu.swift @@ -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,16 +144,16 @@ 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") { + } 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; hideLoadingOverlay(overlay) - if(success) { + 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!")) + errorAlert(title: Text("Error entering group/channel!"), message: Text((data as! NSDictionary)["errorMessage"] as! String)) } }, forMuc: jid) account.joinMuc(jid) @@ -168,14 +168,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 +196,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,9 +217,9 @@ struct AddContactMenu: View { Button(action: { showAlert = toAddEmptyAlert || toAddInvalidAlert - if(!showAlert) { + if !showAlert { let jidComponents = HelperTools.splitJid(toAdd) - if(jidComponents["host"] == nil || jidComponents["host"]!.isEmpty) { + if jidComponents["host"] == nil || jidComponents["host"]!.isEmpty { errorAlert(title: Text("Error"), message: Text("Something went wrong while parsing the string...")) showAlert = true return diff --git a/Monal/Classes/ContactDetails.swift b/Monal/Classes/ContactDetails.swift index 1f3af26879..a4fdd48f8b 100644 --- a/Monal/Classes/ContactDetails.swift +++ b/Monal/Classes/ContactDetails.swift @@ -140,15 +140,14 @@ struct ContactDetails: View { .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); -// } - + })) { + Text("Pin Chat") + } + if(contact.obj.isGroup && contact.obj.mucType == "group") { NavigationLink(destination: LazyClosureView(MemberList(mucContact:contact))) { Text("Group Members") diff --git a/Monal/Classes/CreateGroupMenu.swift b/Monal/Classes/CreateGroupMenu.swift index d88db79a82..1c99b7474c 100644 --- a/Monal/Classes/CreateGroupMenu.swift +++ b/Monal/Classes/CreateGroupMenu.swift @@ -47,14 +47,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?) } @@ -70,41 +70,46 @@ struct CreateGroupMenu: View { 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 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!"), message: Text((data as! NSDictionary)["errorMessage"] as! String)) + } + }, forMuc: roomJid) }, label: { Text("Create new group") }) } - if(self.selectedContacts.count > 0) { + if self.selectedContacts.count > 0 { Section(header: Text("Selected Group Members")) { ForEach(self.selectedContacts, id: \.obj.contactJid) { contact in ContactEntry(contact: contact) 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/LoadingOverlay.swift b/Monal/Classes/LoadingOverlay.swift index bb8f0d4f62..49450045a0 100644 --- a/Monal/Classes/LoadingOverlay.swift +++ b/Monal/Classes/LoadingOverlay.swift @@ -61,6 +61,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 +75,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 +89,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/MLCall.m b/Monal/Classes/MLCall.m index 03705f57ea..186a65c204 100644 --- a/Monal/Classes/MLCall.m +++ b/Monal/Classes/MLCall.m @@ -410,6 +410,11 @@ -(BOOL) isConnected -(void) setAudioSession:(AVAudioSession*) audioSession { @synchronized(self) { + if(audioSession == _audioSession) + { + DDLogWarn(@"Trying to activate same audio session a second time, ignoring..."); + return; + } if(audioSession != nil) MLAssert(_audioSession == nil, @"Audio session should never be activated without deactivating old audio session first!", (@{ @"oldAudioSession": nilWrapper(_audioSession), 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..174f6a4234 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; @@ -81,7 +81,15 @@ +(void) checkMimeTypeAndSizeForHistoryID:(NSNumber*) historyId request.cachePolicy = NSURLRequestReturnCacheDataElseLoad; NSURLSession* session = [NSURLSession sharedSession]; - [[session dataTaskWithRequest:request completionHandler:^(NSData* _Nullable data __unused, NSURLResponse* _Nullable response, NSError* _Nullable error __unused) { + [[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 +162,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,7 +175,7 @@ +(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; @@ -179,8 +187,8 @@ +(void) downloadFileForHistoryID:(NSNumber*) historyId andForceDownload:(BOOL) f 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 +214,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 +231,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 +239,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 +249,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 +263,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 +826,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 +883,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 +922,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 +955,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/MLIQProcessor.m b/Monal/Classes/MLIQProcessor.m index fcb64a0337..8aaa03660c 100644 --- a/Monal/Classes/MLIQProcessor.m +++ b/Monal/Classes/MLIQProcessor.m @@ -561,8 +561,8 @@ +(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"]; $$ $$class_handler(handleServerDiscoItems, $$ID(xmpp*, account), $$ID(XMPPIQ*, iqNode)) diff --git a/Monal/Classes/MLMucProcessor.h b/Monal/Classes/MLMucProcessor.h index 2e06ecf3d2..f3d82f8f65 100644 --- a/Monal/Classes/MLMucProcessor.h +++ b/Monal/Classes/MLMucProcessor.h @@ -26,7 +26,8 @@ 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) 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..1d67680c2d 100644 --- a/Monal/Classes/MLMucProcessor.m +++ b/Monal/Classes/MLMucProcessor.m @@ -191,6 +191,7 @@ -(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; } } @@ -844,17 +845,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..."); @@ -1468,14 +1485,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, 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/MLServerDetails.m b/Monal/Classes/MLServerDetails.m index 2b079b5f53..6b067c7c0a 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]; @@ -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..f67b0e4b80 100644 --- a/Monal/Classes/MLSettingsTableViewController.m +++ b/Monal/Classes/MLSettingsTableViewController.m @@ -376,7 +376,7 @@ -(void)tableView:(UITableView*) tableView didSelectRowAtIndexPath:(NSIndexPath*) 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/MLXMPPConnection.h b/Monal/Classes/MLXMPPConnection.h index b9b1878fdc..8a6dce3592 100644 --- a/Monal/Classes/MLXMPPConnection.h +++ b/Monal/Classes/MLXMPPConnection.h @@ -37,7 +37,7 @@ NS_ASSUME_NONNULL_BEGIN @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; diff --git a/Monal/Classes/MLXMPPConnection.m b/Monal/Classes/MLXMPPConnection.m index a48857fb3e..b832a4124e 100644 --- a/Monal/Classes/MLXMPPConnection.m +++ b/Monal/Classes/MLXMPPConnection.m @@ -23,6 +23,7 @@ -(id) initWithServer:(MLXMPPServer*) server andIdentity:(MLXMPPIdentity*) identi self.server = server; self.identity = identity; self.serverFeatures = [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..01247a8ead 100644 --- a/Monal/Classes/MLXMPPManager.m +++ b/Monal/Classes/MLXMPPManager.m @@ -141,6 +141,14 @@ -(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 } -(void) upgradeFloatUserSettingsToInteger:(NSString*) settingsName @@ -325,9 +333,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]; diff --git a/Monal/Classes/NotificationSettings.swift b/Monal/Classes/NotificationSettings.swift index 42e3135967..8e37883ebb 100644 --- a/Monal/Classes/NotificationSettings.swift +++ b/Monal/Classes/NotificationSettings.swift @@ -92,7 +92,7 @@ struct NotificationSettings: View { } } 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) } diff --git a/Monal/Classes/PrivacySettings.swift b/Monal/Classes/PrivacySettings.swift index a7fb6754b2..b86f41c07a 100644 --- a/Monal/Classes/PrivacySettings.swift +++ b/Monal/Classes/PrivacySettings.swift @@ -75,6 +75,9 @@ class PrivacyDefaultDB: ObservableObject { @defaultsDB("ImageUploadQuality") var imageUploadQuality : Float + + @defaultsDB("showKeyboardOnChatOpen") + var showKeyboardOnChatOpen: Bool } @@ -99,7 +102,7 @@ struct PrivacySettings: View { .resizable() .aspectRatio(contentMode: .fit) .frame(width: 20, height: 20) - Text("Publishing") + Text("Publishing & Appearance") } } NavigationLink(destination: PreviewsScreen()) { @@ -144,13 +147,18 @@ struct PrivacyScreen: View { var body: some View { Form { - Picker("Notification privacy", selection: $privacyDefaultDB.notificationPrivacySetting) { + Picker(selection: $privacyDefaultDB.notificationPrivacySetting, label: Text("Notification privacy")) { 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) + .frame(width: .infinity, height: 56, alignment: .trailing) + Toggle(isOn: $privacyDefaultDB.omemoDefaultOn) { + Text("Enable encryption by default for new chats") + } + Toggle(isOn: $privacyDefaultDB.autodeleteAllMessagesAfter3Days) { + Text("Autodelete all messages after 3 days") + } } .navigationBarTitle("Privacy & security", displayMode: .inline) } @@ -161,12 +169,27 @@ struct PublishingScreen: View { 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) + Section(header: Text("Publishing")) { + Toggle(isOn: $privacyDefaultDB.sendLastUserInteraction) { + Text("Send last interaction time") + } + Toggle(isOn: $privacyDefaultDB.sendLastChatState) { + Text("Send typing notifications") + } + Toggle(isOn: $privacyDefaultDB.sendReceivedMarkers) { + Text("Send message received state") + } + Toggle(isOn: $privacyDefaultDB.sendDisplayedMarkers) { + Text("Send message displayed state") + } + } + Section(header: Text("Appearance")) { + Toggle(isOn: $privacyDefaultDB.showKeyboardOnChatOpen) { + Text("Autofocus text input on chat open") + } + } } - .navigationBarTitle("Publishing", displayMode: .inline) + .navigationBarTitle("Publishing & Appearance", displayMode: .inline) } } @@ -175,8 +198,12 @@ struct PreviewsScreen: View { var body: some View { Form { - Toggle("Show inline geo location", isOn: $privacyDefaultDB.showGeoLocation) - Toggle("Show URL previews", isOn: $privacyDefaultDB.showURLPreview) + Toggle(isOn: $privacyDefaultDB.showGeoLocation) { + Text("Show inline geo location") + } + Toggle(isOn: $privacyDefaultDB.showURLPreview) { + Text("Show URL previews") + } } .navigationBarTitle("Previews", displayMode: .inline) } @@ -187,11 +214,21 @@ struct CommunicationScreen: View { 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) + Toggle(isOn: $privacyDefaultDB.allowNonRosterContacts) { + Text("Allow contacts not in my contact list to contact me") + } + Toggle(isOn: $privacyDefaultDB.allowVersionIQ) { + Text("Allow approved contacts to query my Monal and iOS version") + } + Toggle(isOn: $privacyDefaultDB.allowCallsFromNonRosterContacts) { + Text("Calls: Allow contacts not in my contact list to call me") + } + Toggle(isOn: $privacyDefaultDB.webrtcAllowP2P) { + Text("Calls: Allow P2P sessions") + } + Toggle(isOn: $privacyDefaultDB.webrtcUseFallbackTurn) { + Text("Calls: Allow TURN fallback to Monal-Servers") + } } .navigationBarTitle("Communication", displayMode: .inline) } @@ -203,7 +240,9 @@ struct MLAutoDownloadFiletransferSettingView: View { var body: some View { Form { Section(header: Text("General File Transfer Settings")) { - Toggle("Auto-Download Media", isOn: $privacyDefaultDB.autodownloadFiletransfers) + Toggle(isOn: $privacyDefaultDB.autodownloadFiletransfers) { + Text("Auto-Download Media") + } } Section(header: Text("Download Settings")) { @@ -211,40 +250,49 @@ struct MLAutoDownloadFiletransferSettingView: View { 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")} + 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("Load over WiFi up to: \(UInt(privacyDefaultDB.autodownloadFiletransfersWifiMaxSize/(1024*1024))) MiB") } Text("Adjust the maximum file size for auto-downloads over cellular network") .foregroundColor(.secondary) .font(.footnote) - Slider(value: $privacyDefaultDB.autodownloadFiletransfersMobileMaxSize.bytecount(mappedTo: 1024*1024), - in: 0.0...100.0, - step: 1.0, - minimumValueLabel: Text("1 MiB"), - maximumValueLabel: Text("100 MiB"), - label: {Text("Load over Cellular")} + 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") + Text("Load over cellular up to: \(Int(privacyDefaultDB.autodownloadFiletransfersMobileMaxSize/(1024*1024))) MiB") Section(header: Text("Upload Settings")) { Text("Adjust the quality of images uploaded") .foregroundColor(.secondary) .font(.footnote) - Slider(value: $privacyDefaultDB.imageUploadQuality, - in: 0.33...1.0, - step: 0.01, - minimumValueLabel: Text("33%"), - maximumValueLabel: Text("100%"), - label: {Text("Upload Settings") - }) - Text("Image Upload Quality : \(String(format: "%.0f%%", privacyDefaultDB.imageUploadQuality*100))") + 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))") } } } diff --git a/Monal/Classes/SwiftuiHelpers.swift b/Monal/Classes/SwiftuiHelpers.swift index 3e60317783..8a58e4547d 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 { @@ -483,7 +479,7 @@ class SwiftuiInterface : NSObject { 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))) diff --git a/Monal/Classes/chatViewController.m b/Monal/Classes/chatViewController.m index 1ed71c276e..d2718adaa0 100644 --- a/Monal/Classes/chatViewController.m +++ b/Monal/Classes/chatViewController.m @@ -813,10 +813,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"]) { diff --git a/Monal/Classes/xmpp.m b/Monal/Classes/xmpp.m index 55d4e3ca86..ff776e3e21 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 16 #define CONNECT_TIMEOUT 7.0 #define IQ_TIMEOUT 60.0 NSString* const kQueueID = @"queueID"; @@ -3496,8 +3495,9 @@ -(void) realPersistState 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"]; @@ -3655,7 +3655,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"]) { @@ -3929,7 +3929,7 @@ -(void) bindResource:(NSString*) resource 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 diff --git a/Monal/Monal.xcodeproj/project.pbxproj b/Monal/Monal.xcodeproj/project.pbxproj index e8969e929d..93f01047d2 100644 --- a/Monal/Monal.xcodeproj/project.pbxproj +++ b/Monal/Monal.xcodeproj/project.pbxproj @@ -2657,7 +2657,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 14.0; LLVM_LTO = YES; MACOSX_DEPLOYMENT_TARGET = 11.0; - MARKETING_VERSION = 6.3.0; + MARKETING_VERSION = 6.3.1; PRESERVE_DEAD_CODE_INITS_AND_TERMS = NO; SDKROOT = iphoneos; STRIP_INSTALLED_PRODUCT = NO; @@ -3024,7 +3024,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 14.0; LLVM_LTO = YES; MACOSX_DEPLOYMENT_TARGET = 11.0; - MARKETING_VERSION = 6.3.0; + MARKETING_VERSION = 6.3.1; PRESERVE_DEAD_CODE_INITS_AND_TERMS = NO; SDKROOT = iphoneos; STRIP_INSTALLED_PRODUCT = NO; @@ -3186,7 +3186,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 14.0; LLVM_LTO = YES; MACOSX_DEPLOYMENT_TARGET = 11.0; - MARKETING_VERSION = 6.3.0; + MARKETING_VERSION = 6.3.1; ONLY_ACTIVE_ARCH = YES; PRESERVE_DEAD_CODE_INITS_AND_TERMS = NO; RUN_CLANG_STATIC_ANALYZER = YES; @@ -3462,7 +3462,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 14.0; LLVM_LTO = YES; MACOSX_DEPLOYMENT_TARGET = 11.0; - MARKETING_VERSION = 6.3.0; + MARKETING_VERSION = 6.3.1; PRESERVE_DEAD_CODE_INITS_AND_TERMS = NO; RUN_CLANG_STATIC_ANALYZER = YES; SDKROOT = iphoneos; @@ -3815,7 +3815,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 14.0; LLVM_LTO = YES; MACOSX_DEPLOYMENT_TARGET = 11.0; - MARKETING_VERSION = 6.3.0; + MARKETING_VERSION = 6.3.1; PRESERVE_DEAD_CODE_INITS_AND_TERMS = NO; SDKROOT = iphoneos; STRIP_INSTALLED_PRODUCT = NO; @@ -4229,7 +4229,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 14.0; LLVM_LTO = NO; MACOSX_DEPLOYMENT_TARGET = 11.0; - MARKETING_VERSION = 6.3.0; + MARKETING_VERSION = 6.3.1; PRESERVE_DEAD_CODE_INITS_AND_TERMS = NO; RUN_CLANG_STATIC_ANALYZER = YES; SDKROOT = iphoneos; 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/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