From 6f17360d9045d3f40a0bf27bc946e8f4d064ef6d Mon Sep 17 00:00:00 2001 From: Matthew Fennell Date: Sun, 30 Jun 2024 12:04:51 +0100 Subject: [PATCH 01/35] Remove unused init from AddTopLevelNavigation --- Monal/Classes/SwiftuiHelpers.swift | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Monal/Classes/SwiftuiHelpers.swift b/Monal/Classes/SwiftuiHelpers.swift index bb89ea0d6c..c1c0f99f1e 100644 --- a/Monal/Classes/SwiftuiHelpers.swift +++ b/Monal/Classes/SwiftuiHelpers.swift @@ -513,10 +513,6 @@ struct AddTopLevelNavigation: View { self.build = build self.delegate = delegate } - init(withDelegate delegate: SheetDismisserProtocol, andClosure build: @escaping () -> Content) { - self.build = build - self.delegate = delegate - } var body: some View { NavigationView { build() From 1c50bef0a224366f32b9a9a28808935f1517cf41 Mon Sep 17 00:00:00 2001 From: Matthew Fennell Date: Sun, 30 Jun 2024 12:05:37 +0100 Subject: [PATCH 02/35] Remove sheet dismisser from makeOwnOmemoKeyView The SheetDismisserProtocol was not used, so there is no need for a corresponding DismissAction. --- Monal/Classes/SwiftuiHelpers.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/Monal/Classes/SwiftuiHelpers.swift b/Monal/Classes/SwiftuiHelpers.swift index c1c0f99f1e..1fda2714f2 100644 --- a/Monal/Classes/SwiftuiHelpers.swift +++ b/Monal/Classes/SwiftuiHelpers.swift @@ -615,9 +615,7 @@ class SwiftuiInterface : NSObject { @objc func makeOwnOmemoKeyView(_ ownContact: MLContact?) -> UIViewController { - let delegate = SheetDismisserProtocol() let host = UIHostingController(rootView:AnyView(EmptyView())) - delegate.host = host if(ownContact == nil) { host.rootView = AnyView(OmemoKeys(contact: nil)) } else { From 8ac87d927ea757ce8b3515a5a09232dfa7aa0c11 Mon Sep 17 00:00:00 2001 From: Matthew Fennell Date: Sun, 30 Jun 2024 12:07:39 +0100 Subject: [PATCH 03/35] Remove sheet dismisser from ContactRequestsMenu The SheetDismisserProtocol was not used, so there is no need for a corresponding DismissAction. --- Monal/Classes/ContactRequestsMenu.swift | 7 ++----- Monal/Classes/SwiftuiHelpers.swift | 2 +- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/Monal/Classes/ContactRequestsMenu.swift b/Monal/Classes/ContactRequestsMenu.swift index 040d3283af..023caeda9f 100644 --- a/Monal/Classes/ContactRequestsMenu.swift +++ b/Monal/Classes/ContactRequestsMenu.swift @@ -63,7 +63,6 @@ struct ContactRequestsMenuEntry: View { } struct ContactRequestsMenu: View { - var delegate: SheetDismisserProtocol @State private var pendingRequests: [MLContact] var body: some View { @@ -95,15 +94,13 @@ struct ContactRequestsMenu: View { } } - init(delegate: SheetDismisserProtocol) { - self.delegate = delegate + init() { self.pendingRequests = DataLayer.sharedInstance().allContactRequests() as! [MLContact] } } struct ContactRequestsMenu_Previews: PreviewProvider { - static var delegate = SheetDismisserProtocol() static var previews: some View { - ContactRequestsMenu(delegate: delegate) + ContactRequestsMenu() } } diff --git a/Monal/Classes/SwiftuiHelpers.swift b/Monal/Classes/SwiftuiHelpers.swift index 1fda2714f2..f7691d1907 100644 --- a/Monal/Classes/SwiftuiHelpers.swift +++ b/Monal/Classes/SwiftuiHelpers.swift @@ -673,7 +673,7 @@ class SwiftuiInterface : NSObject { case "LogIn": host.rootView = AnyView(UIKitWorkaround(WelcomeLogIn(delegate:delegate))) case "ContactRequests": - host.rootView = AnyView(AddTopLevelNavigation(withDelegate: delegate, to: ContactRequestsMenu(delegate: delegate))) + host.rootView = AnyView(AddTopLevelNavigation(withDelegate: delegate, to: ContactRequestsMenu())) case "CreateGroup": host.rootView = AnyView(AddTopLevelNavigation(withDelegate: delegate, to: CreateGroupMenu(delegate: delegate))) case "ChatPlaceholder": From 9419c19fbdc7777a3aa8cd33e0884f9ea017e942 Mon Sep 17 00:00:00 2001 From: Matthew Fennell Date: Sun, 30 Jun 2024 12:08:32 +0100 Subject: [PATCH 04/35] Remove sheet dismisser from AccountPicker The SheetDismisserProtocol was not used, so there is no need for a corresponding DismissAction. --- Monal/Classes/AccountPicker.swift | 7 ++----- Monal/Classes/SwiftuiHelpers.swift | 2 +- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/Monal/Classes/AccountPicker.swift b/Monal/Classes/AccountPicker.swift index 2b321cdc9c..d05b455a12 100644 --- a/Monal/Classes/AccountPicker.swift +++ b/Monal/Classes/AccountPicker.swift @@ -7,7 +7,6 @@ // struct AccountPicker: View { - let delegate: SheetDismisserProtocol let contacts: [MLContact] let callType: MLCallType #if IS_ALPHA @@ -18,8 +17,7 @@ struct AccountPicker: View { let appLogoId = "AppLogo" #endif - init(delegate:SheetDismisserProtocol, contacts:[MLContact], callType: MLCallType) { - self.delegate = delegate + init(contacts:[MLContact], callType: MLCallType) { self.contacts = contacts self.callType = callType } @@ -63,8 +61,7 @@ struct AccountPicker: View { } struct AccountPicker_Previews: PreviewProvider { - static var delegate = SheetDismisserProtocol() static var previews: some View { - AccountPicker(delegate:delegate, contacts:[MLContact.makeDummyContact(0)], callType:.audio) + AccountPicker(contacts:[MLContact.makeDummyContact(0)], callType:.audio) } } diff --git a/Monal/Classes/SwiftuiHelpers.swift b/Monal/Classes/SwiftuiHelpers.swift index f7691d1907..40b26399cc 100644 --- a/Monal/Classes/SwiftuiHelpers.swift +++ b/Monal/Classes/SwiftuiHelpers.swift @@ -582,7 +582,7 @@ class SwiftuiInterface : NSObject { let delegate = SheetDismisserProtocol() let host = UIHostingController(rootView:AnyView(EmptyView())) delegate.host = host - host.rootView = AnyView(AddTopLevelNavigation(withDelegate:delegate, to:AccountPicker(delegate:delegate, contacts:contacts, callType:MLCallType(rawValue: callType)!))) + host.rootView = AnyView(AddTopLevelNavigation(withDelegate:delegate, to:AccountPicker(contacts:contacts, callType:MLCallType(rawValue: callType)!))) return host } From 136f11fd76a878a4b2c5651500afae66134b6706 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Mon, 1 Jul 2024 20:22:39 +0200 Subject: [PATCH 05/35] Deactivate message search shortcut --- Monal/Classes/chatViewController.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Monal/Classes/chatViewController.m b/Monal/Classes/chatViewController.m index 9ee5a98b83..72bfaf8740 100644 --- a/Monal/Classes/chatViewController.m +++ b/Monal/Classes/chatViewController.m @@ -2923,7 +2923,7 @@ -(void) commandIPressed:(UIKeyCommand*)keyCommand // Open search ViewController -(void) commandFPressed:(UIKeyCommand*)keyCommand { - [self showSeachButtonAction]; + //[self showSeachButtonAction]; } // List of custom hardware key commands From 7833bc2f54572eac64a457d54ed13e4e7e101789 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Mon, 1 Jul 2024 21:07:19 +0200 Subject: [PATCH 06/35] Make sure ios/macos only changelog entries aren't pushed to apple --- .github/workflows/beta.build-push.yml | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/.github/workflows/beta.build-push.yml b/.github/workflows/beta.build-push.yml index 7845a7abf2..c95b89b666 100644 --- a/.github/workflows/beta.build-push.yml +++ b/.github/workflows/beta.build-push.yml @@ -59,16 +59,27 @@ jobs: run: xcrun altool --validate-app --file ./Monal/build/ipa/Monal.ipa --type ios --asc-provider S8D843U34Y -u "$(cat /Users/ci/apple_connect_upload_mail.txt)" -p "$(cat /Users/ci/apple_connect_upload_secret.txt)" - name: Push beta tag to repo 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') git push origin Build_iOS_$buildNumber - 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') + echo "tag=Build_iOS_$buildNumber" | tee /dev/stderr >> "$GITHUB_OUTPUT" + echo "name=Monal Beta $(git log -n 1 --merges --pretty=format:%s | sed -E 's/^[\t\n ]*([^\n\t ]+)[\t\n ]+\(([^\n\t ]+)\)[\t\n ]*$/\1 (Build '$buildNumber', PR \2)/g')" | tee /dev/stderr >> "$GITHUB_OUTPUT" + echo "notes<<__EOF__" | tee /dev/stderr >> "$GITHUB_OUTPUT" - echo "$(git log -n 1 --merges --pretty=format:%b)" | tee /dev/stderr >> "$GITHUB_OUTPUT" + echo "$(git log -n 1 --merges --pretty=format:%b)" | sed -E 's/^[\t\n ]*IOS_ONLY[\t\n ]*(.*)$/\1' | sed -E 's/^[\t\n ]*MACOS_ONLY[\t\n ]*(.*)$/\1' | tee /dev/stderr >> "$GITHUB_OUTPUT" + echo "__EOF__" | tee /dev/stderr >> "$GITHUB_OUTPUT" + + echo "notes_ios<<__EOF__" | tee /dev/stderr >> "$GITHUB_OUTPUT" + echo "$(git log -n 1 --merges --pretty=format:%b)" | grep -v '^[\t\n ]*MACOS_ONLY.*$' | sed -E 's/^[\t\n ]*IOS_ONLY[\t\n ]*(.*)$/\1' | tee /dev/stderr >> "$GITHUB_OUTPUT" + echo "__EOF__" | tee /dev/stderr >> "$GITHUB_OUTPUT" + + echo "notes_macos<<__EOF__" | tee /dev/stderr >> "$GITHUB_OUTPUT" + echo "$(git log -n 1 --merges --pretty=format:%b)" | grep -v '^[\t\n ]*IOS_ONLY.*$' | sed -E 's/^[\t\n ]*MACOS_ONLY[\t\n ]*(.*)$/\1' | tee /dev/stderr >> "$GITHUB_OUTPUT" echo "__EOF__" | tee /dev/stderr >> "$GITHUB_OUTPUT" - name: Publish ios to appstore connect #run: xcrun altool --upload-app -f ./Monal/build/ipa/Monal.ipa --type ios --asc-provider S8D843U34Y --team-id S8D843U34Y -u "$(cat /Users/ci/apple_connect_upload_mail.txt)" -p "$(cat /Users/ci/apple_connect_upload_secret.txt)" From 98de6faffe540be3ba0b1fa9f8e5a48017f9664d Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Thu, 4 Jul 2024 03:04:24 +0200 Subject: [PATCH 07/35] Use fastlane for stable releases, too This uses our new mail2webhook python script installed on the mailserver to trigger a workflow posting release information once the iOS build reaches the appstore. --- .github/workflows/beta.build-push.yml | 40 ++++---- .github/workflows/publish-stable-release.yml | 96 ++++++++++++++++++++ .github/workflows/stable.build-push.yml | 70 +++++++++++--- scripts/mail2webhook.py | 78 ++++++++++++++++ 4 files changed, 254 insertions(+), 30 deletions(-) create mode 100644 .github/workflows/publish-stable-release.yml create mode 100644 scripts/mail2webhook.py diff --git a/.github/workflows/beta.build-push.yml b/.github/workflows/beta.build-push.yml index c95b89b666..da2feffead 100644 --- a/.github/workflows/beta.build-push.yml +++ b/.github/workflows/beta.build-push.yml @@ -18,7 +18,7 @@ jobs: outputs: release-tag: ${{ steps.releasenotes.outputs.tag }} release-name: ${{ steps.releasenotes.outputs.name }} - release-changelog: ${{ steps.releasenotes.outputs.notes }} + release-notes: ${{ steps.releasenotes.outputs.notes }} env: APP_NAME: "Monal" APP_DIR: "Monal.app" @@ -65,26 +65,34 @@ jobs: id: releasenotes run: | buildNumber=$(git tag --sort="v:refname" | grep "Build_iOS" | tail -n1 | sed 's/Build_iOS_//g') + mkdir -p /Users/ci/releases + OUTPUT_FILE="/Users/ci/releases/$buildNumber.output" + touch "$OUTPUT_FILE" + echo "OUTPUT_FILE=$OUTPUT_FILE" | tee /dev/stderr >> "$GITHUB_OUTPUT" - echo "tag=Build_iOS_$buildNumber" | tee /dev/stderr >> "$GITHUB_OUTPUT" + echo "buildNumber=$buildNumber" | tee /dev/stderr >> "$OUTPUT_FILE" + echo "tag=Build_iOS_$buildNumber" | tee /dev/stderr >> "$OUTPUT_FILE" + echo "version=$(git log -n 1 --merges --pretty=format:%s | sed -E 's/^[\t\n ]*([^\n\t ]+)[\t\n ]+\(([^\n\t ]+)\)[\t\n ]*$/\1/g')" | tee /dev/stderr >> "$OUTPUT_FILE" - echo "name=Monal Beta $(git log -n 1 --merges --pretty=format:%s | sed -E 's/^[\t\n ]*([^\n\t ]+)[\t\n ]+\(([^\n\t ]+)\)[\t\n ]*$/\1 (Build '$buildNumber', PR \2)/g')" | tee /dev/stderr >> "$GITHUB_OUTPUT" + echo "name=Monal Beta $(git log -n 1 --merges --pretty=format:%s | sed -E 's/^[\t\n ]*([^\n\t ]+)[\t\n ]+\(([^\n\t ]+)\)[\t\n ]*$/\1 (Build '$buildNumber', PR \2)/g')" | tee /dev/stderr >> "$OUTPUT_FILE" - echo "notes<<__EOF__" | tee /dev/stderr >> "$GITHUB_OUTPUT" - echo "$(git log -n 1 --merges --pretty=format:%b)" | sed -E 's/^[\t\n ]*IOS_ONLY[\t\n ]*(.*)$/\1' | sed -E 's/^[\t\n ]*MACOS_ONLY[\t\n ]*(.*)$/\1' | tee /dev/stderr >> "$GITHUB_OUTPUT" - echo "__EOF__" | tee /dev/stderr >> "$GITHUB_OUTPUT" + echo "notes<<__EOF__" | tee /dev/stderr >> "$OUTPUT_FILE" + echo "$(git log -n 1 --merges --pretty=format:%b)" | sed -E 's/^[\t\n ]*IOS_ONLY[\t\n ]*(.*)$/\1' | sed -E 's/^[\t\n ]*MACOS_ONLY[\t\n ]*(.*)$/\1' | tee /dev/stderr >> "$OUTPUT_FILE" + echo "__EOF__" | tee /dev/stderr >> "$OUTPUT_FILE" - echo "notes_ios<<__EOF__" | tee /dev/stderr >> "$GITHUB_OUTPUT" - echo "$(git log -n 1 --merges --pretty=format:%b)" | grep -v '^[\t\n ]*MACOS_ONLY.*$' | sed -E 's/^[\t\n ]*IOS_ONLY[\t\n ]*(.*)$/\1' | tee /dev/stderr >> "$GITHUB_OUTPUT" - echo "__EOF__" | tee /dev/stderr >> "$GITHUB_OUTPUT" + echo "notes_ios<<__EOF__" | tee /dev/stderr >> "$OUTPUT_FILE" + echo "$(git log -n 1 --merges --pretty=format:%b)" | grep -v '^[\t\n ]*MACOS_ONLY.*$' | sed -E 's/^[\t\n ]*IOS_ONLY[\t\n ]*(.*)$/\1' | tee /dev/stderr >> "$OUTPUT_FILE" + echo "__EOF__" | tee /dev/stderr >> "$OUTPUT_FILE" - echo "notes_macos<<__EOF__" | tee /dev/stderr >> "$GITHUB_OUTPUT" - echo "$(git log -n 1 --merges --pretty=format:%b)" | grep -v '^[\t\n ]*IOS_ONLY.*$' | sed -E 's/^[\t\n ]*MACOS_ONLY[\t\n ]*(.*)$/\1' | tee /dev/stderr >> "$GITHUB_OUTPUT" - echo "__EOF__" | tee /dev/stderr >> "$GITHUB_OUTPUT" + echo "notes_macos<<__EOF__" | tee /dev/stderr >> "$OUTPUT_FILE" + echo "$(git log -n 1 --merges --pretty=format:%b)" | grep -v '^[\t\n ]*IOS_ONLY.*$' | sed -E 's/^[\t\n ]*MACOS_ONLY[\t\n ]*(.*)$/\1' | tee /dev/stderr >> "$OUTPUT_FILE" + echo "__EOF__" | tee /dev/stderr >> "$OUTPUT_FILE" + + cat "$OUTPUT_FILE" >> "$GITHUB_OUTPUT" - name: Publish ios to appstore connect #run: xcrun altool --upload-app -f ./Monal/build/ipa/Monal.ipa --type ios --asc-provider S8D843U34Y --team-id S8D843U34Y -u "$(cat /Users/ci/apple_connect_upload_mail.txt)" -p "$(cat /Users/ci/apple_connect_upload_secret.txt)" env: - PILOT_CHANGELOG: ${{ steps.releasenotes.outputs.notes }} + PILOT_CHANGELOG: ${{ steps.releasenotes.outputs.notes_ios }} run: | fastlane run upload_to_testflight api_key_path:"/Users/ci/appstoreconnect/key.json" team_id:"S8D843U34Y" ipa:"./Monal/build/ipa/Monal.ipa" distribute_external:true groups:"Internal Pre-Beta Testers","Public Beta" reject_build_waiting_for_review:true submit_beta_review:true - name: Notarize catalyst @@ -101,7 +109,7 @@ jobs: - 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 env: - PILOT_CHANGELOG: ${{ steps.releasenotes.outputs.notes }} + PILOT_CHANGELOG: ${{ steps.releasenotes.outputs.notes_macos }} run: | fastlane run upload_to_testflight api_key_path:"/Users/ci/appstoreconnect/key.json" team_id:"S8D843U34Y" pkg:"./Monal/build/app/Monal.pkg" distribute_external:true groups:"Internal Pre-Beta Testers","Public Beta" reject_build_waiting_for_review:true submit_beta_review:true - uses: actions/upload-artifact@v4 @@ -183,5 +191,5 @@ jobs: recipient_is_room: true bot_alias: "Monal Release Bot" message: | - New Betarelease: ${{ needs.buildAndPublishBeta.outputs.release-name }} - ${{ needs.buildAndPublishBeta.outputs.release-changelog }} + ${{ needs.buildAndPublishBeta.outputs.release-name }} was released + ${{ needs.buildAndPublishBeta.outputs.release-notes }} diff --git a/.github/workflows/publish-stable-release.yml b/.github/workflows/publish-stable-release.yml new file mode 100644 index 0000000000..c05348d385 --- /dev/null +++ b/.github/workflows/publish-stable-release.yml @@ -0,0 +1,96 @@ +name: Publish release +on: + repository_dispatch: + types: [distribution] +jobs: + extractChangelog: + runs-on: self-hosted + outputs: + release-buildNumber: ${{ steps.releasenotes.outputs.buildNumber }} + release-tag: ${{ steps.releasenotes.outputs.tag }} + release-version: ${{ steps.releasenotes.outputs.version }} + release-name: ${{ steps.releasenotes.outputs.name }} + release-notes: ${{ steps.releasenotes.outputs.notes }} + release-notes_ios: ${{ steps.releasenotes.outputs.notes_ios }} + release-notes_macos: ${{ steps.releasenotes.outputs.notes_macos }} + # create release only if the ios app made it to the appstore and ignore the macos appstore state + if: github.event.client_payload.Platform == 'iOS' + steps: + # - run: | + # echo ${{ github.event.client_payload.AppName }} + # echo ${{ github.event.client_payload.Platform }} + # echo ${{ github.event.client_payload.AppVersionNumber }} + - name: Load release info + id: releasenotes + run: | + buildNumber="$(fastlane run app_store_build_number api_key_path:"/Users/ci/appstoreconnect/key.json" team_id:"S8D843U34Y" app_identifier:"G7YU7X7KRJ.SworIM" live:false version:"${{ github.event.client_payload.AppVersionNumber }}" 2>&1 | tee /dev/stderr | grep Result | sed -E 's/^.*Result: ([0-9]+).*$/\1/g')" + mkdir -p /Users/ci/releases + OUTPUT_FILE="/Users/ci/releases/$buildNumber.output" + touch "$OUTPUT_FILE" + cat "$OUTPUT_FILE" >> "$GITHUB_OUTPUT" + + promoteDraftRelease: + name: Promote draft release to live release + runs-on: ubuntu-latest + needs: [extractChangelog] + steps: + - name: Promote draft release to live release + run: | + echo "ID: ${{ steps.releasenotes.outputs.releaseID }}" + curl -L \ + -X PATCH \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "https://github.com/gitapi/repos/${{ github.repository }}/releases/${{ steps.draftrelease.outputs.id }}" \ + -d '{"draft": false, "prerelease": false, "make_latest": true}' + + notifyMuc: + name: Notify support MUC about new stable release + runs-on: ubuntu-latest + needs: [extractChangelog] + steps: + - name: Notify support MUC + uses: monal-im/xmpp-notifier@master + with: # Set the secrets as inputs + jid: ${{ secrets.BOT_JID }} + password: ${{ secrets.BOT_PASSWORD }} + server_host: ${{ secrets.BOT_SERVER }} + recipient: monal@chat.yax.im + recipient_is_room: true + bot_alias: "Monal Release Bot" + message: | + ${{ needs.extractChangelog.outputs.release-name }} was released: + ${{ needs.extractChangelog.outputs.release-notes }} + + notifyMastodon: + name: Post release info on mastodon + runs-on: ubuntu-latest + needs: [extractChangelog] + steps: + - name: Patch changelog length + id: changelog + env: + NOTES: ${{ needs.extractChangelog.outputs.release-notes }} + run: | + if [ "${#NOTES}" -gt 400 ]; then + NOTES="To see the complete list of bugfixes and improvements, check our releases page: https://github.com/monal-im/Monal/releases/tag/${{ needs.extractChangelog.outputs.release-tag }}" + fi + echo "notes<<__EOF__" | tee /dev/stderr >> "$GITHUB_OUTPUT" + echo "$NOTES" >> "$GITHUB_OUTPUT" + echo "__EOF__" | tee /dev/stderr >> "$GITHUB_OUTPUT" + - name: Post release info on mastodon + id: toot + uses: cbrgm/mastodon-github-action@v2.1.3 + with: + access-token: ${{ secrets.MASTODON_ACCESS_TOKEN }} + url: ${{ secrets.MASTODON_URL }} + + message: "${{ needs.extractChangelog.outputs.release-name }} released.\n\n${{ steps.changelog.outputs.notes }}\n\n#Monal #ios #macos #xmpp #im #chat #messaging" + visibility: "public" + language: "en" + - name: Get toot information + run: | + echo "Toot ID: ${{ steps.toot.outputs.id }}" + echo "Toot URL: ${{ steps.toot.outputs.url }}" + echo "Scheduled at: ${{ steps.toot.outputs.scheduled_at }}" diff --git a/.github/workflows/stable.build-push.yml b/.github/workflows/stable.build-push.yml index b3fae8ea6f..c28ba1143c 100644 --- a/.github/workflows/stable.build-push.yml +++ b/.github/workflows/stable.build-push.yml @@ -58,8 +58,48 @@ jobs: run: | buildNumber=$(git tag --sort="v:refname" |grep "Build_iOS" | tail -n1 | sed 's/Build_iOS_//g') git push origin Build_iOS_$buildNumber + - 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') + mkdir -p /Users/ci/releases + OUTPUT_FILE="/Users/ci/releases/$buildNumber.output" + touch "$OUTPUT_FILE" + echo "OUTPUT_FILE=$OUTPUT_FILE" | tee /dev/stderr >> "$GITHUB_OUTPUT" + + echo "buildNumber=$buildNumber" | tee /dev/stderr >> "$OUTPUT_FILE" + echo "tag=Build_iOS_$buildNumber" | tee /dev/stderr >> "$OUTPUT_FILE" + echo "version=$(git log -n 1 --merges --pretty=format:%s | sed -E 's/^[\t\n ]*([^\n\t ]+)[\t\n ]+\(([^\n\t ]+)\)[\t\n ]*$/\1/g')" | tee /dev/stderr >> "$OUTPUT_FILE" + + echo "name=Monal $(git log -n 1 --merges --pretty=format:%s | sed -E 's/^[\t\n ]*([^\n\t ]+)[\t\n ]+\(([^\n\t ]+)\)[\t\n ]*$/\1 (Build '$buildNumber', PR \2)/g')" | tee /dev/stderr >> "$OUTPUT_FILE" + + echo "notes<<__EOF__" | tee /dev/stderr >> "$OUTPUT_FILE" + echo "$(git log -n 1 --merges --pretty=format:%b)" | sed -E 's/^[\t\n ]*IOS_ONLY[\t\n ]*(.*)$/\1' | sed -E 's/^[\t\n ]*MACOS_ONLY[\t\n ]*(.*)$/\1' | tee /dev/stderr >> "$OUTPUT_FILE" + echo "__EOF__" | tee /dev/stderr >> "$OUTPUT_FILE" + + echo "notes_ios<<__EOF__" | tee /dev/stderr >> "$OUTPUT_FILE" + echo "$(git log -n 1 --merges --pretty=format:%b)" | grep -v '^[\t\n ]*MACOS_ONLY.*$' | sed -E 's/^[\t\n ]*IOS_ONLY[\t\n ]*(.*)$/\1' | tee /dev/stderr >> "$OUTPUT_FILE" + echo "__EOF__" | tee /dev/stderr >> "$OUTPUT_FILE" + + echo "notes_macos<<__EOF__" | tee /dev/stderr >> "$OUTPUT_FILE" + echo "$(git log -n 1 --merges --pretty=format:%b)" | grep -v '^[\t\n ]*IOS_ONLY.*$' | sed -E 's/^[\t\n ]*MACOS_ONLY[\t\n ]*(.*)$/\1' | tee /dev/stderr >> "$OUTPUT_FILE" + echo "__EOF__" | tee /dev/stderr >> "$OUTPUT_FILE" + + cat "$OUTPUT_FILE" >> "$GITHUB_OUTPUT" + - name: Create fastlane metadata directory + id: metadata + env: + CHANGELOG: ${{ steps.releasenotes.outputs.notes_ios }} + run: | + path="$(mktemp -d)" + echo -n "$CHANGELOG" > "$path/release_notes.txt" + echo "path=$path" | tee /dev/stderr >> "$GITHUB_OUTPUT" - name: Publish ios to appstore connect - run: xcrun altool --upload-app --file ./Monal/build/ipa/Monal.ipa --type ios --asc-provider S8D843U34Y --team-id S8D843U34Y -u $(cat /Users/ci/apple_connect_upload_mail.txt) -p "$(cat /Users/ci/apple_connect_upload_secret.txt)" + #run: xcrun altool --upload-app --file ./Monal/build/ipa/Monal.ipa --type ios --asc-provider S8D843U34Y --team-id S8D843U34Y -u $(cat /Users/ci/apple_connect_upload_mail.txt) -p "$(cat /Users/ci/apple_connect_upload_secret.txt)" + env: + DELIVER_METADATA_PATH: ${{ steps.metadata.outputs.path }} + run: | + fastlane run upload_to_app_store api_key_path:"/Users/ci/appstoreconnect/key.json" team_id:"S8D843U34Y" ipa:"./Monal/build/ipa/Monal.ipa" app_version:"${{ steps.releasenotes.outputs.version }}" reject_if_possible:true submit_for_review:true automatic_release:true skip_metadata: true skip_screenshots: true - name: Notarize catalyst run: xcrun notarytool submit ./Monal/build/app/Monal.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: staple @@ -69,12 +109,19 @@ jobs: stapler validate "$APP_DIR" /usr/bin/ditto -c -k --sequesterRsrc --keepParent "$APP_DIR" "../$APP_NAME.zip" cd ../../../.. - - name: upload new catalyst stable to monal-im.org + - name: Upload new catalyst stable to monal-im.org run: ./scripts/uploadNonAlpha.sh stable - 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 maccatalyst.G7YU7X7KRJ.SworIM + #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 maccatalyst.G7YU7X7KRJ.SworIM + env: + DELIVER_METADATA_PATH: ${{ steps.metadata.outputs.path }} + run: | + fastlane run upload_to_app_store api_key_path:"/Users/ci/appstoreconnect/key.json" team_id:"S8D843U34Y" pkg:"./Monal/build/app/Monal.pkg" app_version:"${{ steps.releasenotes.outputs.version }}" reject_if_possible:true submit_for_review:true automatic_release:true skip_metadata: true skip_screenshots: true # - name: Update xmpp.org client list with new timestamp # run: ./scripts/push_xmpp.org.sh + - name: Remove fastlane metadata directory + run: | + rm -rf "${{ steps.metadata.outputs.path }}" - uses: actions/upload-artifact@v4 with: name: monal-catalyst-zip @@ -100,16 +147,8 @@ jobs: # name: monal-ios-dsym # path: Monal/build/ios_Monal.xcarchive/dSYMs # if-no-files-found: error - - 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') - echo "tag=Build_iOS_$buildNumber" | tee /dev/stderr >> "$GITHUB_OUTPUT" - echo "name=Monal $(git log -n 1 --merges --pretty=format:%s | sed -E 's/^[\t\n ]*([^\n\t ]+)[\t\n ]+\(([^\n\t ]+)\)[\t\n ]*$/\1 (Build '$buildNumber', PR \2)/g')" | tee /dev/stderr >> "$GITHUB_OUTPUT" - echo "notes<<__EOF__" | tee /dev/stderr >> "$GITHUB_OUTPUT" - echo "$(git log -n 1 --merges --pretty=format:%b)" | tee /dev/stderr >> "$GITHUB_OUTPUT" - echo "__EOF__" | tee /dev/stderr >> "$GITHUB_OUTPUT" - - name: Release + - name: Create Draft Release + id: draftrelease uses: softprops/action-gh-release@v2 with: name: "${{ steps.releasenotes.outputs.name }}" @@ -123,5 +162,8 @@ jobs: ./Monal/build/app/Monal.zip fail_on_unmatched_files: true token: ${{ secrets.GITHUB_TOKEN }} - draft: false prerelease: false + draft: true + - name: Write draft release id to build env + run: | + echo "releaseID=${{ steps.draftrelease.outputs.id }}" | tee /dev/stderr >> "${{ steps.releasenotes.outputs.OUTPUT_FILE }}" diff --git a/scripts/mail2webhook.py b/scripts/mail2webhook.py new file mode 100644 index 0000000000..69c984a589 --- /dev/null +++ b/scripts/mail2webhook.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +import sys +import argparse +import email +import email.parser +import re +import requests + +# see https://stackoverflow.com/a/60978847/3528174 +def to_camel_case(text): + s = text.replace("-", " ").replace("_", " ") + s = s.split() + if len(text) == 0: + return text + return s[0] + ''.join(i.capitalize() for i in s[1:]) + +# parse commandline +parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter, description="Simple python script to trigger a github ") +parser.add_argument("--token", metavar='TOKEN', required=True, help="Github token to use to authenticate the workflow trigger workflow") +parser.add_argument("--repo", metavar='REPO', required=True, help="Github user/organisation and repository name to trigger the workflow in (Example: 'monal-im/Monal')") +parser.add_argument("--type", metavar='TYPE', required=True, help="Event type to trigger the github workflow with") +parser.add_argument("--filter", metavar='FILTER', default=[], action='append', required=False, help="'key=value-regex' pairs that should be used to filter the app properties given in the mail body") +args = parser.parse_args() + + +parser = email.parser.BytesParser() +message = parser.parse(sys.stdin.buffer) + +subject = message["subject"] +date = message["date"] + +# python > 3.9 variant +#body = message.get_body(preferencelist=("plain",)) + +# python <= 3.9 variant +# see https://stackoverflow.com/a/32840516/3528174 +body = "" +if message.is_multipart(): + for part in message.walk(): + ctype = part.get_content_type() + cdispo = str(part.get('Content-Disposition')) + if ctype == 'text/plain' and 'attachment' not in cdispo: + body = part.get_payload(decode=True) # decode + break +else: + body = message.get_payload(decode=True) + +# transform body in an array of stripped strings +body = [s.strip() for s in str(body, 'UTF-8').split("\n")] + +# parse app properties +properties = {to_camel_case(k.strip()): v.strip() for k, v in [line.split(": ", 1) for line in body if len(line.split(": ", 1)) > 1]} + +# sanity checks +if "The following app has been approved for distribution:" not in body: + print("Wrong state mentioned in mail", file=sys.stderr) + sys.exit(0) +for entry in args.filter: + k, v = entry.split("=", 1) + if k not in properties: + print(f"Unknown filter key: '{k}'", file=sys.stderr) + sys.exit(0) + if re.search(v, properties[k]) == None: + print(f"Wrong {k}: '{properties[k]}'", file=sys.stderr) + sys.exit(0) + +# trigger workflow +with requests.post(f"https://github.com/gitapi/repos/{args.repo}/dispatches", json={ + "event_type": args.type, + "client_payload": properties, +}, headers={ + "Accept": "application/vnd.github+json", + "X-GitHub-Api-Version": "2022-11-28", + "Authorization": f"Bearer {args.token}" +}) as r: + r.raise_for_status() + +sys.exit(0) From b979485caa881af4131abe0d5db2df28b1ad2683 Mon Sep 17 00:00:00 2001 From: Friedrich Altheide <11352905+FriedrichAltheide@users.noreply.github.com> Date: Sat, 6 Jul 2024 06:25:58 +0200 Subject: [PATCH 08/35] rust: fix build --- rust/sdp-to-jingle/src/xep_0167.rs | 11 ++++++----- rust/sdp-to-jingle/src/xep_0176.rs | 6 ------ 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/rust/sdp-to-jingle/src/xep_0167.rs b/rust/sdp-to-jingle/src/xep_0167.rs index 7ee95a8550..9454833749 100644 --- a/rust/sdp-to-jingle/src/xep_0167.rs +++ b/rust/sdp-to-jingle/src/xep_0167.rs @@ -285,10 +285,10 @@ impl JingleRtpSessionsPayloadType { //TODO: implement this for quickxml deserialization, too! if any::type_name::() == any::type_name::() { match param.value.to_lowercase().as_str() { - "false" => value = "false".to_string().clone(), - "0" => value = "false".to_string().clone(), - "true" => value = "true".to_string().clone(), - "1" => value = "true".to_string().clone(), + "false" => value.clone_from(&"false".to_string()), + "0" => value.clone_from(&"false".to_string()), + "true" => value.clone_from(&"true".to_string()), + "1" => value.clone_from(&"true".to_string()), _ => { panic!("unallowed truth value: {}", value) } @@ -663,7 +663,7 @@ impl JingleRtpSessions { SdpAttribute::MaxMessageSize(_) => {} SdpAttribute::MaxPtime(_) => {} SdpAttribute::Mid(name) => { - content.name = name.clone(); + content.name.clone_from(name); } SdpAttribute::Msid(_) => {} SdpAttribute::MsidSemantic(_) => {} @@ -736,6 +736,7 @@ impl JingleRtpSessions { SdpAttribute::SsrcGroup(semantics, ssrcs) => { jingle.add_ssrc_group(JingleSsrcGroup::new_from_sdp(semantics, ssrcs)); } + SdpAttribute::FrameRate(_) => {} } } if fingerprint.is_set() { diff --git a/rust/sdp-to-jingle/src/xep_0176.rs b/rust/sdp-to-jingle/src/xep_0176.rs index d868aa3d9f..8c0699cac5 100644 --- a/rust/sdp-to-jingle/src/xep_0176.rs +++ b/rust/sdp-to-jingle/src/xep_0176.rs @@ -174,12 +174,6 @@ impl JingleTransportCandidate { protocol: match candidate.transport { SdpAttributeCandidateTransport::Udp => "udp".to_string(), 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!" - .to_string(), - )); - } }, raddr: candidate.raddr.as_ref().map(|addr| format!("{}", addr)), rport: candidate.rport, From f47132e650cf49e28ae8b26a43e0b04fdc54b672 Mon Sep 17 00:00:00 2001 From: Friedrich Altheide <11352905+FriedrichAltheide@users.noreply.github.com> Date: Sat, 6 Jul 2024 06:27:56 +0200 Subject: [PATCH 09/35] bump crates --- rust/Cargo.lock | 125 ++++++++++++++++++---------------- rust/sdp-to-jingle/Cargo.toml | 2 +- 2 files changed, 67 insertions(+), 60 deletions(-) diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 07067bcb68..84a8ba227c 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "bitflags" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" [[package]] name = "cfg-if" @@ -16,9 +16,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "errno" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" dependencies = [ "libc", "windows-sys", @@ -26,9 +26,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.0.2" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984" +checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" [[package]] name = "form_urlencoded" @@ -51,27 +51,27 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.153" +version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] name = "linux-raw-sys" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "log" -version = "0.4.21" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "memchr" -version = "2.7.2" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "monal-panic-handler" @@ -95,18 +95,18 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "proc-macro2" -version = "1.0.79" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" dependencies = [ "unicode-ident", ] [[package]] name = "quick-xml" -version = "0.31.0" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33" +checksum = "86e446ed58cef1bbfe847bc2fda0e2e4ea9f0e57b90c507d4781292590d72a4e" dependencies = [ "memchr", "serde", @@ -114,18 +114,18 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.35" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] [[package]] name = "rustix" -version = "0.38.32" +version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" dependencies = [ "bitflags", "errno", @@ -146,29 +146,29 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.197" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" +checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.197" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" +checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.68", ] [[package]] name = "swift-bridge" -version = "0.1.53" +version = "0.1.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72088d7882bd9c900d194cbc6008222c876450f68ce97212ac764775307bfd74" +checksum = "6180c668892926e0bc19d75a81b0ee2fdce3ab15ff062a61b3ce9b4d562eac1b" dependencies = [ "swift-bridge-build", "swift-bridge-macro", @@ -176,9 +176,9 @@ dependencies = [ [[package]] name = "swift-bridge-build" -version = "0.1.53" +version = "0.1.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0216c84c63a11fb704946f9c4843c9fad28aaf2431cbbd674a37d86d71f2100" +checksum = "7b8256d2d8c35795afeab117528f5e42b2706ca29b20f768929d458c7f245fdd" dependencies = [ "proc-macro2", "swift-bridge-ir", @@ -188,9 +188,9 @@ dependencies = [ [[package]] name = "swift-bridge-ir" -version = "0.1.53" +version = "0.1.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "183036306714fcb1a53192dd80b89694eef24389b034f3392109b3447006550f" +checksum = "a28407ee88b57fac3e8c9314a0eefb1f63a3743cb0beef4b8d93189d5d8ce0f1" dependencies = [ "proc-macro2", "quote", @@ -199,9 +199,9 @@ dependencies = [ [[package]] name = "swift-bridge-macro" -version = "0.1.53" +version = "0.1.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89560c6f6a3b65ec983c6fca5eb9d5e4c839ff41d8162c24339e258a20bf04a6" +checksum = "e69ec9898b591cfcf473a584e98b54517400dcc67b0d3f8fdf2a099ce7971e3a" dependencies = [ "proc-macro2", "quote", @@ -222,9 +222,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.55" +version = "2.0.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "002a1b3dbf967edfafc32655d0f377ab0bb7b994aa1d32c8cc7e9b8bf3ebb8f0" +checksum = "901fa70d88b9d6c98022e23b4136f9f3e54e4662c3bc1bd1d84a42a9a0f0c1e9" dependencies = [ "proc-macro2", "quote", @@ -245,9 +245,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +checksum = "ce6b6a2fb3a985e99cebfaefa9faa3024743da73304ca1c683a36429613d3d22" dependencies = [ "tinyvec_macros", ] @@ -281,9 +281,9 @@ dependencies = [ [[package]] name = "url" -version = "2.5.0" +version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" dependencies = [ "form_urlencoded", "idna", @@ -292,9 +292,9 @@ dependencies = [ [[package]] name = "webrtc-sdp" -version = "0.3.11" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7351fba122c7f6566779efdef49d2213e842f69fa1c654eef1fd9301f425064" +checksum = "9f4994ae6a67e7ed5bfeebc87b10a0bd67da5e5dbfb68db8cafbc9c9ab784dcd" dependencies = [ "log", "serde", @@ -313,13 +313,14 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.4" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ "windows_aarch64_gnullvm", "windows_aarch64_msvc", "windows_i686_gnu", + "windows_i686_gnullvm", "windows_i686_msvc", "windows_x86_64_gnu", "windows_x86_64_gnullvm", @@ -328,42 +329,48 @@ dependencies = [ [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.4" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" -version = "0.52.4" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" -version = "0.52.4" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" -version = "0.52.4" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" -version = "0.52.4" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.4" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" -version = "0.52.4" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" diff --git a/rust/sdp-to-jingle/Cargo.toml b/rust/sdp-to-jingle/Cargo.toml index 8f304300a0..8ad2bef786 100644 --- a/rust/sdp-to-jingle/Cargo.toml +++ b/rust/sdp-to-jingle/Cargo.toml @@ -11,6 +11,6 @@ crate-type = ["staticlib", "lib"] [dependencies] serde = {version = "1.0"} serde_derive = {version = "1.0"} -quick-xml = { version = "0.31.0", features = ["serialize", "overlapped-lists"] } +quick-xml = { version = "0.35.0", features = ["serialize", "overlapped-lists"] } webrtc-sdp = {version = "0.3.10", features = ["serialize"] } From c8a7e7ecb9abb5f26c5511cc310c1aa308f06790 Mon Sep 17 00:00:00 2001 From: thevaidik Date: Fri, 5 Jul 2024 18:48:35 +0530 Subject: [PATCH 10/35] Onboarding implementation --- Monal/Classes/ActiveChatsViewController.h | 1 + Monal/Classes/ActiveChatsViewController.m | 17 ++ Monal/Classes/BoardingCards.swift | 185 ++++++++++++++++++++++ Monal/Classes/GeneralSettings.swift | 32 ++-- Monal/Classes/SwiftuiHelpers.swift | 3 + Monal/Monal.xcodeproj/project.pbxproj | 4 + 6 files changed, 231 insertions(+), 11 deletions(-) create mode 100644 Monal/Classes/BoardingCards.swift diff --git a/Monal/Classes/ActiveChatsViewController.h b/Monal/Classes/ActiveChatsViewController.h index ef345377a9..ebe5e61965 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) showOnboarding; -(void) showNotificationSettings; -(void) showDetails; -(void) showRegisterWithUsername:(NSString*) username onHost:(NSString*) host withToken:(NSString* _Nullable) token usingCompletion:(monal_id_block_t _Nullable) callback; diff --git a/Monal/Classes/ActiveChatsViewController.m b/Monal/Classes/ActiveChatsViewController.m index c09d3861bb..e966c1911c 100755 --- a/Monal/Classes/ActiveChatsViewController.m +++ b/Monal/Classes/ActiveChatsViewController.m @@ -426,6 +426,13 @@ -(void) segueToIntroScreensIfNeeded [self presentViewController:passwordMigration animated:YES completion:^{}]; return; } + + if(![[HelperTools defaultsDB] boolForKey:@"hasCompletedOnboarding"]) + { + [self showOnboarding]; + return; + } + // display quick start if the user never seen it or if there are 0 enabled accounts if([[DataLayer sharedInstance] enabledAccountCnts].intValue == 0) { @@ -433,6 +440,7 @@ -(void) segueToIntroScreensIfNeeded [self presentViewController:loginViewController animated:YES completion:^{}]; return; } + if(![[HelperTools defaultsDB] boolForKey:@"HasSeenPrivacySettings"]) { [self showPrivacySettings]; @@ -520,6 +528,15 @@ -(void) showPrivacySettings [self presentViewController:view animated:YES completion:^{}]; } +-(void) showOnboarding +{ + [self dismissCompleteViewChainWithAnimation:NO andCompletion:^{ + UIViewController* callViewController = [[SwiftuiInterface new] makeViewWithName:@"OnboardingView"]; + callViewController.modalPresentationStyle = UIModalPresentationFullScreen; + [self presentViewController:callViewController animated:NO completion:^{}]; + }]; +} + -(void) showSettings { [self performSegueWithIdentifier:@"showSettings" sender:self]; diff --git a/Monal/Classes/BoardingCards.swift b/Monal/Classes/BoardingCards.swift new file mode 100644 index 0000000000..a5d1199790 --- /dev/null +++ b/Monal/Classes/BoardingCards.swift @@ -0,0 +1,185 @@ +// +// BoardingCards.swift +// Monal +// +// Created by Vaidik Dubey on 05/06/24. +// Copyright © 2024 monal-im.org. All rights reserved. +// + +import SwiftUI + +class OnboardingState: ObservableObject { + @defaultsDB("hasCompletedOnboarding") + var hasCompletedOnboarding: Bool +} + +struct OnboardingCard: Identifiable { + let id = UUID() + let title: Text? + let description: Text? + let imageName: String? + let articleText: Text? + let customView: AnyView? +} + +struct OnboardingView: View { + @ObservedObject var onboardingState = OnboardingState() + var delegate: SheetDismisserProtocol + let cards: [OnboardingCard] + + @State private var currentIndex = 0 + + var body: some View { + VStack { + TabView(selection: $currentIndex) { + ForEach(cards.indices, id: \.self) { index in + ScrollView { + VStack(alignment: .leading, spacing: 16) { + + HStack { + if currentIndex > 0 { + Button(action: { + currentIndex -= 1 + }) { + Image(systemName: "chevron.left") + .foregroundColor(.blue) + } + } + } + + if let imageName = cards[index].imageName { + HStack { + Image(systemName: imageName) + .font(.custom("MarkerFelt-Wide", size: 80)) + .foregroundColor(.blue) + } + } + + VStack { + if let title = cards[index].title { + title + .font(.title) + .fontWeight(.bold) + .foregroundColor(.primary) + .padding(.bottom, 4) + } + + if let description = cards[index].description { + description + .font(.custom("HelveticaNeue-Medium", size: 20)) + .foregroundColor(.primary) + .multilineTextAlignment(.leading) + Divider() + } + } + + if let articleText = cards[index].articleText { + articleText + .font(.custom("HelveticaNeue-Medium", size: 20)) + .foregroundColor(.primary) + .multilineTextAlignment(.leading) + } + + if let view = cards[index].customView { + view + .frame(minWidth: 350, maxWidth: .infinity, minHeight: 600, maxHeight: .infinity) + } + + HStack { + Spacer() + if index < cards.count - 1 { + Button(action: { + currentIndex += 1 + }) { + HStack { + Text("Next") + .fontWeight(.bold) + Image(systemName: "chevron.right") + } + .foregroundColor(.blue) + } + } + Spacer() + } + + HStack { + Spacer() + if index == cards.count - 1 { + Button(action: { + delegate.dismiss() + onboardingState.hasCompletedOnboarding = true + }) { + Text("Close") + .fontWeight(.bold) + .padding() + .background(Color.blue) + .foregroundColor(.white) + .cornerRadius(10) + } + } + Spacer() + } + } + .padding() + } + .background(Color(UIColor.systemBackground)) + .cornerRadius(10) + .shadow(color: Color.gray.opacity(0.3), radius: 10) + .padding(.horizontal, 3) + .padding(.vertical, 5) + } + } + .tabViewStyle(PageTabViewStyle()) + .indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .always)) + .frame(width: 350, height: 770) + .padding() + } + .background(Color.clear) + } +} + +@ViewBuilder +func createOnboardingView(delegate: SheetDismisserProtocol) -> some View { + let cards = [ + OnboardingCard( + title: Text("Welcome to Monal !"), + description: Text("Privacy like its 1999 🔒"), + imageName: "hand.wave", + articleText: Text(""" + Modern iOS and MacOS XMPP chat client. + """), + customView: nil + ), + OnboardingCard( + title: Text("Features"), + description: Text("Here's a quick look at what you can expect:"), + imageName: "sparkles", + articleText: Text(""" + • 🔐 OMEMO Encryption : Secure multi-end messaging using the OMEMO protocol.. + + • 🛜 Decentralized Network : Leverages the decentralized nature of XMPP, avoiding central servers. + + • 🌐 Data privacy : We do not sell or track information for external parties (nor for anyone else). + + • 👨‍💻 Open Source : The app's source code is publicly available for audit and contribution. + """), + customView: nil + ), + OnboardingCard( + title: Text("Settings"), + description: Text("These are important privacy settings you may want to review !"), + imageName: nil, + articleText: nil, + customView: AnyView(PrivacySettingsOnboarding(onboardingActive: true)) + ) + ] + OnboardingView(delegate: delegate, cards: cards) +} + +struct OnboardingView_Previews: PreviewProvider { + static var delegate = SheetDismisserProtocol() + static var previews: some View { + createOnboardingView(delegate: delegate) + .environmentObject(OnboardingState()) + } +} diff --git a/Monal/Classes/GeneralSettings.swift b/Monal/Classes/GeneralSettings.swift index 387081c488..a252bb410f 100644 --- a/Monal/Classes/GeneralSettings.swift +++ b/Monal/Classes/GeneralSettings.swift @@ -354,6 +354,17 @@ struct PrivacySettings: View { var body: some View { Form { + PrivacySettingsOnboarding(onboardingActive: false) + } + .navigationBarTitle(Text("Privacy"), displayMode: .inline) + } +} +struct PrivacySettingsOnboarding: View { + @ObservedObject var generalSettingsDefaultsDB = GeneralSettingsDefaultsDB() + var onboardingActive: Bool + + var body: some View { + VStack { Section(header: Text("Activity indications")) { SettingsToggle(isOn: $generalSettingsDefaultsDB.sendReceivedMarkers) { Text("Send message received") @@ -372,7 +383,6 @@ struct PrivacySettings: View { 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") @@ -386,19 +396,19 @@ struct PrivacySettings: View { 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.") + if !onboardingActive { + 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) } } diff --git a/Monal/Classes/SwiftuiHelpers.swift b/Monal/Classes/SwiftuiHelpers.swift index 40b26399cc..2386bfdc53 100644 --- a/Monal/Classes/SwiftuiHelpers.swift +++ b/Monal/Classes/SwiftuiHelpers.swift @@ -684,6 +684,9 @@ class SwiftuiInterface : NSObject { host.rootView = AnyView(AddTopLevelNavigation(withDelegate: delegate, to: PrivacySettings())) case "ActiveChatsNotificatioSettings": host.rootView = AnyView(AddTopLevelNavigation(withDelegate: delegate, to: NotificationSettings())) + case "OnboardingView": + host.rootView = AnyView(createOnboardingView(delegate:delegate)) + default: unreachable() } diff --git a/Monal/Monal.xcodeproj/project.pbxproj b/Monal/Monal.xcodeproj/project.pbxproj index 5dc9d911d2..ffb0970952 100644 --- a/Monal/Monal.xcodeproj/project.pbxproj +++ b/Monal/Monal.xcodeproj/project.pbxproj @@ -10,6 +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 */; }; + 20D3611C2C10E12500E46587 /* BoardingCards.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20D3611B2C10E12500E46587 /* BoardingCards.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 */; }; @@ -307,6 +308,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 = ""; }; + 20D3611B2C10E12500E46587 /* BoardingCards.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoardingCards.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 = ""; }; @@ -1226,6 +1228,7 @@ 54F0B81828231690003664BD /* WelcomeLogIn.swift */, C12436122434AB5D00B8F074 /* MLAttributedLabel.h */, C12436132434AB5D00B8F074 /* MLAttributedLabel.m */, + 20D3611B2C10E12500E46587 /* BoardingCards.swift */, ); name = OnBoard; sourceTree = ""; @@ -2076,6 +2079,7 @@ 54F0B81928231691003664BD /* WelcomeLogIn.swift in Sources */, E8CF9CC726249640001A1952 /* MLSettingsAboutViewController.m in Sources */, C10490492612ED2F0054AC9E /* MLEmoji.swift in Sources */, + 20D3611C2C10E12500E46587 /* BoardingCards.swift in Sources */, 3D06A515281FFCC000DDAE90 /* NotificationDebugging.swift in Sources */, 845D636B2AD4AEDA0066EFFB /* ImageViewer.swift in Sources */, 2636C43F177BD58C001CA71F /* XMPPEdit.m in Sources */, From bb4dc9afa21f1ba63f6f6b25278d6ec1c6d437f7 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Mon, 8 Jul 2024 08:24:51 +0200 Subject: [PATCH 11/35] Add historyID to mlfiletransfer info dictionary --- Monal/Classes/MLFiletransfer.m | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Monal/Classes/MLFiletransfer.m b/Monal/Classes/MLFiletransfer.m index 804d565f94..8a99d8f486 100644 --- a/Monal/Classes/MLFiletransfer.m +++ b/Monal/Classes/MLFiletransfer.m @@ -521,6 +521,7 @@ +(NSDictionary*) getFileInfoForMessage:(MLMessage*) msg @"mimeType": msg.filetransferMimeType, @"size": msg.filetransferSize, @"fileExtension": [filename pathExtension], + @"historyID": msg.messageDBId, }; else return @{ @@ -528,6 +529,7 @@ +(NSDictionary*) getFileInfoForMessage:(MLMessage*) msg @"filename": filename, @"needsDownloading": @YES, @"fileExtension": [filename pathExtension], + @"historyID": msg.messageDBId, }; } return @{ @@ -539,6 +541,7 @@ +(NSDictionary*) getFileInfoForMessage:(MLMessage*) msg @"cacheId": [cacheFile lastPathComponent], @"cacheFile": cacheFile, @"fileExtension": [filename pathExtension], + @"historyID": msg.messageDBId, }; } From 840ad88ad498d2a47801c5d165aeae29a82712d9 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Mon, 8 Jul 2024 10:29:50 +0200 Subject: [PATCH 12/35] Try to fix "Can't add self as subview" in active chats --- Monal/Classes/ActiveChatsViewController.m | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Monal/Classes/ActiveChatsViewController.m b/Monal/Classes/ActiveChatsViewController.m index e966c1911c..7eebc45f89 100755 --- a/Monal/Classes/ActiveChatsViewController.m +++ b/Monal/Classes/ActiveChatsViewController.m @@ -380,9 +380,6 @@ -(void) viewWillAppear:(BOOL) animated { DDLogDebug(@"active chats view will appear"); [super viewWillAppear:animated]; - if(self.unpinnedContacts.count == 0 && self.pinnedContacts.count == 0) - [self refreshDisplay]; // load contacts - [self segueToIntroScreensIfNeeded]; } -(void) viewWillDisappear:(BOOL) animated @@ -395,6 +392,10 @@ -(void) viewDidAppear:(BOOL) animated { DDLogDebug(@"active chats view did appear"); [super viewDidAppear:animated]; + + if(self.unpinnedContacts.count == 0 && self.pinnedContacts.count == 0) + [self refreshDisplay]; // load contacts + [self segueToIntroScreensIfNeeded]; } -(void) didReceiveMemoryWarning From 2748be4bfac875a430933d9176d59eeb63e5f8f2 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Mon, 8 Jul 2024 10:35:25 +0200 Subject: [PATCH 13/35] Fix crash on isEncrypted change when updating MLContact --- Monal/Classes/chatViewController.m | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Monal/Classes/chatViewController.m b/Monal/Classes/chatViewController.m index 72bfaf8740..e61a94ac3b 100644 --- a/Monal/Classes/chatViewController.m +++ b/Monal/Classes/chatViewController.m @@ -594,7 +594,9 @@ -(IBAction) toggleEncryption:(id) sender -(void) observeValueForKeyPath:(NSString*) keyPath ofObject:(id) object change:(NSDictionary *) change context:(void*) context { if([keyPath isEqualToString:@"isEncrypted"] && object == self.contact) - [self displayEncryptionStateInUI]; + dispatch_async(dispatch_get_main_queue(), ^{ + [self displayEncryptionStateInUI]; + }); } -(void) displayEncryptionStateInUI From d0c00e47e52baaaab7922937601f864a24d4454e Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Mon, 8 Jul 2024 12:44:59 +0200 Subject: [PATCH 14/35] Fix typo --- Monal/Classes/ActiveChatsViewController.m | 2 +- Monal/Classes/SwiftuiHelpers.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Monal/Classes/ActiveChatsViewController.m b/Monal/Classes/ActiveChatsViewController.m index 7eebc45f89..a3caafa838 100755 --- a/Monal/Classes/ActiveChatsViewController.m +++ b/Monal/Classes/ActiveChatsViewController.m @@ -519,7 +519,7 @@ -(void) openConversationPlaceholder:(MLContact*) contact -(void) showNotificationSettings { - UIViewController* view = [[SwiftuiInterface new] makeViewWithName:@"ActiveChatsNotificatioSettings"]; + UIViewController* view = [[SwiftuiInterface new] makeViewWithName:@"ActiveChatsNotificationSettings"]; [self presentViewController:view animated:YES completion:^{}]; } diff --git a/Monal/Classes/SwiftuiHelpers.swift b/Monal/Classes/SwiftuiHelpers.swift index 2386bfdc53..0a556821f4 100644 --- a/Monal/Classes/SwiftuiHelpers.swift +++ b/Monal/Classes/SwiftuiHelpers.swift @@ -682,7 +682,7 @@ class SwiftuiInterface : NSObject { host.rootView = AnyView(UIKitWorkaround(GeneralSettings())) case "ActiveChatsPrivacySettings": host.rootView = AnyView(AddTopLevelNavigation(withDelegate: delegate, to: PrivacySettings())) - case "ActiveChatsNotificatioSettings": + case "ActiveChatsNotificationSettings": host.rootView = AnyView(AddTopLevelNavigation(withDelegate: delegate, to: NotificationSettings())) case "OnboardingView": host.rootView = AnyView(createOnboardingView(delegate:delegate)) From 273b8f7829c28fdb9ef4c7d6fd67a0dd50e50814 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Fri, 12 Jul 2024 14:36:41 +0200 Subject: [PATCH 15/35] Properly display geo: urls having a query string The query string (usually containing a zoom factor) will be ignored, though. --- Monal/Classes/MLConstants.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Monal/Classes/MLConstants.h b/Monal/Classes/MLConstants.h index 722c0f6aa4..81148aed4f 100644 --- a/Monal/Classes/MLConstants.h +++ b/Monal/Classes/MLConstants.h @@ -217,4 +217,4 @@ static inline NSString* _Nonnull LocalizationNotNeeded(NSString* _Nonnull s) //build MLXMLNode query statistics (will only optimize MLXMLNode queries if *not* defined) //#define QueryStatistics 1 -#define geoPattern @"^geo:(-?(?:90|[1-8][0-9]|[0-9])(?:\\.[0-9]{1,32})?),(-?(?:180|1[0-7][0-9]|[0-9]{1,2})(?:\\.[0-9]{1,32})?)(;.*)?$" +#define geoPattern @"^geo:(-?(?:90|[1-8][0-9]|[0-9])(?:\\.[0-9]{1,32})?),(-?(?:180|1[0-7][0-9]|[0-9]{1,2})(?:\\.[0-9]{1,32})?)(;.*)?([?].*)?$" From 31a4bfdd386d8d2184118934954ad8684e046a60 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Sat, 13 Jul 2024 12:24:52 +0200 Subject: [PATCH 16/35] Change empty active chats wording --- Monal/Classes/ActiveChatsViewController.m | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Monal/Classes/ActiveChatsViewController.m b/Monal/Classes/ActiveChatsViewController.m index a3caafa838..0899997a98 100755 --- a/Monal/Classes/ActiveChatsViewController.m +++ b/Monal/Classes/ActiveChatsViewController.m @@ -965,7 +965,7 @@ -(CGFloat) spaceHeightForEmptyDataSet:(UIScrollView*) scrollView -(NSAttributedString*) titleForEmptyDataSet:(UIScrollView*) scrollView { - NSString* text = NSLocalizedString(@"No one is here", @""); + NSString* text = NSLocalizedString(@"No active conversations", @""); NSDictionary* attributes = @{NSFontAttributeName: [UIFont boldSystemFontOfSize:18.0f], NSForegroundColorAttributeName: (self.traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark ? [UIColor whiteColor] : [UIColor blackColor])}; @@ -975,7 +975,7 @@ -(NSAttributedString*) titleForEmptyDataSet:(UIScrollView*) scrollView - (NSAttributedString*)descriptionForEmptyDataSet:(UIScrollView*) scrollView { - NSString* text = NSLocalizedString(@"When you start talking to someone,\n they will show up here.", @""); + NSString* text = NSLocalizedString(@"When you start a conversation\nwith someone, they will\nshow up here.", @""); NSMutableParagraphStyle* paragraph = [NSMutableParagraphStyle new]; paragraph.lineBreakMode = NSLineBreakByWordWrapping; From 277a68031385610e2f142864f87261a31960732b Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Sat, 13 Jul 2024 12:12:39 +0200 Subject: [PATCH 17/35] Fix dark mode handling of chat placeholder image --- Monal/Classes/ActiveChatsViewController.m | 2 + Monal/Classes/MLPlaceholderViewController.h | 19 -------- Monal/Classes/MLPlaceholderViewController.m | 46 ------------------- Monal/Monal.xcodeproj/project.pbxproj | 6 --- Monal/localization/Base.lproj/Main.storyboard | 35 -------------- 5 files changed, 2 insertions(+), 106 deletions(-) delete mode 100644 Monal/Classes/MLPlaceholderViewController.h delete mode 100644 Monal/Classes/MLPlaceholderViewController.m diff --git a/Monal/Classes/ActiveChatsViewController.m b/Monal/Classes/ActiveChatsViewController.m index 0899997a98..1230287e9b 100755 --- a/Monal/Classes/ActiveChatsViewController.m +++ b/Monal/Classes/ActiveChatsViewController.m @@ -380,6 +380,8 @@ -(void) viewWillAppear:(BOOL) animated { DDLogDebug(@"active chats view will appear"); [super viewWillAppear:animated]; + + [self openConversationPlaceholder:nil]; } -(void) viewWillDisappear:(BOOL) animated diff --git a/Monal/Classes/MLPlaceholderViewController.h b/Monal/Classes/MLPlaceholderViewController.h deleted file mode 100644 index e999d5b54f..0000000000 --- a/Monal/Classes/MLPlaceholderViewController.h +++ /dev/null @@ -1,19 +0,0 @@ -// -// MLPlaceholderViewController.h -// Monal -// -// Created by Anurodh Pokharel on 1/5/20. -// Copyright © 2020 Monal.im. All rights reserved. -// - -#import -#import "HelperTools.h" -#import "MLImageManager.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface MLPlaceholderViewController : UIViewController - -@end - -NS_ASSUME_NONNULL_END diff --git a/Monal/Classes/MLPlaceholderViewController.m b/Monal/Classes/MLPlaceholderViewController.m deleted file mode 100644 index 4913c80825..0000000000 --- a/Monal/Classes/MLPlaceholderViewController.m +++ /dev/null @@ -1,46 +0,0 @@ -// -// MLPlaceholderViewController.m -// Monal -// -// Created by Anurodh Pokharel on 1/5/20. -// Copyright © 2020 Monal.im. All rights reserved. -// - -#import "MLPlaceholderViewController.h" - -@interface MLPlaceholderViewController () - -@property (weak, nonatomic) IBOutlet UIImageView *backgroundImageView; - -@end - -@implementation MLPlaceholderViewController - -- (void) viewDidLoad -{ - [super viewDidLoad]; - // Do any additional setup after loading the view. -} - - --(void) viewWillAppear:(BOOL)animated -{ - [super viewWillAppear:animated]; - self.splitViewController.preferredDisplayMode = UISplitViewControllerDisplayModeOneBesideSecondary; - - self.backgroundImageView.image = [UIImage imageNamed:@"park_colors"]; -} - --(void) viewWillDisappear:(BOOL)animated -{ - [super viewWillDisappear:animated]; - - [[NSNotificationCenter defaultCenter] removeObserver:self]; -} - --(void) dealloc -{ - [[NSNotificationCenter defaultCenter] removeObserver:self]; -} - -@end diff --git a/Monal/Monal.xcodeproj/project.pbxproj b/Monal/Monal.xcodeproj/project.pbxproj index ffb0970952..29424f0190 100644 --- a/Monal/Monal.xcodeproj/project.pbxproj +++ b/Monal/Monal.xcodeproj/project.pbxproj @@ -40,7 +40,6 @@ 2664D28523F2312400CD4085 /* MLAccountPickerViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2664D28423F2312400CD4085 /* MLAccountPickerViewController.m */; }; 268DD58617C4541000C673A9 /* MLChatCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 268DD58517C4541000C673A9 /* MLChatCell.m */; }; 2696EED21791245A00BC54B8 /* chatViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2696EED11791245A00BC54B8 /* chatViewController.m */; }; - 26A78ED823C2B59400C7CF40 /* MLPlaceholderViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 26A78ED723C2B59400C7CF40 /* MLPlaceholderViewController.m */; }; 26AA70152146BBB900598605 /* ShareViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 26AA70142146BBB900598605 /* ShareViewController.m */; }; 26AA70182146BBB900598605 /* iosShare.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 26AA70162146BBB900598605 /* iosShare.storyboard */; }; 26AA701C2146BBB900598605 /* shareSheet.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 26AA70112146BBB800598605 /* shareSheet.appex */; platformFilter = ios; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; @@ -399,8 +398,6 @@ 268DD58517C4541000C673A9 /* MLChatCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MLChatCell.m; sourceTree = ""; }; 2696EED01791245A00BC54B8 /* chatViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = chatViewController.h; sourceTree = ""; }; 2696EED11791245A00BC54B8 /* chatViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = chatViewController.m; sourceTree = ""; }; - 26A78ED623C2B59400C7CF40 /* MLPlaceholderViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MLPlaceholderViewController.h; sourceTree = ""; }; - 26A78ED723C2B59400C7CF40 /* MLPlaceholderViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MLPlaceholderViewController.m; sourceTree = ""; }; 26AA70112146BBB800598605 /* shareSheet.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = shareSheet.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 26AA70132146BBB900598605 /* ShareViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ShareViewController.h; sourceTree = ""; }; 26AA70142146BBB900598605 /* ShareViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ShareViewController.m; sourceTree = ""; }; @@ -1064,8 +1061,6 @@ 26DB52121777EA5100B50353 /* Tab views */, 26158AF01FFA6E4500E53BDC /* MLWebViewController.h */, 26158AF11FFA6E4500E53BDC /* MLWebViewController.m */, - 26A78ED623C2B59400C7CF40 /* MLPlaceholderViewController.h */, - 26A78ED723C2B59400C7CF40 /* MLPlaceholderViewController.m */, 84FC375828981A5600634E3E /* PasswordMigration.swift */, 3D631822294BAB1D00026BE7 /* ContactPicker.swift */, 3D27D955290B0BB60014748B /* AddContactMenu.swift */, @@ -2089,7 +2084,6 @@ 3DC5035C2822F5220064C8A7 /* OmemoKeys.swift in Sources */, 262797AF178A577300B85D94 /* MLContactCell.m in Sources */, 2696EED21791245A00BC54B8 /* chatViewController.m in Sources */, - 26A78ED823C2B59400C7CF40 /* MLPlaceholderViewController.m in Sources */, C12436142434AB5D00B8F074 /* MLAttributedLabel.m in Sources */, 3D85E587282AE523006F5B3A /* OmemoQrCodeView.swift in Sources */, 849A53E4287135B2007E941A /* MLVoIPProcessor.m in Sources */, diff --git a/Monal/localization/Base.lproj/Main.storyboard b/Monal/localization/Base.lproj/Main.storyboard index dd2ae0e236..e0b4db6b7a 100644 --- a/Monal/localization/Base.lproj/Main.storyboard +++ b/Monal/localization/Base.lproj/Main.storyboard @@ -76,36 +76,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -169,7 +139,6 @@ - @@ -200,7 +169,6 @@ - @@ -2529,9 +2497,6 @@ - - - From 22eab94a80dbc19b81654a9cf30e8243c494f589 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Sat, 13 Jul 2024 16:29:30 +0200 Subject: [PATCH 18/35] Fix onboarding --- Monal/Classes/ActiveChatsViewController.h | 5 +- Monal/Classes/ActiveChatsViewController.m | 69 +++++++++++-- Monal/Classes/BoardingCards.swift | 117 ++++++++++++++-------- Monal/Classes/GeneralSettings.swift | 72 +++++++------ Monal/Classes/MLXMPPManager.m | 4 + Monal/Classes/RegisterAccount.swift | 7 -- Monal/Classes/SwiftuiHelpers.swift | 4 +- Monal/Classes/WelcomeLogIn.swift | 14 +-- 8 files changed, 181 insertions(+), 111 deletions(-) diff --git a/Monal/Classes/ActiveChatsViewController.h b/Monal/Classes/ActiveChatsViewController.h index ebe5e61965..497d630066 100644 --- a/Monal/Classes/ActiveChatsViewController.h +++ b/Monal/Classes/ActiveChatsViewController.h @@ -17,7 +17,7 @@ NS_ASSUME_NONNULL_BEGIN @class chatViewController; @class MLCall; -@interface ActiveChatsViewController : UITableViewController +@interface ActiveChatsViewController : UITableViewController @property (nonatomic, strong) UITableView* chatListTable; @property (nonatomic, weak) IBOutlet UIBarButtonItem* settingsButton; @@ -25,6 +25,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, weak) IBOutlet UIBarButtonItem* composeButton; @property (nonatomic, strong) chatViewController* currentChatViewController; @property (nonatomic, strong) UIActivityIndicatorView* spinner; +@property (nonatomic) BOOL enqueueGeneralSettings; -(void) showCallContactNotFoundAlert:(NSString*) jid; -(void) callContact:(MLContact*) contact withUIKitSender:(_Nullable id) sender; @@ -38,7 +39,7 @@ NS_ASSUME_NONNULL_BEGIN -(void) showContacts; -(void) deleteConversation; -(void) showSettings; --(void) showPrivacySettings; +-(void) showGeneralSettings; -(void) showOnboarding; -(void) showNotificationSettings; -(void) showDetails; diff --git a/Monal/Classes/ActiveChatsViewController.m b/Monal/Classes/ActiveChatsViewController.m index 1230287e9b..555f216fc5 100755 --- a/Monal/Classes/ActiveChatsViewController.m +++ b/Monal/Classes/ActiveChatsViewController.m @@ -395,6 +395,28 @@ -(void) viewDidAppear:(BOOL) animated DDLogDebug(@"active chats view did appear"); [super viewDidAppear:animated]; + [self refresh]; +} + +-(void) presentationControllerDidDismiss:(UIPresentationController*) presentationController +{ + DDLogError(@"DID DISMISS!"); + dispatch_async(dispatch_get_main_queue(), ^{ + [self refresh]; + }); +} + +-(void) presentationControllerWillDismiss:(UIPresentationController*) presentationController +{ + DDLogError(@"WILL DISMISS!"); + dispatch_async(dispatch_get_main_queue(), ^{ + [self refresh]; + }); +} + +-(void) refresh +{ + DDLogError(@"REFRESH!"); if(self.unpinnedContacts.count == 0 && self.pinnedContacts.count == 0) [self refreshDisplay]; // load contacts [self segueToIntroScreensIfNeeded]; @@ -414,6 +436,7 @@ -(void) showAddContactWithJid:(NSString*) jid preauthToken:(NSString* _Nullable) [self presentChatWithContact:newContact]; }); }]; + addContactMenuView.presentationController.delegate = self; [self presentViewController:addContactMenuView animated:NO completion:^{}]; }]; }); @@ -426,6 +449,7 @@ -(void) segueToIntroScreensIfNeeded if(needingMigration.count > 0) { UIViewController* passwordMigration = [[SwiftuiInterface new] makePasswordMigration:needingMigration]; + passwordMigration.presentationController.delegate = self; [self presentViewController:passwordMigration animated:YES completion:^{}]; return; } @@ -436,17 +460,19 @@ -(void) segueToIntroScreensIfNeeded return; } - // display quick start if the user never seen it or if there are 0 enabled accounts - if([[DataLayer sharedInstance] enabledAccountCnts].intValue == 0) + if(self.enqueueGeneralSettings) { - UIViewController* loginViewController = [[SwiftuiInterface new] makeViewWithName:@"WelcomeLogIn"]; - [self presentViewController:loginViewController animated:YES completion:^{}]; + self.enqueueGeneralSettings = NO; + [self showGeneralSettings]; return; } - if(![[HelperTools defaultsDB] boolForKey:@"HasSeenPrivacySettings"]) + // display quick start if the user never seen it or if there are 0 enabled accounts + if([[DataLayer sharedInstance] enabledAccountCnts].intValue == 0) { - [self showPrivacySettings]; + UIViewController* loginViewController = [[SwiftuiInterface new] makeViewWithName:@"WelcomeLogIn"]; + loginViewController.presentationController.delegate = self; + [self presentViewController:loginViewController animated:YES completion:^{}]; return; } @@ -471,6 +497,7 @@ -(void) showWarningsIfNeeded [messageAlert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Close", @"") style:UIAlertActionStyleCancel handler:^(UIAlertAction* action __unused) { [_mamWarningDisplayed addObject:accountNo]; }]]; + messageAlert.presentationController.delegate = self; [self presentViewController:messageAlert animated:YES completion:nil]; } else @@ -485,6 +512,7 @@ -(void) showWarningsIfNeeded [messageAlert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Close", @"") style:UIAlertActionStyleCancel handler:^(UIAlertAction* action __unused) { [_smacksWarningDisplayed addObject:accountNo]; }]]; + messageAlert.presentationController.delegate = self; [self presentViewController:messageAlert animated:YES completion:nil]; } else @@ -499,6 +527,7 @@ -(void) showWarningsIfNeeded [messageAlert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Close", @"") style:UIAlertActionStyleCancel handler:^(UIAlertAction* action __unused) { [_pushWarningDisplayed addObject:accountNo]; }]]; + messageAlert.presentationController.delegate = self; [self presentViewController:messageAlert animated:YES completion:nil]; } else @@ -521,14 +550,20 @@ -(void) openConversationPlaceholder:(MLContact*) contact -(void) showNotificationSettings { - UIViewController* view = [[SwiftuiInterface new] makeViewWithName:@"ActiveChatsNotificationSettings"]; - [self presentViewController:view animated:YES completion:^{}]; + [self dismissCompleteViewChainWithAnimation:NO andCompletion:^{ + UIViewController* view = [[SwiftuiInterface new] makeViewWithName:@"ActiveChatsNotificationSettings"]; + view.presentationController.delegate = self; + [self presentViewController:view animated:YES completion:^{}]; + }]; } --(void) showPrivacySettings +-(void) showGeneralSettings { - UIViewController* view = [[SwiftuiInterface new] makeViewWithName:@"ActiveChatsPrivacySettings"]; - [self presentViewController:view animated:YES completion:^{}]; + [self dismissCompleteViewChainWithAnimation:NO andCompletion:^{ + UIViewController* view = [[SwiftuiInterface new] makeViewWithName:@"ActiveChatsGeneralSettings"]; + view.presentationController.delegate = self; + [self presentViewController:view animated:YES completion:^{}]; + }]; } -(void) showOnboarding @@ -536,6 +571,8 @@ -(void) showOnboarding [self dismissCompleteViewChainWithAnimation:NO andCompletion:^{ UIViewController* callViewController = [[SwiftuiInterface new] makeViewWithName:@"OnboardingView"]; callViewController.modalPresentationStyle = UIModalPresentationFullScreen; + //viewDidAppear (which in turn will call segueToIntroScreensIfNeeded) already called because of fullscreen presentation style above + //view.presentationController.delegate = self; [self presentViewController:callViewController animated:NO completion:^{}]; }]; } @@ -549,6 +586,7 @@ -(void) showCallContactNotFoundAlert:(NSString*) jid { UIAlertController* messageAlert = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"Contact not found", @"") message:[NSString stringWithFormat:NSLocalizedString(@"You tried to call contact '%@' but this contact could not be found in your contact list.", @""), jid] preferredStyle:UIAlertControllerStyleAlert]; [messageAlert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Close", @"") style:UIAlertActionStyleCancel handler:^(UIAlertAction* action __unused) {}]]; + messageAlert.presentationController.delegate = self; [self presentViewController:messageAlert animated:YES completion:nil]; } @@ -592,6 +630,7 @@ -(void) callContact:(MLContact*) contact withUIKitSender:(_Nullable id) sender } else popPresenter.sourceView = self.view; + alert.presentationController.delegate = self; [self presentViewController:alert animated:YES completion:nil]; } } @@ -600,6 +639,7 @@ -(void) presentAccountPickerForContacts:(NSArray*) contacts andCallT { [self dismissCompleteViewChainWithAnimation:NO andCompletion:^{ UIViewController* accountPickerController = [[SwiftuiInterface new] makeAccountPickerForContacts:contacts andCallType:callType]; + accountPickerController.presentationController.delegate = self; [self presentViewController:accountPickerController animated:YES completion:^{}]; }]; } @@ -609,6 +649,8 @@ -(void) presentCall:(MLCall*) call [self dismissCompleteViewChainWithAnimation:NO andCompletion:^{ UIViewController* callViewController = [[SwiftuiInterface new] makeCallScreenForCall:call]; callViewController.modalPresentationStyle = UIModalPresentationFullScreen; + //viewDidAppear (which in turn will call segueToIntroScreensIfNeeded) already called because of fullscreen presentation style above + //callViewController.presentationController.delegate = self; [self presentViewController:callViewController animated:NO completion:^{}]; }]; } @@ -682,6 +724,7 @@ -(BOOL) showAccountNumberWarningIfNeeded [alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Close", @"") style:UIAlertActionStyleDefault handler:^(UIAlertAction* action __unused) { [alert dismissViewControllerAnimated:YES completion:nil]; }]]; + alert.presentationController.delegate = self; [self presentViewController:alert animated:YES completion:nil]; return YES; } @@ -714,6 +757,7 @@ -(void) prepareForSegue:(UIStoryboardSegue*) segue sender:(id) sender else if([segue.identifier isEqualToString:@"showDetails"]) { UIViewController* detailsViewController = [[SwiftuiInterface new] makeContactDetails:sender]; + detailsViewController.presentationController.delegate = self; [self presentViewController:detailsViewController animated:YES completion:^{}]; } else if([segue.identifier isEqualToString:@"showContacts"]) @@ -858,6 +902,7 @@ -(void) tableView:(UITableView*) tableView accessoryButtonTappedForRowWithIndexP selected = self.unpinnedContacts[indexPath.row]; } UIViewController* detailsViewController = [[SwiftuiInterface new] makeContactDetails:selected]; + detailsViewController.presentationController.delegate = self; [self presentViewController:detailsViewController animated:YES completion:^{}]; } @@ -1034,6 +1079,7 @@ -(void) showRegisterWithUsername:(NSString*) username onHost:(NSString*) host wi DDLogWarn(@"Dummy reg completion called for accountNo: %@", accountNo); })), }]; + registerViewController.presentationController.delegate = self; [self presentViewController:registerViewController animated:YES completion:^{}]; }]; } @@ -1043,6 +1089,7 @@ -(void) showDetails if([MLNotificationManager sharedInstance].currentContact != nil) { UIViewController* detailsViewController = [[SwiftuiInterface new] makeContactDetails:[MLNotificationManager sharedInstance].currentContact]; + detailsViewController.presentationController.delegate = self; [self presentViewController:detailsViewController animated:YES completion:^{}]; } } diff --git a/Monal/Classes/BoardingCards.swift b/Monal/Classes/BoardingCards.swift index a5d1199790..3743239893 100644 --- a/Monal/Classes/BoardingCards.swift +++ b/Monal/Classes/BoardingCards.swift @@ -6,7 +6,7 @@ // Copyright © 2024 monal-im.org. All rights reserved. // -import SwiftUI +import FrameUp class OnboardingState: ObservableObject { @defaultsDB("hasCompletedOnboarding") @@ -23,17 +23,20 @@ struct OnboardingCard: Identifiable { } struct OnboardingView: View { - @ObservedObject var onboardingState = OnboardingState() var delegate: SheetDismisserProtocol let cards: [OnboardingCard] - + @ObservedObject var onboardingState = OnboardingState() @State private var currentIndex = 0 var body: some View { - VStack { + ZStack { + Color.background + .edgesIgnoringSafeArea(.all) TabView(selection: $currentIndex) { ForEach(cards.indices, id: \.self) { index in + //SmartScrollView(.vertical, showsIndicators: true, optionalScrolling: true, shrinkToFit: false) { ScrollView { + Group { VStack(alignment: .leading, spacing: 16) { HStack { @@ -47,15 +50,13 @@ struct OnboardingView: View { } } - if let imageName = cards[index].imageName { - HStack { + HStack { + if let imageName = cards[index].imageName { Image(systemName: imageName) .font(.custom("MarkerFelt-Wide", size: 80)) .foregroundColor(.blue) + } - } - - VStack { if let title = cards[index].title { title .font(.title) @@ -63,14 +64,14 @@ struct OnboardingView: View { .foregroundColor(.primary) .padding(.bottom, 4) } - - if let description = cards[index].description { - description - .font(.custom("HelveticaNeue-Medium", size: 20)) - .foregroundColor(.primary) - .multilineTextAlignment(.leading) - Divider() - } + } + + if let description = cards[index].description { + description + .font(.custom("HelveticaNeue-Medium", size: 20)) + .foregroundColor(.primary) + .multilineTextAlignment(.leading) + Divider() } if let articleText = cards[index].articleText { @@ -82,9 +83,10 @@ struct OnboardingView: View { if let view = cards[index].customView { view - .frame(minWidth: 350, maxWidth: .infinity, minHeight: 600, maxHeight: .infinity) } + Spacer() + HStack { Spacer() if index < cards.count - 1 { @@ -98,16 +100,10 @@ struct OnboardingView: View { } .foregroundColor(.blue) } - } - Spacer() - } - - HStack { - Spacer() - if index == cards.count - 1 { + } else { Button(action: { - delegate.dismiss() onboardingState.hasCompletedOnboarding = true + delegate.dismiss() }) { Text("Close") .fontWeight(.bold) @@ -117,24 +113,24 @@ struct OnboardingView: View { .cornerRadius(10) } } - Spacer() } + + Spacer().frame(height: 16) } .padding() + .frame(maxHeight: .infinity) + .background(Color.green) + //.edgesIgnoringSafeArea([.bottom, .leading, .trailing]) } - .background(Color(UIColor.systemBackground)) - .cornerRadius(10) - .shadow(color: Color.gray.opacity(0.3), radius: 10) - .padding(.horizontal, 3) - .padding(.vertical, 5) + .background(Color.red) + .edgesIgnoringSafeArea([.bottom, .leading, .trailing]) + } + //.background(Color(UIColor.systemBackground)) + .background(Color.yellow) + .edgesIgnoringSafeArea([.bottom, .leading, .trailing]) } } - .tabViewStyle(PageTabViewStyle()) - .indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .always)) - .frame(width: 350, height: 770) - .padding() } - .background(Color.clear) } } @@ -167,15 +163,56 @@ func createOnboardingView(delegate: SheetDismisserProtocol) -> some View { ), OnboardingCard( title: Text("Settings"), - description: Text("These are important privacy settings you may want to review !"), + description: Text("These are important privacy settings you may want to review!"), + imageName: nil, + articleText: nil, + customView: AnyView(PrivacySettingsSubview(onboardingPart:0)) + ), + OnboardingCard( + title: Text("Settings"), + description: Text("These are important privacy settings you may want to review!"), imageName: nil, articleText: nil, - customView: AnyView(PrivacySettingsOnboarding(onboardingActive: true)) - ) + customView: AnyView(PrivacySettingsSubview(onboardingPart:1)) + ), + OnboardingCard( + title: Text("Even more to customize!"), + description: Text("You can customize even more, just use the button below to open the settings."), + imageName: "hand.wave", + articleText: nil, + customView: AnyView(TakeMeToSettingsView(delegate:delegate)) + ), ] OnboardingView(delegate: delegate, cards: cards) } +struct TakeMeToSettingsView: View { + @ObservedObject var onboardingState = OnboardingState() + var delegate: SheetDismisserProtocol + + var body: some View { + HStack { + Spacer() + Button(action: { + let appDelegate = UIApplication.shared.delegate as! MonalAppDelegate + if let activeChats = appDelegate.activeChats { + activeChats.enqueueGeneralSettings = true + } + onboardingState.hasCompletedOnboarding = true + delegate.dismiss() + }) { + Text("Take me to settings") + .fontWeight(.bold) + .padding() + .background(Color.blue) + .foregroundColor(.white) + .cornerRadius(10) + } + Spacer() + } + } +} + struct OnboardingView_Previews: PreviewProvider { static var delegate = SheetDismisserProtocol() static var previews: some View { diff --git a/Monal/Classes/GeneralSettings.swift b/Monal/Classes/GeneralSettings.swift index a252bb410f..c71a0339f5 100644 --- a/Monal/Classes/GeneralSettings.swift +++ b/Monal/Classes/GeneralSettings.swift @@ -98,9 +98,6 @@ class GeneralSettingsDefaultsDB: ObservableObject { @defaultsDB("allowCallsFromNonRosterContacts") var allowCallsFromNonRosterContacts: Bool - @defaultsDB("HasSeenPrivacySettings") - var hasSeenPrivacySettings: Bool - @defaultsDB("AutodownloadFiletransfers") var autodownloadFiletransfers : Bool @@ -192,9 +189,6 @@ struct GeneralSettings: View { } } .navigationBarTitle(Text("General Settings")) - .onAppear { - generalSettingsDefaultsDB.hasSeenPrivacySettings = true - } } } @@ -354,49 +348,53 @@ struct PrivacySettings: View { var body: some View { Form { - PrivacySettingsOnboarding(onboardingActive: false) + PrivacySettingsSubview(onboardingPart:-1) } .navigationBarTitle(Text("Privacy"), displayMode: .inline) } } -struct PrivacySettingsOnboarding: View { +struct PrivacySettingsSubview: View { @ObservedObject var generalSettingsDefaultsDB = GeneralSettingsDefaultsDB() - var onboardingActive: Bool + var onboardingPart: Int var body: some View { VStack { - 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.") + if onboardingPart == -1 || onboardingPart == 0 { + 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.") + if onboardingPart == -1 || onboardingPart == 1 { + 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: Binding( + get: { generalSettingsDefaultsDB.allowCallsFromNonRosterContacts && generalSettingsDefaultsDB.allowNonRosterContacts }, + set: { generalSettingsDefaultsDB.allowCallsFromNonRosterContacts = $0 } + )) { + Text("Accept incoming calls from strangers") + Text("Allow contacts not in your contact list to call you.") + }.disabled(!generalSettingsDefaultsDB.allowNonRosterContacts) } - SettingsToggle(isOn: Binding( - get: { generalSettingsDefaultsDB.allowCallsFromNonRosterContacts && generalSettingsDefaultsDB.allowNonRosterContacts }, - set: { generalSettingsDefaultsDB.allowCallsFromNonRosterContacts = $0 } - )) { - Text("Accept incoming calls from strangers") - Text("Allow contacts not in your contact list to call you.") - }.disabled(!generalSettingsDefaultsDB.allowNonRosterContacts) } - if !onboardingActive { + if onboardingPart == -1 || onboardingPart == 2 { Section(header: Text("Misc")) { SettingsToggle(isOn: $generalSettingsDefaultsDB.allowVersionIQ) { Text("Publish version") diff --git a/Monal/Classes/MLXMPPManager.m b/Monal/Classes/MLXMPPManager.m index 1f5a508c71..6b0aa1295d 100644 --- a/Monal/Classes/MLXMPPManager.m +++ b/Monal/Classes/MLXMPPManager.m @@ -144,6 +144,10 @@ -(void) defaultSettings [self upgradeBoolUserSettingsIfUnset:@"useInlineSafari" toDefault:NO]; else [self upgradeBoolUserSettingsIfUnset:@"useInlineSafari" toDefault:YES]; + + [self upgradeBoolUserSettingsIfUnset:@"hasCompletedOnboarding" toDefault:NO]; + + [[HelperTools defaultsDB] setBool:NO forKey:@"hasCompletedOnboarding"]; } -(void) upgradeFloatUserSettingsToInteger:(NSString*) settingsName diff --git a/Monal/Classes/RegisterAccount.swift b/Monal/Classes/RegisterAccount.swift index 90a7b7c9a8..6a29d81238 100644 --- a/Monal/Classes/RegisterAccount.swift +++ b/Monal/Classes/RegisterAccount.swift @@ -388,13 +388,6 @@ struct RegisterAccount: View { if(self.registerComplete == true) { self.delegate.dismiss() - if !HelperTools.defaultsDB().bool(forKey:"HasSeenPrivacySettings") { - let appDelegate = UIApplication.shared.delegate as! MonalAppDelegate - if let activeChats = appDelegate.activeChats { - activeChats.showPrivacySettings() - } - } - if let completion = self.completionHandler { DDLogVerbose("Calling reg completion handler...") completion(self.registeredAccountNo as NSNumber) diff --git a/Monal/Classes/SwiftuiHelpers.swift b/Monal/Classes/SwiftuiHelpers.swift index 0a556821f4..00b8e3b5ff 100644 --- a/Monal/Classes/SwiftuiHelpers.swift +++ b/Monal/Classes/SwiftuiHelpers.swift @@ -680,8 +680,8 @@ class SwiftuiInterface : NSObject { host.rootView = AnyView(ChatPlaceholder()) case "GeneralSettings" : host.rootView = AnyView(UIKitWorkaround(GeneralSettings())) - case "ActiveChatsPrivacySettings": - host.rootView = AnyView(AddTopLevelNavigation(withDelegate: delegate, to: PrivacySettings())) + case "ActiveChatsGeneralSettings": + host.rootView = AnyView(AddTopLevelNavigation(withDelegate: delegate, to: GeneralSettings())) case "ActiveChatsNotificationSettings": host.rootView = AnyView(AddTopLevelNavigation(withDelegate: delegate, to: NotificationSettings())) case "OnboardingView": diff --git a/Monal/Classes/WelcomeLogIn.swift b/Monal/Classes/WelcomeLogIn.swift index d1c277a944..0379ee5d73 100644 --- a/Monal/Classes/WelcomeLogIn.swift +++ b/Monal/Classes/WelcomeLogIn.swift @@ -110,16 +110,6 @@ 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 { @@ -186,7 +176,7 @@ struct WelcomeLogIn: View { .alert(isPresented: $showAlert) { Alert(title: alertPrompt.title, message: alertPrompt.message, dismissButton: .default(alertPrompt.dismissLabel, action: { if(self.loginComplete == true) { - dismissAndShowPrivacySettings() + self.delegate.dismiss() } })) } @@ -225,7 +215,7 @@ struct WelcomeLogIn: View { if(DataLayer.sharedInstance().enabledAccountCnts() == 0) { Button(action: { - dismissAndShowPrivacySettings() + self.delegate.dismiss() }){ Text("Set up account later") .frame(maxWidth: .infinity) From ac7ba70365ea3c23123877c861a003a06b128749 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Sun, 14 Jul 2024 11:04:35 +0200 Subject: [PATCH 19/35] Fix delegate crash --- Monal/Classes/ActiveChatsViewController.m | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Monal/Classes/ActiveChatsViewController.m b/Monal/Classes/ActiveChatsViewController.m index 555f216fc5..9d4a38280d 100755 --- a/Monal/Classes/ActiveChatsViewController.m +++ b/Monal/Classes/ActiveChatsViewController.m @@ -497,7 +497,6 @@ -(void) showWarningsIfNeeded [messageAlert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Close", @"") style:UIAlertActionStyleCancel handler:^(UIAlertAction* action __unused) { [_mamWarningDisplayed addObject:accountNo]; }]]; - messageAlert.presentationController.delegate = self; [self presentViewController:messageAlert animated:YES completion:nil]; } else @@ -512,7 +511,6 @@ -(void) showWarningsIfNeeded [messageAlert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Close", @"") style:UIAlertActionStyleCancel handler:^(UIAlertAction* action __unused) { [_smacksWarningDisplayed addObject:accountNo]; }]]; - messageAlert.presentationController.delegate = self; [self presentViewController:messageAlert animated:YES completion:nil]; } else @@ -527,7 +525,6 @@ -(void) showWarningsIfNeeded [messageAlert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Close", @"") style:UIAlertActionStyleCancel handler:^(UIAlertAction* action __unused) { [_pushWarningDisplayed addObject:accountNo]; }]]; - messageAlert.presentationController.delegate = self; [self presentViewController:messageAlert animated:YES completion:nil]; } else @@ -586,7 +583,6 @@ -(void) showCallContactNotFoundAlert:(NSString*) jid { UIAlertController* messageAlert = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"Contact not found", @"") message:[NSString stringWithFormat:NSLocalizedString(@"You tried to call contact '%@' but this contact could not be found in your contact list.", @""), jid] preferredStyle:UIAlertControllerStyleAlert]; [messageAlert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Close", @"") style:UIAlertActionStyleCancel handler:^(UIAlertAction* action __unused) {}]]; - messageAlert.presentationController.delegate = self; [self presentViewController:messageAlert animated:YES completion:nil]; } @@ -630,7 +626,6 @@ -(void) callContact:(MLContact*) contact withUIKitSender:(_Nullable id) sender } else popPresenter.sourceView = self.view; - alert.presentationController.delegate = self; [self presentViewController:alert animated:YES completion:nil]; } } @@ -724,7 +719,6 @@ -(BOOL) showAccountNumberWarningIfNeeded [alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Close", @"") style:UIAlertActionStyleDefault handler:^(UIAlertAction* action __unused) { [alert dismissViewControllerAnimated:YES completion:nil]; }]]; - alert.presentationController.delegate = self; [self presentViewController:alert animated:YES completion:nil]; return YES; } From 43a41746d7497b8000ab47875f527bed7132afd8 Mon Sep 17 00:00:00 2001 From: Ryan Lintott Date: Sat, 13 Jul 2024 15:18:04 -0400 Subject: [PATCH 20/35] Updated BoardingCards view to take the full available space and scroll if necessary. --- Monal/Classes/BoardingCards.swift | 144 ++++++++++++++---------------- 1 file changed, 67 insertions(+), 77 deletions(-) diff --git a/Monal/Classes/BoardingCards.swift b/Monal/Classes/BoardingCards.swift index 3743239893..81edfec469 100644 --- a/Monal/Classes/BoardingCards.swift +++ b/Monal/Classes/BoardingCards.swift @@ -30,104 +30,94 @@ struct OnboardingView: View { var body: some View { ZStack { - Color.background - .edgesIgnoringSafeArea(.all) - TabView(selection: $currentIndex) { - ForEach(cards.indices, id: \.self) { index in - //SmartScrollView(.vertical, showsIndicators: true, optionalScrolling: true, shrinkToFit: false) { - ScrollView { - Group { - VStack(alignment: .leading, spacing: 16) { - - HStack { + /// Ensure the ZStack takes the entire area + Color.clear + + ForEach(Array(zip(cards, cards.indices)), id: \.1) { card, index in + /// Only show card that's visible + if index == currentIndex { + GeometryReader { proxy in + SmartScrollView(.vertical, showsIndicators: true, optionalScrolling: true, shrinkToFit: false) { + VStack(alignment: .leading, spacing: 16) { + if currentIndex > 0 { - Button(action: { + Button { currentIndex -= 1 - }) { - Image(systemName: "chevron.left") + } label: { + Label("Back", systemImage: "chevron.left") + .labelStyle(.iconOnly) .foregroundColor(.blue) } } - } - - HStack { - if let imageName = cards[index].imageName { - Image(systemName: imageName) - .font(.custom("MarkerFelt-Wide", size: 80)) - .foregroundColor(.blue) + + HStack { + if let imageName = card.imageName { + Image(systemName: imageName) + .font(.custom("MarkerFelt-Wide", size: 80)) + .foregroundColor(.blue) + + } - } - if let title = cards[index].title { - title + card.title? .font(.title) .fontWeight(.bold) .foregroundColor(.primary) .padding(.bottom, 4) } - } - - if let description = cards[index].description { - description - .font(.custom("HelveticaNeue-Medium", size: 20)) - .foregroundColor(.primary) - .multilineTextAlignment(.leading) - Divider() - } - - if let articleText = cards[index].articleText { - articleText + + if let description = card.description { + description + .font(.custom("HelveticaNeue-Medium", size: 20)) + .foregroundColor(.primary) + .multilineTextAlignment(.leading) + /// This ensures text doesn't get truncated which sometimes happens in ScrollView + .fixedSize(horizontal: false, vertical: true) + + Divider() + } + + card.articleText? .font(.custom("HelveticaNeue-Medium", size: 20)) .foregroundColor(.primary) .multilineTextAlignment(.leading) - } - - if let view = cards[index].customView { - view - } - - Spacer() - - HStack { + + card.customView + Spacer() - if index < cards.count - 1 { - Button(action: { - currentIndex += 1 - }) { - HStack { - Text("Next") + + Group { + if index < cards.count - 1 { + Button { + currentIndex += 1 + } label: { + HStack { + Text("Next") + .fontWeight(.bold) + Image(systemName: "chevron.right") + } + } + } else { + Button { + onboardingState.hasCompletedOnboarding = true + delegate.dismiss() + } label: { + Text("Close") .fontWeight(.bold) - Image(systemName: "chevron.right") + .padding() + .background(Color.blue) + .foregroundColor(.white) + .cornerRadius(10) } - .foregroundColor(.blue) - } - } else { - Button(action: { - onboardingState.hasCompletedOnboarding = true - delegate.dismiss() - }) { - Text("Close") - .fontWeight(.bold) - .padding() - .background(Color.blue) - .foregroundColor(.white) - .cornerRadius(10) } } + .frame(maxWidth: .infinity, alignment: .trailing) } - - Spacer().frame(height: 16) + .padding(.bottom, 16) + .padding() + /// Sets the minimum frame height to the available height of the scrollview and the maxHeight to infinity + .frame(minHeight: proxy.size.height, maxHeight: .infinity) } - .padding() - .frame(maxHeight: .infinity) - .background(Color.green) - //.edgesIgnoringSafeArea([.bottom, .leading, .trailing]) - } - .background(Color.red) - .edgesIgnoringSafeArea([.bottom, .leading, .trailing]) } - //.background(Color(UIColor.systemBackground)) - .background(Color.yellow) - .edgesIgnoringSafeArea([.bottom, .leading, .trailing]) } } } @@ -151,7 +141,7 @@ func createOnboardingView(delegate: SheetDismisserProtocol) -> some View { description: Text("Here's a quick look at what you can expect:"), imageName: "sparkles", articleText: Text(""" - • 🔐 OMEMO Encryption : Secure multi-end messaging using the OMEMO protocol.. + • 🔐 OMEMO Encryption : Secure multi-end messaging using the OMEMO protocol. • 🛜 Decentralized Network : Leverages the decentralized nature of XMPP, avoiding central servers. From 9001857efa0a246bd807829a5afef144f508cefe Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Sun, 14 Jul 2024 19:51:05 +0200 Subject: [PATCH 21/35] More onboarding fixes --- Monal/Classes/ActiveChatsViewController.h | 3 +- Monal/Classes/ActiveChatsViewController.m | 61 ++++++++++------- Monal/Classes/GeneralSettings.swift | 82 +++++++++++------------ Monal/Classes/SwiftuiHelpers.swift | 62 +++++++++++++---- 4 files changed, 126 insertions(+), 82 deletions(-) diff --git a/Monal/Classes/ActiveChatsViewController.h b/Monal/Classes/ActiveChatsViewController.h index 497d630066..aa46b7f5ec 100644 --- a/Monal/Classes/ActiveChatsViewController.h +++ b/Monal/Classes/ActiveChatsViewController.h @@ -14,10 +14,11 @@ NS_ASSUME_NONNULL_BEGIN +@class UIHostingControllerWorkaround; @class chatViewController; @class MLCall; -@interface ActiveChatsViewController : UITableViewController +@interface ActiveChatsViewController : UITableViewController @property (nonatomic, strong) UITableView* chatListTable; @property (nonatomic, weak) IBOutlet UIBarButtonItem* settingsButton; diff --git a/Monal/Classes/ActiveChatsViewController.m b/Monal/Classes/ActiveChatsViewController.m index 9d4a38280d..ba7e9dd168 100755 --- a/Monal/Classes/ActiveChatsViewController.m +++ b/Monal/Classes/ActiveChatsViewController.m @@ -39,6 +39,7 @@ @interface ActiveChatsViewController() { int _startedOrientation; double _portraitTop; double _landscapeTop; + BOOL _loginAutodisplayedAlready; } @property (atomic, strong) NSMutableArray* unpinnedContacts; @property (atomic, strong) NSMutableArray* pinnedContacts; @@ -67,6 +68,7 @@ +(void) initialize -(id) initWithNibName:(NSString*) nibNameOrNil bundle:(NSBundle*) nibBundleOrNil { self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; + _loginAutodisplayedAlready = NO; return self; } @@ -398,17 +400,8 @@ -(void) viewDidAppear:(BOOL) animated [self refresh]; } --(void) presentationControllerDidDismiss:(UIPresentationController*) presentationController +-(void) sheetDismissed { - DDLogError(@"DID DISMISS!"); - dispatch_async(dispatch_get_main_queue(), ^{ - [self refresh]; - }); -} - --(void) presentationControllerWillDismiss:(UIPresentationController*) presentationController -{ - DDLogError(@"WILL DISMISS!"); dispatch_async(dispatch_get_main_queue(), ^{ [self refresh]; }); @@ -416,7 +409,6 @@ -(void) presentationControllerWillDismiss:(UIPresentationController*) presentati -(void) refresh { - DDLogError(@"REFRESH!"); if(self.unpinnedContacts.count == 0 && self.pinnedContacts.count == 0) [self refreshDisplay]; // load contacts [self segueToIntroScreensIfNeeded]; @@ -436,7 +428,9 @@ -(void) showAddContactWithJid:(NSString*) jid preauthToken:(NSString* _Nullable) [self presentChatWithContact:newContact]; }); }]; - addContactMenuView.presentationController.delegate = self; + addContactMenuView.ml_disposeCallback = ^{ + [self sheetDismissed]; + }; [self presentViewController:addContactMenuView animated:NO completion:^{}]; }]; }); @@ -449,7 +443,9 @@ -(void) segueToIntroScreensIfNeeded if(needingMigration.count > 0) { UIViewController* passwordMigration = [[SwiftuiInterface new] makePasswordMigration:needingMigration]; - passwordMigration.presentationController.delegate = self; + passwordMigration.ml_disposeCallback = ^{ + [self sheetDismissed]; + }; [self presentViewController:passwordMigration animated:YES completion:^{}]; return; } @@ -468,10 +464,13 @@ -(void) segueToIntroScreensIfNeeded } // display quick start if the user never seen it or if there are 0 enabled accounts - if([[DataLayer sharedInstance] enabledAccountCnts].intValue == 0) + if([[DataLayer sharedInstance] enabledAccountCnts].intValue == 0 && !_loginAutodisplayedAlready) { UIViewController* loginViewController = [[SwiftuiInterface new] makeViewWithName:@"WelcomeLogIn"]; - loginViewController.presentationController.delegate = self; + loginViewController.ml_disposeCallback = ^{ + self->_loginAutodisplayedAlready = YES; + [self sheetDismissed]; + }; [self presentViewController:loginViewController animated:YES completion:^{}]; return; } @@ -549,7 +548,9 @@ -(void) showNotificationSettings { [self dismissCompleteViewChainWithAnimation:NO andCompletion:^{ UIViewController* view = [[SwiftuiInterface new] makeViewWithName:@"ActiveChatsNotificationSettings"]; - view.presentationController.delegate = self; + view.ml_disposeCallback = ^{ + [self sheetDismissed]; + }; [self presentViewController:view animated:YES completion:^{}]; }]; } @@ -558,7 +559,9 @@ -(void) showGeneralSettings { [self dismissCompleteViewChainWithAnimation:NO andCompletion:^{ UIViewController* view = [[SwiftuiInterface new] makeViewWithName:@"ActiveChatsGeneralSettings"]; - view.presentationController.delegate = self; + view.ml_disposeCallback = ^{ + [self sheetDismissed]; + }; [self presentViewController:view animated:YES completion:^{}]; }]; } @@ -568,8 +571,6 @@ -(void) showOnboarding [self dismissCompleteViewChainWithAnimation:NO andCompletion:^{ UIViewController* callViewController = [[SwiftuiInterface new] makeViewWithName:@"OnboardingView"]; callViewController.modalPresentationStyle = UIModalPresentationFullScreen; - //viewDidAppear (which in turn will call segueToIntroScreensIfNeeded) already called because of fullscreen presentation style above - //view.presentationController.delegate = self; [self presentViewController:callViewController animated:NO completion:^{}]; }]; } @@ -634,7 +635,9 @@ -(void) presentAccountPickerForContacts:(NSArray*) contacts andCallT { [self dismissCompleteViewChainWithAnimation:NO andCompletion:^{ UIViewController* accountPickerController = [[SwiftuiInterface new] makeAccountPickerForContacts:contacts andCallType:callType]; - accountPickerController.presentationController.delegate = self; + accountPickerController.ml_disposeCallback = ^{ + [self sheetDismissed]; + }; [self presentViewController:accountPickerController animated:YES completion:^{}]; }]; } @@ -644,8 +647,6 @@ -(void) presentCall:(MLCall*) call [self dismissCompleteViewChainWithAnimation:NO andCompletion:^{ UIViewController* callViewController = [[SwiftuiInterface new] makeCallScreenForCall:call]; callViewController.modalPresentationStyle = UIModalPresentationFullScreen; - //viewDidAppear (which in turn will call segueToIntroScreensIfNeeded) already called because of fullscreen presentation style above - //callViewController.presentationController.delegate = self; [self presentViewController:callViewController animated:NO completion:^{}]; }]; } @@ -751,7 +752,9 @@ -(void) prepareForSegue:(UIStoryboardSegue*) segue sender:(id) sender else if([segue.identifier isEqualToString:@"showDetails"]) { UIViewController* detailsViewController = [[SwiftuiInterface new] makeContactDetails:sender]; - detailsViewController.presentationController.delegate = self; + detailsViewController.ml_disposeCallback = ^{ + [self sheetDismissed]; + }; [self presentViewController:detailsViewController animated:YES completion:^{}]; } else if([segue.identifier isEqualToString:@"showContacts"]) @@ -896,7 +899,9 @@ -(void) tableView:(UITableView*) tableView accessoryButtonTappedForRowWithIndexP selected = self.unpinnedContacts[indexPath.row]; } UIViewController* detailsViewController = [[SwiftuiInterface new] makeContactDetails:selected]; - detailsViewController.presentationController.delegate = self; + detailsViewController.ml_disposeCallback = ^{ + [self sheetDismissed]; + }; [self presentViewController:detailsViewController animated:YES completion:^{}]; } @@ -1073,7 +1078,9 @@ -(void) showRegisterWithUsername:(NSString*) username onHost:(NSString*) host wi DDLogWarn(@"Dummy reg completion called for accountNo: %@", accountNo); })), }]; - registerViewController.presentationController.delegate = self; + registerViewController.ml_disposeCallback = ^{ + [self sheetDismissed]; + }; [self presentViewController:registerViewController animated:YES completion:^{}]; }]; } @@ -1083,7 +1090,9 @@ -(void) showDetails if([MLNotificationManager sharedInstance].currentContact != nil) { UIViewController* detailsViewController = [[SwiftuiInterface new] makeContactDetails:[MLNotificationManager sharedInstance].currentContact]; - detailsViewController.presentationController.delegate = self; + detailsViewController.ml_disposeCallback = ^{ + [self sheetDismissed]; + }; [self presentViewController:detailsViewController animated:YES completion:^{}]; } } diff --git a/Monal/Classes/GeneralSettings.swift b/Monal/Classes/GeneralSettings.swift index c71a0339f5..b853145224 100644 --- a/Monal/Classes/GeneralSettings.swift +++ b/Monal/Classes/GeneralSettings.swift @@ -358,52 +358,50 @@ struct PrivacySettingsSubview: View { var onboardingPart: Int var body: some View { - VStack { - if onboardingPart == -1 || onboardingPart == 0 { - 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.") - } + if onboardingPart == -1 || onboardingPart == 0 { + 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.") } } - if onboardingPart == -1 || onboardingPart == 1 { - 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: Binding( - get: { generalSettingsDefaultsDB.allowCallsFromNonRosterContacts && generalSettingsDefaultsDB.allowNonRosterContacts }, - set: { generalSettingsDefaultsDB.allowCallsFromNonRosterContacts = $0 } - )) { - Text("Accept incoming calls from strangers") - Text("Allow contacts not in your contact list to call you.") - }.disabled(!generalSettingsDefaultsDB.allowNonRosterContacts) + } + if onboardingPart == -1 || onboardingPart == 1 { + 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: Binding( + get: { generalSettingsDefaultsDB.allowCallsFromNonRosterContacts && generalSettingsDefaultsDB.allowNonRosterContacts }, + set: { generalSettingsDefaultsDB.allowCallsFromNonRosterContacts = $0 } + )) { + Text("Accept incoming calls from strangers") + Text("Allow contacts not in your contact list to call you.") + }.disabled(!generalSettingsDefaultsDB.allowNonRosterContacts) } - if onboardingPart == -1 || onboardingPart == 2 { - 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.") - } + } + if onboardingPart == -1 || onboardingPart == 2 { + 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.") } } } diff --git a/Monal/Classes/SwiftuiHelpers.swift b/Monal/Classes/SwiftuiHelpers.swift index 00b8e3b5ff..20b5c762e8 100644 --- a/Monal/Classes/SwiftuiHelpers.swift +++ b/Monal/Classes/SwiftuiHelpers.swift @@ -574,6 +574,41 @@ extension View { } } +public extension UIViewController { + private struct AssociatedKeys { + static var DisposeCallbackKey = "ml_disposeCallbackKey" + } + + private class DisposeCallback : NSObject { + let callback: monal_void_block_t + + init(withCallback callback: @escaping monal_void_block_t) { + self.callback = callback + } + + deinit { + self.callback() + } + } + + @objc + var ml_disposeCallback: monal_void_block_t { + get { + return withUnsafePointer(to: &AssociatedKeys.DisposeCallbackKey) { pointer in + if let callback = (objc_getAssociatedObject(self, pointer) as? DisposeCallback)?.callback { + return callback + } + unreachable("You can't get what you did not set!") + } + } + set { + withUnsafePointer(to: &AssociatedKeys.DisposeCallbackKey) { pointer in + objc_setAssociatedObject(self, pointer, DisposeCallback(withCallback: newValue), .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + } + } + } +} + // Interfaces between ObjectiveC/Storyboards and SwiftUI @objc class SwiftuiInterface : NSObject { @@ -663,33 +698,34 @@ class SwiftuiInterface : NSObject { @objc func makeView(name: String) -> UIViewController { let delegate = SheetDismisserProtocol() - let host = UIHostingController(rootView:AnyView(EmptyView())) - delegate.host = host + var host: UIHostingController? = nil + //let host = UIHostingController(rootView:AnyView(EmptyView())) switch(name) { // TODO names are currently taken from the segue identifier, an enum would be nice once everything is ported to SwiftUI case "DebugView": - host.rootView = AnyView(UIKitWorkaround(DebugView())) + host = UIHostingController(rootView:AnyView(UIKitWorkaround(DebugView()))) case "WelcomeLogIn": - host.rootView = AnyView(AddTopLevelNavigation(withDelegate:delegate, to:WelcomeLogIn(delegate:delegate))) + host = UIHostingController(rootView:AnyView(AddTopLevelNavigation(withDelegate:delegate, to:WelcomeLogIn(delegate:delegate)))) case "LogIn": - host.rootView = AnyView(UIKitWorkaround(WelcomeLogIn(delegate:delegate))) + host = UIHostingController(rootView:AnyView(UIKitWorkaround(WelcomeLogIn(delegate:delegate)))) case "ContactRequests": - host.rootView = AnyView(AddTopLevelNavigation(withDelegate: delegate, to: ContactRequestsMenu())) + host = UIHostingController(rootView:AnyView(AddTopLevelNavigation(withDelegate: delegate, to: ContactRequestsMenu()))) case "CreateGroup": - host.rootView = AnyView(AddTopLevelNavigation(withDelegate: delegate, to: CreateGroupMenu(delegate: delegate))) + host = UIHostingController(rootView:AnyView(AddTopLevelNavigation(withDelegate: delegate, to: CreateGroupMenu(delegate: delegate)))) case "ChatPlaceholder": - host.rootView = AnyView(ChatPlaceholder()) + host = UIHostingController(rootView:AnyView(ChatPlaceholder())) case "GeneralSettings" : - host.rootView = AnyView(UIKitWorkaround(GeneralSettings())) + host = UIHostingController(rootView:AnyView(UIKitWorkaround(GeneralSettings()))) case "ActiveChatsGeneralSettings": - host.rootView = AnyView(AddTopLevelNavigation(withDelegate: delegate, to: GeneralSettings())) + host = UIHostingController(rootView:AnyView(AddTopLevelNavigation(withDelegate: delegate, to: GeneralSettings()))) case "ActiveChatsNotificationSettings": - host.rootView = AnyView(AddTopLevelNavigation(withDelegate: delegate, to: NotificationSettings())) + host = UIHostingController(rootView:AnyView(AddTopLevelNavigation(withDelegate: delegate, to: NotificationSettings()))) case "OnboardingView": - host.rootView = AnyView(createOnboardingView(delegate:delegate)) + host = UIHostingController(rootView:AnyView(createOnboardingView(delegate:delegate))) default: unreachable() } - return host + delegate.host = host! + return host! } } From 1bf35c0829569f741f5d33e40a5478218814c197 Mon Sep 17 00:00:00 2001 From: Ryan Lintott Date: Sun, 14 Jul 2024 13:38:16 -0400 Subject: [PATCH 22/35] BoardingCards updated to improve accessibility Fixed tap area on back button as it was tiny Hid the title image from accessibility and merged it with the title into one accessibility element making it a header Made each card a modal for accessibility so VoiceOver moves back to the top when tapping next. --- Monal/Classes/BoardingCards.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Monal/Classes/BoardingCards.swift b/Monal/Classes/BoardingCards.swift index 81edfec469..e0331a6f12 100644 --- a/Monal/Classes/BoardingCards.swift +++ b/Monal/Classes/BoardingCards.swift @@ -47,6 +47,7 @@ struct OnboardingView: View { Label("Back", systemImage: "chevron.left") .labelStyle(.iconOnly) .foregroundColor(.blue) + .padding() } } @@ -55,6 +56,7 @@ struct OnboardingView: View { Image(systemName: imageName) .font(.custom("MarkerFelt-Wide", size: 80)) .foregroundColor(.blue) + .accessibilityHidden(true) } @@ -64,6 +66,8 @@ struct OnboardingView: View { .foregroundColor(.primary) .padding(.bottom, 4) } + .accessibilityElement(children: .combine) + .accessibilityAddTraits(.isHeader) if let description = card.description { description @@ -118,6 +122,7 @@ struct OnboardingView: View { .frame(minHeight: proxy.size.height, maxHeight: .infinity) } } + .accessibilityAddTraits(.isModal) } } } From a4d26a4dcf057afc664b010001580b111baeeb2d Mon Sep 17 00:00:00 2001 From: Ryan Lintott Date: Sun, 14 Jul 2024 13:58:25 -0400 Subject: [PATCH 23/35] Fixed accessibility labels on Contacts toolbar buttons --- Monal/Classes/ContactsViewController.m | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Monal/Classes/ContactsViewController.m b/Monal/Classes/ContactsViewController.m index 7eb6ca5f28..3adace979f 100644 --- a/Monal/Classes/ContactsViewController.m +++ b/Monal/Classes/ContactsViewController.m @@ -68,7 +68,8 @@ -(void) configureContactRequestsImage hasNotification:[[DataLayer sharedInstance] allContactRequests].count > 0 withTapHandler:requestsTapRecoginzer]; [self.navigationItem.rightBarButtonItems[1] setIsAccessibilityElement:YES]; - [self.navigationItem.rightBarButtonItems[1] setAccessibilityLabel:NSLocalizedString(@"Open list of pending contact requests", @"")]; + [self.navigationItem.rightBarButtonItems[1] setAccessibilityLabel:NSLocalizedString(@"Pending contact requests", @"")]; + [self.navigationItem.rightBarButtonItems[1] setAccessibilityTraits:UIAccessibilityTraitButton]; } @@ -106,11 +107,13 @@ -(void) viewDidLoad UIBarButtonItem* addContact = [UIBarButtonItem new]; addContact.image = [UIImage systemImageNamed:@"person.fill.badge.plus"]; + addContact.accessibilityLabel = @"Add contact"; [addContact setAction:@selector(openAddContacts:)]; [addContact setTarget:self]; UIBarButtonItem* createGroup = [[UIBarButtonItem alloc] init]; createGroup.image = [UIImage systemImageNamed:@"person.3.fill"]; + createGroup.accessibilityLabel = @"Create contact group"; [createGroup setAction:@selector(openCreateGroup:)]; [createGroup setTarget:self]; self.navigationItem.rightBarButtonItems = @[addContact, [[UIBarButtonItem alloc] init], createGroup]; From 7c52fab6f74ced93c0c77089daf368b934ea2670 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Mon, 15 Jul 2024 01:58:13 +0200 Subject: [PATCH 24/35] Improve onboarding cards display on macOS and iPad --- Monal/Classes/ActiveChatsViewController.m | 11 ++++++++--- Monal/Classes/BoardingCards.swift | 9 +++++++-- Monal/Classes/MLXMPPManager.m | 3 +++ 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/Monal/Classes/ActiveChatsViewController.m b/Monal/Classes/ActiveChatsViewController.m index ba7e9dd168..3d9f8a201c 100755 --- a/Monal/Classes/ActiveChatsViewController.m +++ b/Monal/Classes/ActiveChatsViewController.m @@ -569,9 +569,14 @@ -(void) showGeneralSettings -(void) showOnboarding { [self dismissCompleteViewChainWithAnimation:NO andCompletion:^{ - UIViewController* callViewController = [[SwiftuiInterface new] makeViewWithName:@"OnboardingView"]; - callViewController.modalPresentationStyle = UIModalPresentationFullScreen; - [self presentViewController:callViewController animated:NO completion:^{}]; + UIViewController* view = [[SwiftuiInterface new] makeViewWithName:@"OnboardingView"]; + if(UIDevice.currentDevice.userInterfaceIdiom != UIUserInterfaceIdiomPad) + view.modalPresentationStyle = UIModalPresentationFullScreen; + else + view.ml_disposeCallback = ^{ + [self sheetDismissed]; + }; + [self presentViewController:view animated:NO completion:^{}]; }]; } diff --git a/Monal/Classes/BoardingCards.swift b/Monal/Classes/BoardingCards.swift index e0331a6f12..4cdfe62003 100644 --- a/Monal/Classes/BoardingCards.swift +++ b/Monal/Classes/BoardingCards.swift @@ -126,6 +126,11 @@ struct OnboardingView: View { } } } + .onAppear { + //force portrait mode and lock ui there + UIDevice.current.setValue(UIInterfaceOrientation.portrait.rawValue, forKey: "orientation") + (UIApplication.shared.delegate as! MonalAppDelegate).orientationLock = .portrait + } } } @@ -134,10 +139,10 @@ func createOnboardingView(delegate: SheetDismisserProtocol) -> some View { let cards = [ OnboardingCard( title: Text("Welcome to Monal !"), - description: Text("Privacy like its 1999 🔒"), + description: Text("Become part of a worldwide decentralized chat network!"), imageName: "hand.wave", articleText: Text(""" - Modern iOS and MacOS XMPP chat client. + Modern iOS and macOS XMPP chat client.\n\nXMPP is a federated network: Just like email, you can register your account on many servers and still talk to anyone, even if they signed up on a different server. """), customView: nil ), diff --git a/Monal/Classes/MLXMPPManager.m b/Monal/Classes/MLXMPPManager.m index 6b0aa1295d..b2d2ad1297 100644 --- a/Monal/Classes/MLXMPPManager.m +++ b/Monal/Classes/MLXMPPManager.m @@ -147,7 +147,10 @@ -(void) defaultSettings [self upgradeBoolUserSettingsIfUnset:@"hasCompletedOnboarding" toDefault:NO]; +//always show onboarding on simulator for now +#if TARGET_OS_SIMULATOR [[HelperTools defaultsDB] setBool:NO forKey:@"hasCompletedOnboarding"]; +#endif } -(void) upgradeFloatUserSettingsToInteger:(NSString*) settingsName From 21856fe062deb8223bfd8363dfcd55b232a3ab29 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Tue, 16 Jul 2024 22:56:49 +0200 Subject: [PATCH 25/35] Promisify loading overlay --- Monal/Classes/AddContactMenu.swift | 22 ++++---- Monal/Classes/ContactDetails.swift | 20 +++++--- Monal/Classes/DebugView.swift | 16 ++---- Monal/Classes/LoadingOverlay.swift | 71 +++++++++++++++++++++----- Monal/Classes/MemberList.swift | 82 +++++++++++++++--------------- Monal/Classes/SwiftHelpers.swift | 36 +++++++++++++ Monal/Classes/SwiftuiHelpers.swift | 12 +++-- Monal/Classes/WelcomeLogIn.swift | 12 ++--- Monal/Classes/xmpp.h | 3 +- Monal/Classes/xmpp.m | 46 +++++++++-------- 10 files changed, 202 insertions(+), 118 deletions(-) diff --git a/Monal/Classes/AddContactMenu.swift b/Monal/Classes/AddContactMenu.swift index 94011b2dc9..dbe2d5d898 100644 --- a/Monal/Classes/AddContactMenu.swift +++ b/Monal/Classes/AddContactMenu.swift @@ -134,10 +134,11 @@ struct AddContactMenu: View { } return } - showLoadingOverlay(overlay, headline: NSLocalizedString("Adding...", comment: "")) - account.checkJidType(jid, withCompletion: { type, errorMsg in + showPromisingLoadingOverlay(overlay, headline:NSLocalizedString("Adding...", comment: ""), description:"") { + account.checkJidType(jid) + }.done { type in + let type = type as! String if type == "account" { - hideLoadingOverlay(overlay) let contact = MLContact.createContact(fromJid: jid, andAccountNo: account.accountNo) self.newContact = contact MLXMPPManager.sharedInstance().add(contact, withPreauthToken:preauthToken) @@ -145,21 +146,20 @@ struct AddContactMenu: View { trustFingerprints(self.importScannedFingerprints ? self.scannedFingerprints : [:], for:jid, on:account) successAlert(title: Text("Permission Requested"), message: Text("The new contact will be added to your contacts list when the person you've added has approved your request.")) } else if type == "muc" { - performMucAction(account:account, mucJid:jid, overlay:overlay, headlineView:Text("Adding Group/Channel..."), descriptionView:Text("")) { - account.joinMuc(jid) + showPromisingLoadingOverlay(overlay, headlineView:Text("Adding Group/Channel..."), descriptionView:Text("")) { + promisifyMucAction(account:account, mucJid:jid) { + 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) } - } else { - hideLoadingOverlay(overlay) - errorAlert(title: Text("Error"), message: Text(errorMsg ?? "Undefined error")) } - }) + }.catch { error in + errorAlert(title: Text("Error"), message: Text(error.localizedDescription)) + } } var body: some View { diff --git a/Monal/Classes/ContactDetails.swift b/Monal/Classes/ContactDetails.swift index ab67fae0d9..87ce059d64 100644 --- a/Monal/Classes/ContactDetails.swift +++ b/Monal/Classes/ContactDetails.swift @@ -149,8 +149,10 @@ struct ContactDetails: View { .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) + showPromisingLoadingOverlay(overlay, headlineView:Text("Removing avatar..."), descriptionView:Text("")) { + promisifyMucAction(account:account, mucJid:contact.contactJid) { + self.account.mucProcessor.publishAvatar(nil, forMuc: contact.contactJid) + } }.catch { error in errorAlert(title: Text("Error removing avatar!"), message: Text("\(String(describing:error))")) hideLoadingOverlay(overlay) @@ -542,8 +544,10 @@ struct ContactDetails: View { .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) + showPromisingLoadingOverlay(overlay, headlineView:contact.mucType == "group" ? Text("Destroying group...") : Text("Destroying channel..."), descriptionView:Text("")) { + promisifyMucAction(account:account, mucJid:contact.contactJid) { + self.account.mucProcessor.destroyRoom(contact.contactJid as String) + } }.done { callback in if let callback = callback { self.successCallback = callback @@ -551,8 +555,6 @@ struct ContactDetails: View { 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) } } ) @@ -655,8 +657,10 @@ struct ContactDetails: View { }, onCanceled: { inputImage = nil }) { (image, cropRect, angle) in - performMucAction(account:account, mucJid:contact.contactJid, overlay:overlay, headlineView:Text("Uploading avatar..."), descriptionView:Text("")) { - self.account.mucProcessor.publishAvatar(image, forMuc: contact.contactJid) + showPromisingLoadingOverlay(overlay, headlineView:Text("Uploading avatar..."), descriptionView:Text("")) { + promisifyMucAction(account:account, mucJid:contact.contactJid) { + self.account.mucProcessor.publishAvatar(image, forMuc: contact.contactJid) + } }.catch { error in errorAlert(title: Text("Error changing avatar!"), message: Text("\(String(describing:error))")) hideLoadingOverlay(overlay) diff --git a/Monal/Classes/DebugView.swift b/Monal/Classes/DebugView.swift index 7fe70c6121..33f851f3b1 100644 --- a/Monal/Classes/DebugView.swift +++ b/Monal/Classes/DebugView.swift @@ -177,8 +177,8 @@ struct CrashTestingView: View { } struct DebugView: View { - @State private var isReconnecting: Bool = false @StateObject private var overlay = LoadingOverlayState() + var body: some View { TabView { LogFilesView() @@ -200,18 +200,10 @@ struct DebugView: View { .frame(maxWidth: .infinity, maxHeight: .infinity) .padding() .addLoadingOverlay(overlay) - .onChange(of: isReconnecting) { _ in - if isReconnecting{ - showLoadingOverlay(overlay, headline: "Reconnecting", description: "Will log out and reconnect all (connected) accounts.") - } else { - hideLoadingOverlay(overlay) - } - } .navigationBarItems(trailing:Button("Reconnect All") { - isReconnecting = true - MLXMPPManager.sharedInstance().reconnectAll() - DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) { - isReconnecting = false + showLoadingOverlay(overlay, headline: "Reconnecting", description: "Will log out and reconnect all (connected) accounts.") { + MLXMPPManager.sharedInstance().reconnectAll() + return after(seconds:3.0) } }) } diff --git a/Monal/Classes/LoadingOverlay.swift b/Monal/Classes/LoadingOverlay.swift index 89fbbbe801..e40a5e0e6c 100644 --- a/Monal/Classes/LoadingOverlay.swift +++ b/Monal/Classes/LoadingOverlay.swift @@ -52,8 +52,8 @@ extension View { } } -func showLoadingOverlay(_ overlay: LoadingOverlayState, headlineView headline: T1, descriptionView description: T2) { - DispatchQueue.main.async { +func showPromisingLoadingOverlay(_ overlay: LoadingOverlayState, headlineView headline: T1, descriptionView description: T2) -> Guarantee { + return HelperTools.wait(atLeastSeconds:1.0, for:AnyPromise(DispatchQueue.main.async(.promise) { overlay.headline = AnyView(headline) overlay.description = AnyView(description) overlay.enabled = true @@ -63,23 +63,70 @@ func showLoadingOverlay(_ overlay: LoadingOverlayState, headli DispatchQueue.main.asyncAfter(deadline: .now() + 0.250) { overlay.objectWillChange.send() } + })).toGuarantee() +} + +func showPromisingLoadingOverlay(_ overlay: LoadingOverlayState, headline: T, description: T = "") -> Guarantee { + return showPromisingLoadingOverlay(overlay, headlineView:Text(headline), descriptionView:Text(description)) +} + +func showPromisingLoadingOverlay(_ overlay: LoadingOverlayState, headlineView headline: T1, descriptionView description: T2, firstlyClosure: @escaping () -> U) -> Promise { + return Promise { seal in + showPromisingLoadingOverlay(overlay, headlineView: headline, descriptionView: description).done { + let _ = firstlyClosure().done { value in + hideLoadingOverlay(overlay) + seal.fulfill(value) + }.catch { error in + hideLoadingOverlay(overlay) + seal.reject(error) + } + } } } -func showLoadingOverlay(_ overlay: LoadingOverlayState, headline: T, description: T = "") { - DispatchQueue.main.async { - overlay.headline = AnyView(Text(headline)) - overlay.description = AnyView(Text(description)) - 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() +func showPromisingLoadingOverlay(_ overlay: LoadingOverlayState, headlineView headline: T1, descriptionView description: T2, firstlyClosure: @escaping () -> U) -> Guarantee { + return Guarantee { seal in + showPromisingLoadingOverlay(overlay, headlineView: headline, descriptionView: description).done { + let _ = firstlyClosure().finally { + hideLoadingOverlay(overlay) + seal(()) + } } } } +func showPromisingLoadingOverlay(_ overlay: LoadingOverlayState, headline: T, description: T = "", firstlyClosure: @escaping () -> U) -> Promise { + return showPromisingLoadingOverlay(overlay, headlineView:Text(headline), descriptionView:Text(description), firstlyClosure:firstlyClosure) +} + +func showPromisingLoadingOverlay(_ overlay: LoadingOverlayState, headline: T, description: T = "", firstlyClosure: @escaping () -> U) -> Guarantee { + return showPromisingLoadingOverlay(overlay, headlineView:Text(headline), descriptionView:Text(description), firstlyClosure:firstlyClosure) +} + +func showLoadingOverlay(_ overlay: LoadingOverlayState, headlineView headline: T1, descriptionView description: T2) { + let _ = showPromisingLoadingOverlay(overlay, headlineView:headline, descriptionView:description) +} + +func showLoadingOverlay(_ overlay: LoadingOverlayState, headline: T, description: T = "") { + let _ = showPromisingLoadingOverlay(overlay, headline:headline, description:description) +} + +func showLoadingOverlay(_ overlay: LoadingOverlayState, headlineView headline: T1, descriptionView description: T2, firstlyClosure: @escaping () -> U) { + let _ = showPromisingLoadingOverlay(overlay, headlineView:headline, descriptionView:description, firstlyClosure:firstlyClosure) +} + +func showLoadingOverlay(_ overlay: LoadingOverlayState, headlineView headline: T1, descriptionView description: T2, firstlyClosure: @escaping () -> U) { + let _ = showPromisingLoadingOverlay(overlay, headlineView:headline, descriptionView:description, firstlyClosure:firstlyClosure) +} + +func showLoadingOverlay(_ overlay: LoadingOverlayState, headline: T, description: T = "", firstlyClosure: @escaping () -> U) { + let _ = showPromisingLoadingOverlay(overlay, headline:headline, description:description, firstlyClosure:firstlyClosure) +} + +func showLoadingOverlay(_ overlay: LoadingOverlayState, headline: T, description: T = "", firstlyClosure: @escaping () -> U) { + let _ = showPromisingLoadingOverlay(overlay, headline:headline, description:description, firstlyClosure:firstlyClosure) +} + func hideLoadingOverlay(_ overlay: LoadingOverlayState) { DispatchQueue.main.async { overlay.headline = AnyView(Text("")) diff --git a/Monal/Classes/MemberList.swift b/Monal/Classes/MemberList.swift index 8d917f56c6..091e98e3df 100644 --- a/Monal/Classes/MemberList.swift +++ b/Monal/Classes/MemberList.swift @@ -83,8 +83,8 @@ struct MemberList: View { } } - 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 promisifyAction(action: @escaping ()->Void) -> Promise { + return promisifyMucAction(account:self.account, mucJid:self.muc.contactJid, action:action) } func showAlert(title: Text, description: Text) { @@ -172,46 +172,45 @@ struct MemberList: View { 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) + var outcastResolution: Promise = Promise.value(nil) + if affiliations[contact] == "outcast" { + outcastResolution = showPromisingLoadingOverlay(self.overlay, headlineView: Text("Unblocking user"), descriptionView: Text("Unblocking user for this group/channel: \(contact.contactJid as String)")) { + promisifyAction { + account.mucProcessor.setAffiliation(self.muc.mucType == "group" ? "member" : "none", ofUser:contact.contactJid, inMuc:self.muc.contactJid) } } - .recover { error in + } + outcastResolution.then { _ in + showPromisingLoadingOverlay(self.overlay, headlineView: Text("Inviting user"), descriptionView: Text("Inviting user to this group/channel: \(contact.contactJid as String)")) { + promisifyAction { + account.mucProcessor.inviteUser(contact.contactJid, inMuc: self.muc.contactJid) + } + }.catch { error in showAlert(title:Text("Error inviting user!"), description:Text("\(String(describing:error))")) - return Guarantee.value(nil as monal_void_block_t?) } + return Guarantee.value(()) }.catch { error in showAlert(title:Text("Error unblocking user!"), description:Text("\(String(describing:error))")) - }.finally { - hideLoadingOverlay(overlay) } } else if newAffiliation == "outcast" { showActionSheet(title: Text("Block user?"), description: Text("Do you want to block this user from entering this group/channel?")) { DDLogVerbose("Changing affiliation of \(String(describing:contact)) to: \(String(describing:newAffiliation))...") - performAction(headlineView: Text("Blocking member"), descriptionView: Text("Blocking \(contact.contactJid as String)")) { - account.mucProcessor.setAffiliation(newAffiliation, ofUser:contact.contactJid, inMuc:self.muc.contactJid) + showPromisingLoadingOverlay(self.overlay, headlineView: Text("Blocking member"), descriptionView: Text("Blocking \(contact.contactJid as String)")) { + promisifyAction { + account.mucProcessor.setAffiliation(newAffiliation, ofUser:contact.contactJid, inMuc:self.muc.contactJid) + } }.catch { error in showAlert(title:Text("Error blocking user!"), description:Text("\(String(describing:error))")) - }.finally { - hideLoadingOverlay(overlay) } } } else { DDLogVerbose("Changing affiliation of \(String(describing:contact)) to: \(String(describing:newAffiliation))...") - performAction(headlineView: Text("Changing affiliation"), descriptionView: - Text("Changing affiliation to \(mucAffiliationToString(affiliations[contact])): \(contact.contactJid as String)")) { - account.mucProcessor.setAffiliation(newAffiliation, ofUser:contact.contactJid, inMuc:self.muc.contactJid) + showPromisingLoadingOverlay(self.overlay, headlineView: Text("Changing affiliation"), descriptionView: Text("Changing affiliation to \(mucAffiliationToString(affiliations[contact])): \(contact.contactJid as String)")) { + promisifyAction { + 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) } } } @@ -230,27 +229,28 @@ struct MemberList: View { 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 + showPromisingLoadingOverlay(self.overlay, headlineView: Text("Adding new member"), descriptionView: Text("Adding \(member.contactJid as String)...")) { + promisifyAction { + account.mucProcessor.setAffiliation("member", ofUser:member.contactJid, inMuc:self.muc.contactJid) + } + }.done { _ in + showPromisingLoadingOverlay(self.overlay, headlineView: Text("Inviting new member"), descriptionView: Text("Adding \(member.contactJid as String)...")) { + promisifyAction { + account.mucProcessor.inviteUser(member.contactJid, inMuc: self.muc.contactJid) + } + }.catch { 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) + showPromisingLoadingOverlay(self.overlay, headlineView: Text("Inviting new participant"), descriptionView: Text("Adding \(member.contactJid as String)...")) { + promisifyAction { + 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) } } } @@ -301,12 +301,12 @@ struct MemberList: View { .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) + showPromisingLoadingOverlay(self.overlay, headlineView: Text("Removing \(mucAffiliationToString(affiliations[member]))"), descriptionView: Text("Removing \(member.contactJid as String)...")) { + promisifyAction { + 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) } } }) diff --git a/Monal/Classes/SwiftHelpers.swift b/Monal/Classes/SwiftHelpers.swift index 0756b7dc18..2be6fb5ddc 100644 --- a/Monal/Classes/SwiftHelpers.swift +++ b/Monal/Classes/SwiftHelpers.swift @@ -221,6 +221,42 @@ struct RuntimeError: LocalizedError { } } +extension AnyPromise { + public func toGuarantee() -> Guarantee { + return Guarantee { seal in + self.done { value in + if let value = value as? T { + seal(value) + } else { + HelperTools.throwException(withName:"AnyPromiseConversionError", reason:"Could not cast value to type \(String(describing: T.self))", userInfo:[ + "type": "\(String(describing: T.self))", + "promise": "\(String(describing: self))", + ]) + } + }.catch { error in + HelperTools.throwException(withName:"AnyPromiseConversionError", reason:"Uncatched promise error: \(error)", userInfo:[ + "error": "\(String(describing:error))", + "promise": "\(String(describing: self))", + ]) + } + } + } + + public func toPromise() -> Promise { + return Promise { seal in + self.done { value in + if let value = value as? T { + seal.fulfill(value) + } else { + seal.reject(PMKError.invalidCallingConvention) + } + }.catch { error in + seal.reject(error) + } + } + } +} + //see https://www.avanderlee.com/swift/property-wrappers/ //and https://fatbobman.com/en/posts/adding-published-ability-to-custom-property-wrapper-types/ @propertyWrapper diff --git a/Monal/Classes/SwiftuiHelpers.swift b/Monal/Classes/SwiftuiHelpers.swift index 20b5c762e8..df3f4e82c4 100644 --- a/Monal/Classes/SwiftuiHelpers.swift +++ b/Monal/Classes/SwiftuiHelpers.swift @@ -88,10 +88,9 @@ func getContactList(viewContact: (ObservableKVOWrapper?)) -> OrderedS } } -func performMucAction(account: xmpp, mucJid: String, overlay: LoadingOverlayState, headlineView: Optional, descriptionView: Optional, action: @escaping ()->Void) -> Promise { - showLoadingOverlay(overlay, headlineView:headlineView, descriptionView:descriptionView) +func promisifyMucAction(account: xmpp, mucJid: String, action: @escaping () throws -> Void) -> Promise { return Promise { seal in - DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 1.0) { + DispatchQueue.global(qos: .background).async { account.mucProcessor.addUIHandler({_data in let data = _data as! NSDictionary let success : Bool = data["success"] as! Bool; if !success { @@ -104,7 +103,12 @@ func performMucAction(account: xmpp, mucJid: String, overlay: LoadingOverlayStat } } }, forMuc:mucJid) - action() + do { + try action() + } catch { + seal.reject(error) + } + } } } diff --git a/Monal/Classes/WelcomeLogIn.swift b/Monal/Classes/WelcomeLogIn.swift index 0379ee5d73..4aaaed40db 100644 --- a/Monal/Classes/WelcomeLogIn.swift +++ b/Monal/Classes/WelcomeLogIn.swift @@ -264,13 +264,11 @@ struct WelcomeLogIn: View { if let notificationAccountNo = notification.userInfo?["accountNo"] as? NSNumber, let completed = notification.userInfo?["completed"] as? NSNumber, let all = notification.userInfo?["all"] as? NSNumber, let newAccountNo : NSNumber = self.newAccountNo { if(notificationAccountNo.intValue == newAccountNo.intValue) { isLoadingOmemoBundles = true - DispatchQueue.main.async { - showLoadingOverlay( - overlay, - headline:NSLocalizedString("Loading omemo bundles", comment: ""), - description:String(format: NSLocalizedString("Loading omemo bundles: %@ / %@", comment: ""), completed, all) - ) - } + showLoadingOverlay( + overlay, + headline:NSLocalizedString("Loading omemo bundles", comment: ""), + description:String(format: NSLocalizedString("Loading omemo bundles: %@ / %@", comment: ""), completed, all) + ) } } } diff --git a/Monal/Classes/xmpp.h b/Monal/Classes/xmpp.h index 5aa9e8bbdc..ff6ec897e3 100644 --- a/Monal/Classes/xmpp.h +++ b/Monal/Classes/xmpp.h @@ -46,6 +46,7 @@ FOUNDATION_EXPORT NSString* const kFileName; FOUNDATION_EXPORT NSString* const kContentType; FOUNDATION_EXPORT NSString* const kData; +@class AnyPromise; @class MLPubSub; @class MLXMLNode; @class XMPPDataForm; @@ -159,7 +160,7 @@ typedef void (^monal_iq_handler_t)(XMPPIQ* _Nullable); -(void) updateRosterItem:(MLContact*) contact withName:(NSString*) name; --(void) checkJidType:(NSString*) jid withCompletion:(void (^)(NSString* type, NSString* _Nullable errorMessage)) completion; +-(AnyPromise*) checkJidType:(NSString*) jid; /** join a room on the conference server diff --git a/Monal/Classes/xmpp.m b/Monal/Classes/xmpp.m index 2072513e2e..bcbd5f24ff 100644 --- a/Monal/Classes/xmpp.m +++ b/Monal/Classes/xmpp.m @@ -4441,28 +4441,30 @@ -(void) leaveMuc:(NSString* _Nonnull) room [self.mucProcessor leave:room withBookmarksUpdate:YES keepBuddylistEntry:NO]; } --(void) checkJidType:(NSString*) jid withCompletion:(void (^)(NSString* type, NSString* _Nullable errorMessage)) completion -{ - XMPPIQ* discoInfo = [[XMPPIQ alloc] initWithType:kiqGetType]; - [discoInfo setiqTo:jid]; - [discoInfo setDiscoInfoNode]; - [self sendIq:discoInfo withResponseHandler:^(XMPPIQ* response) { - NSSet* features = [NSSet setWithArray:[response find:@"{http://jabber.org/protocol/disco#info}query/feature@var"]]; - //check if this is a muc or account - if([features containsObject:@"http://jabber.org/protocol/muc"]) - return completion(@"muc", nil); - else - return completion(@"account", nil); - } andErrorHandler:^(XMPPIQ* error) { - //this means the jid is an account which can not be queried if not subscribed - if([error check:@"//error/{urn:ietf:params:xml:ns:xmpp-stanzas}service-unavailable"]) - return completion(@"account", nil); - else if([error check:@"//error/{urn:ietf:params:xml:ns:xmpp-stanzas}subscription-required"]) - return completion(@"account", nil); - //any other error probably means the remote server is not reachable or (even more likely) the jid is incorrect - NSString* errorDescription = [HelperTools extractXMPPError:error withDescription:NSLocalizedString(@"Unexpected error while checking type of jid:", @"")]; - DDLogError(@"checkJidType got an error, informing user: %@", errorDescription); - return completion(@"error", error == nil ? NSLocalizedString(@"Unexpected error while checking type of jid, please try again", @"") : errorDescription); +-(AnyPromise*) checkJidType:(NSString*) jid +{ + return [AnyPromise promiseWithResolverBlock:^(PMKResolver resolve) { + XMPPIQ* discoInfo = [[XMPPIQ alloc] initWithType:kiqGetType]; + [discoInfo setiqTo:jid]; + [discoInfo setDiscoInfoNode]; + [self sendIq:discoInfo withResponseHandler:^(XMPPIQ* response) { + NSSet* features = [NSSet setWithArray:[response find:@"{http://jabber.org/protocol/disco#info}query/feature@var"]]; + //check if this is a muc or account + if([features containsObject:@"http://jabber.org/protocol/muc"]) + return resolve(@"muc"); + else + return resolve(@"account"); + } andErrorHandler:^(XMPPIQ* error) { + //this means the jid is an account which can not be queried if not subscribed + if([error check:@"//error/{urn:ietf:params:xml:ns:xmpp-stanzas}service-unavailable"]) + return resolve(@"account"); + else if([error check:@"//error/{urn:ietf:params:xml:ns:xmpp-stanzas}subscription-required"]) + return resolve(@"account"); + //any other error probably means the remote server is not reachable or (even more likely) the jid is incorrect + NSString* errorDescription = [HelperTools extractXMPPError:error withDescription:NSLocalizedString(@"Unexpected error while checking type of jid:", @"")]; + DDLogError(@"checkJidType got an error, informing user: %@", errorDescription); + resolve([NSError errorWithDomain:@"Monal" code:0 userInfo:@{NSLocalizedDescriptionKey: error == nil ? NSLocalizedString(@"Unexpected error while checking type of jid, please try again", @"") : errorDescription}]); + }]; }]; } From d179ea46b718eed90f9cdb3bd434252f8adc75a6 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Wed, 17 Jul 2024 22:25:15 +0200 Subject: [PATCH 26/35] Add new waitAtLeastSeconds:forPromise: helper tools method --- Monal/Classes/HelperTools.h | 3 +++ Monal/Classes/HelperTools.m | 7 +++++++ 2 files changed, 10 insertions(+) diff --git a/Monal/Classes/HelperTools.h b/Monal/Classes/HelperTools.h index 85e25e5b7d..cabe9eca55 100644 --- a/Monal/Classes/HelperTools.h +++ b/Monal/Classes/HelperTools.h @@ -28,6 +28,7 @@ NS_ASSUME_NONNULL_BEGIN +@class AnyPromise; @class MLXMLNode; @class xmpp; @class XMPPStanza; @@ -180,6 +181,8 @@ void swizzle(Class c, SEL orig, SEL new); +(UIView*) MLCustomViewHeaderWithTitle:(NSString*) title; +(CIImage*) createQRCodeFromString:(NSString*) input; ++(AnyPromise*) waitAtLeastSeconds:(NSTimeInterval) seconds forPromise:(AnyPromise*) promise; + //don't use these four directly, but via createTimer() makro +(MLDelayableTimer*) startDelayableQueuedTimer:(double) timeout withHandler:(monal_void_block_t) handler andCancelHandler:(monal_void_block_t _Nullable) cancelHandler andFile:(char*) file andLine:(int) line andFunc:(char*) func onQueue:(dispatch_queue_t _Nullable) queue; +(monal_void_block_t) startQueuedTimer:(double) timeout withHandler:(monal_void_block_t) handler andCancelHandler:(monal_void_block_t _Nullable) cancelHandler andFile:(char*) file andLine:(int) line andFunc:(char*) func onQueue:(dispatch_queue_t _Nullable) queue; diff --git a/Monal/Classes/HelperTools.m b/Monal/Classes/HelperTools.m index 1b56af12b2..9d690661fb 100644 --- a/Monal/Classes/HelperTools.m +++ b/Monal/Classes/HelperTools.m @@ -2309,6 +2309,13 @@ +(monal_void_block_t) startQueuedTimer:(double) timeout withHandler:(monal_void_ }; } ++(AnyPromise*) waitAtLeastSeconds:(NSTimeInterval) seconds forPromise:(AnyPromise*) promise +{ + return PMKWhen(@[promise, PMKAfter(seconds)]).then(^{ + return promise; + }); +} + +(NSString*) encodeRandomResource { u_int32_t i=arc4random(); From 3fb81845769c8880fa542d279c132fc5f8ef56c9 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Wed, 17 Jul 2024 22:23:55 +0200 Subject: [PATCH 27/35] Improve text on boarding cards and general settings --- Monal/Classes/BoardingCards.swift | 20 ++++++++++++++------ Monal/Classes/GeneralSettings.swift | 6 +++--- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/Monal/Classes/BoardingCards.swift b/Monal/Classes/BoardingCards.swift index 4cdfe62003..20524d1a35 100644 --- a/Monal/Classes/BoardingCards.swift +++ b/Monal/Classes/BoardingCards.swift @@ -76,8 +76,12 @@ struct OnboardingView: View { .multilineTextAlignment(.leading) /// This ensures text doesn't get truncated which sometimes happens in ScrollView .fixedSize(horizontal: false, vertical: true) - + } + + if card.imageName != nil || card.description != nil || card.imageName != nil { + Spacer().frame(height: 1) Divider() + Spacer().frame(height: 1) } card.articleText? @@ -148,16 +152,20 @@ func createOnboardingView(delegate: SheetDismisserProtocol) -> some View { ), OnboardingCard( title: Text("Features"), - description: Text("Here's a quick look at what you can expect:"), + description: nil, imageName: "sparkles", articleText: Text(""" - • 🔐 OMEMO Encryption : Secure multi-end messaging using the OMEMO protocol. + 🛜 Decentralized Network : + Leverages the decentralized nature of XMPP, avoiding central servers. - • 🛜 Decentralized Network : Leverages the decentralized nature of XMPP, avoiding central servers. + 🌐 Data privacy : + We do not sell or track information for external parties (nor for anyone else). - • 🌐 Data privacy : We do not sell or track information for external parties (nor for anyone else). + 🔐 End-to-end encryption : + Secure multi-end messaging using the OMEMO protocol. - • 👨‍💻 Open Source : The app's source code is publicly available for audit and contribution. + 👨‍💻 Open Source : + The app's source code is publicly available for audit and contribution. """), customView: nil ), diff --git a/Monal/Classes/GeneralSettings.swift b/Monal/Classes/GeneralSettings.swift index b853145224..20f06a990e 100644 --- a/Monal/Classes/GeneralSettings.swift +++ b/Monal/Classes/GeneralSettings.swift @@ -361,11 +361,11 @@ struct PrivacySettingsSubview: View { if onboardingPart == -1 || onboardingPart == 0 { Section(header: Text("Activity indications")) { SettingsToggle(isOn: $generalSettingsDefaultsDB.sendReceivedMarkers) { - Text("Send message received") + Text("Send message receipts") Text("Let your contacts know if you received a message.") } SettingsToggle(isOn: $generalSettingsDefaultsDB.sendDisplayedMarkers) { - Text("Send message displayed state") + Text("Send read receipts") Text("Let your contacts know if you read a message.") } SettingsToggle(isOn: $generalSettingsDefaultsDB.sendLastChatState) { @@ -515,6 +515,6 @@ struct AttachmentSettings: View { struct ContentView_Previews: PreviewProvider { static var previews: some View { - PrivacySettings() + GeneralSettings() } } From d2b1eb7f5dae77af49af0be56a21b2733afaf6b8 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Fri, 19 Jul 2024 02:53:23 +0200 Subject: [PATCH 28/35] Improve onboarding flow further --- Monal/Classes/ActiveChatsViewController.m | 28 +++++++++++------------ Monal/Classes/BoardingCards.swift | 8 ++++--- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/Monal/Classes/ActiveChatsViewController.m b/Monal/Classes/ActiveChatsViewController.m index 3d9f8a201c..30f676cb0b 100755 --- a/Monal/Classes/ActiveChatsViewController.m +++ b/Monal/Classes/ActiveChatsViewController.m @@ -39,7 +39,7 @@ @interface ActiveChatsViewController() { int _startedOrientation; double _portraitTop; double _landscapeTop; - BOOL _loginAutodisplayedAlready; + BOOL _loginAlreadyAutodisplayed; } @property (atomic, strong) NSMutableArray* unpinnedContacts; @property (atomic, strong) NSMutableArray* pinnedContacts; @@ -59,18 +59,13 @@ @implementation ActiveChatsViewController +(void) initialize { + DDLogDebug(@"initializing active chats class"); _mamWarningDisplayed = [NSMutableSet new]; _smacksWarningDisplayed = [NSMutableSet new]; _pushWarningDisplayed = [NSMutableSet new]; } #pragma mark view lifecycle --(id) initWithNibName:(NSString*) nibNameOrNil bundle:(NSBundle*) nibBundleOrNil -{ - self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; - _loginAutodisplayedAlready = NO; - return self; -} -(void) configureComposeButton { @@ -89,8 +84,10 @@ -(void) configureComposeButton -(void) viewDidLoad { + DDLogDebug(@"active chats view did load"); [super viewDidLoad]; + _loginAlreadyAutodisplayed = NO; _startedOrientation = 0; self.spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleMedium]; @@ -136,6 +133,7 @@ -(void) viewDidLoad -(void) dealloc { + DDLogDebug(@"active chats dealloc"); [[NSNotificationCenter defaultCenter] removeObserver:self]; } @@ -402,16 +400,16 @@ -(void) viewDidAppear:(BOOL) animated -(void) sheetDismissed { - dispatch_async(dispatch_get_main_queue(), ^{ - [self refresh]; - }); + [self refresh]; } -(void) refresh { - if(self.unpinnedContacts.count == 0 && self.pinnedContacts.count == 0) - [self refreshDisplay]; // load contacts - [self segueToIntroScreensIfNeeded]; + dispatch_async(dispatch_get_main_queue(), ^{ + if(self.unpinnedContacts.count == 0 && self.pinnedContacts.count == 0) + [self refreshDisplay]; // load contacts + [self segueToIntroScreensIfNeeded]; + }); } -(void) didReceiveMemoryWarning @@ -464,11 +462,11 @@ -(void) segueToIntroScreensIfNeeded } // display quick start if the user never seen it or if there are 0 enabled accounts - if([[DataLayer sharedInstance] enabledAccountCnts].intValue == 0 && !_loginAutodisplayedAlready) + if([[DataLayer sharedInstance] enabledAccountCnts].intValue == 0 && !_loginAlreadyAutodisplayed) { UIViewController* loginViewController = [[SwiftuiInterface new] makeViewWithName:@"WelcomeLogIn"]; loginViewController.ml_disposeCallback = ^{ - self->_loginAutodisplayedAlready = YES; + self->_loginAlreadyAutodisplayed = YES; [self sheetDismissed]; }; [self presentViewController:loginViewController animated:YES completion:^{}]; diff --git a/Monal/Classes/BoardingCards.swift b/Monal/Classes/BoardingCards.swift index 20524d1a35..c2d9153b4d 100644 --- a/Monal/Classes/BoardingCards.swift +++ b/Monal/Classes/BoardingCards.swift @@ -131,9 +131,11 @@ struct OnboardingView: View { } } .onAppear { - //force portrait mode and lock ui there - UIDevice.current.setValue(UIInterfaceOrientation.portrait.rawValue, forKey: "orientation") - (UIApplication.shared.delegate as! MonalAppDelegate).orientationLock = .portrait + if UIDevice.current.userInterfaceIdiom != .pad { + //force portrait mode and lock ui there + UIDevice.current.setValue(UIInterfaceOrientation.portrait.rawValue, forKey: "orientation") + (UIApplication.shared.delegate as! MonalAppDelegate).orientationLock = .portrait + } } } } From 2fd2543f71a7294e91dbd11d1769ec50305ea9c8 Mon Sep 17 00:00:00 2001 From: Matthew Fennell Date: Sat, 13 Jul 2024 00:06:43 +0100 Subject: [PATCH 29/35] Sort OmemoKeys known devices by device ID This is needed so that live updates when devices are added or removed happen with a predictable ordering. Without this, each time there is an update, the items in the list could shuffle around, making it difficult to see which is the new device that has been added. --- Monal/Classes/OmemoKeys.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Monal/Classes/OmemoKeys.swift b/Monal/Classes/OmemoKeys.swift index 09eea4dd70..14877e315b 100644 --- a/Monal/Classes/OmemoKeys.swift +++ b/Monal/Classes/OmemoKeys.swift @@ -211,10 +211,14 @@ struct OmemoKeysForContact: View { self.contactJid = contact.obj.contactJid self.account = account self.deviceId = account.omemo.getDeviceId() - self.deviceIds = OrderedSet(self.account.omemo.knownDevices(forAddressName: self.contactJid)) + self.deviceIds = OmemoKeysForContact.knownDevices(account: self.account, jid: self.contactJid) self.selectedDeviceForDeletion = -1 } + private static func knownDevices(account: xmpp, jid: String) -> OrderedSet { + return OrderedSet(account.omemo.knownDevices(forAddressName: jid).sorted { return $0.intValue < $1.intValue }) + } + func deleteButton(deviceId: NSNumber) -> some View { Button(action: { selectedDeviceForDeletion = deviceId // SwiftUI does not like to have deviceID nested in multiple functions, so safe this in the struct... From b39f4af446744199a3ab12fca3bf7a551003d7ad Mon Sep 17 00:00:00 2001 From: Matthew Fennell Date: Mon, 15 Jul 2024 23:27:56 +0100 Subject: [PATCH 30/35] Refresh known devices on updates from the server We add notifications to refresh the device list after any function call in MLOMEMO that could cause an update to contactDeviceId in MLSignalStore. We only pass the account in the notification, and rely on the view to retrieve the full list of devices for that account, so as to keep the view in sync with the database. This allows the view to self-correct if an update is missed; if we have missed a call to refresh and the view does not show a particular device, it will get picked up the next time that refresh is called with some other device update. --- Monal/Classes/MLConstants.h | 1 + Monal/Classes/MLOMEMO.m | 17 +++++++++++++++++ Monal/Classes/OmemoKeys.swift | 12 +++++++++++- 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/Monal/Classes/MLConstants.h b/Monal/Classes/MLConstants.h index 81148aed4f..f512d23fb7 100644 --- a/Monal/Classes/MLConstants.h +++ b/Monal/Classes/MLConstants.h @@ -164,6 +164,7 @@ static inline NSString* _Nonnull LocalizationNotNeeded(NSString* _Nonnull s) #define kMLResourceBoundNotice @"kMLResourceBoundNotice" #define kMonalFinishedCatchup @"kMonalFinishedCatchup" #define kMonalFinishedOmemoBundleFetch @"kMonalFinishedOmemoBundleFetch" +#define kMonalOmemoStateUpdated @"kMonalOmemoStateUpdated" #define kMonalUpdateBundleFetchStatus @"kMonalUpdateBundleFetchStatus" #define kMonalIdle @"kMonalIdle" #define kMonalFiletransfersIdle @"kMonalFiletransfersIdle" diff --git a/Monal/Classes/MLOMEMO.m b/Monal/Classes/MLOMEMO.m index 752696b827..8c60dc3a0d 100644 --- a/Monal/Classes/MLOMEMO.m +++ b/Monal/Classes/MLOMEMO.m @@ -101,6 +101,13 @@ -(OmemoState*) state return [NSSet setWithArray:[self.monalSignalStore knownDevicesForAddressName:addressName]]; } +-(void) notifyKnownDevicesUpdated:(NSString*) jid +{ + [[MLNotificationQueue currentQueue] postNotificationName:kMonalOmemoStateUpdated object:self.account userInfo:@{ + @"jid": jid + }]; +} + -(BOOL) createLocalIdentiyKeyPairIfNeeded { if(self.monalSignalStore.deviceid == 0) @@ -120,6 +127,7 @@ -(BOOL) createLocalIdentiyKeyPairIfNeeded //do everything done in MLSignalStore init not already mimicked above [self.monalSignalStore cleanupKeys]; [self.monalSignalStore reloadCachedPrekeys]; + [self notifyKnownDevicesUpdated:address.name]; //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) @@ -470,6 +478,8 @@ -(void) processOMEMODevices:(NSSet*) receivedDevices from:(NSString*) //handle our own devicelist if([self.account.connectionProperties.identity.jid isEqualToString:source]) [self handleOwnDevicelistUpdate:receivedDevices]; + else + [self notifyKnownDevicesUpdated:source]; } -(void) handleOwnDevicelistUpdate:(NSSet*) receivedDevices @@ -519,6 +529,8 @@ -(void) handleOwnDevicelistUpdate:(NSSet*) receivedDevices //publish own devicelist directly after publishing our bundle [self publishOwnDeviceList]; } + + [self notifyKnownDevicesUpdated:self.account.connectionProperties.identity.jid]; } -(void) publishOwnDeviceList @@ -759,6 +771,8 @@ -(void) processOMEMOKeys:(MLXMLNode*) item forJid:(NSString*) jid andRid:(NSNumb //found and imported a working key --> try to (re)build a new session proactively (or repair a broken one) [self sendKeyTransportElement:jid forRids:[NSSet setWithArray:@[rid]]]; //this will remove the queuedSessionRepairs entry, if any + [self notifyKnownDevicesUpdated:jid]; + return; } while(++processedKeys < preKeyIds.count); DDLogError(@"Could not import a single prekey from bundle for rid %@ (tried %lu keys)", rid, processedKeys); @@ -1307,6 +1321,7 @@ -(void) checkIfSessionIsStillNeeded:(NSString*) buddyJid isMuc:(BOOL) isMuc else if([self.monalSignalStore checkIfSessionIsStillNeeded:buddyJid] == NO) [danglingJids addObject:buddyJid]; + [self notifyKnownDevicesUpdated:buddyJid]; DDLogVerbose(@"Unsubscribing from dangling jids: %@", danglingJids); for(NSString* jid in danglingJids) [self.account.pubsub unsubscribeFromNode:@"eu.siacs.conversations.axolotl.devicelist" forJid:jid withHandler:$newHandler(self, handleDevicelistUnsubscribe)]; @@ -1328,6 +1343,7 @@ -(NSNumber*) getTrustLevel:(SignalAddress*) address identityKey:(NSData*) identi -(void) addIdentityManually:(SignalAddress*) address identityKey:(NSData* _Nonnull) identityKey { [self.monalSignalStore saveIdentity:address identityKey:identityKey]; + [self notifyKnownDevicesUpdated:address.name]; } -(void) updateTrust:(BOOL) trust forAddress:(SignalAddress*)address @@ -1370,6 +1386,7 @@ -(void) deleteDeviceForSource:(NSString*) source andRid:(NSNumber*) rid SignalAddress* address = [[SignalAddress alloc] initWithName:source deviceId:rid.unsignedIntValue]; [self.monalSignalStore deleteDeviceforAddress:address]; [self.monalSignalStore deleteSessionRecordForAddress:address]; + [self notifyKnownDevicesUpdated:address.name]; } //debug button in contactdetails ui diff --git a/Monal/Classes/OmemoKeys.swift b/Monal/Classes/OmemoKeys.swift index 14877e315b..61aabde4c0 100644 --- a/Monal/Classes/OmemoKeys.swift +++ b/Monal/Classes/OmemoKeys.swift @@ -219,6 +219,10 @@ struct OmemoKeysForContact: View { return OrderedSet(account.omemo.knownDevices(forAddressName: jid).sorted { return $0.intValue < $1.intValue }) } + private func refreshKnownDevices() -> Void { + self.deviceIds = OmemoKeysForContact.knownDevices(account: self.account, jid: self.contactJid) + } + func deleteButton(deviceId: NSNumber) -> some View { Button(action: { selectedDeviceForDeletion = deviceId // SwiftUI does not like to have deviceID nested in multiple functions, so safe this in the struct... @@ -237,7 +241,6 @@ struct OmemoKeysForContact: View { return // should be unreachable } account.omemo.deleteDevice(forSource: self.contactJid, andRid: self.selectedDeviceForDeletion) - self.deviceIds.remove(self.selectedDeviceForDeletion) }, secondaryButton: .cancel(Text("Abort")) ) @@ -257,6 +260,13 @@ struct OmemoKeysForContact: View { } } } + .onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("kMonalOmemoStateUpdated")).receive(on: RunLoop.main)) { notification in + if notification.userInfo?["jid"] as? String == self.contactJid { + withAnimation() { + refreshKnownDevices() + } + } + } } } From 599c666dc779bfc90202b147c1257a77669253f6 Mon Sep 17 00:00:00 2001 From: Matthew Fennell Date: Mon, 15 Jul 2024 23:28:47 +0100 Subject: [PATCH 31/35] Use existing method for OmemoKeysEntry trust level --- Monal/Classes/OmemoKeys.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Monal/Classes/OmemoKeys.swift b/Monal/Classes/OmemoKeys.swift index 61aabde4c0..aadcefbaf6 100644 --- a/Monal/Classes/OmemoKeys.swift +++ b/Monal/Classes/OmemoKeys.swift @@ -134,8 +134,7 @@ struct OmemoKeysEntry: View { let trustLevelBinding = Binding.init(get: { return (self.trustLevel.int32Value != MLOmemoNotTrusted) }, set: { keyEnabled in - self.account.omemo.updateTrust(keyEnabled, for: self.address) - self.trustLevel = self.account.omemo.getTrustLevel(self.address, identityKey: self.fingerprint) + setTrustLevel(keyEnabled) }) let fingerprintString = HelperTools.signalHexKeyWithSpaces(with: fingerprint) From 8f83a1ead1147c1c5e8f76c457690ec70309db7a Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Fri, 19 Jul 2024 03:44:17 +0200 Subject: [PATCH 32/35] Update develop-push workflow to post alpha releases to dedicated channel --- .github/workflows/develop-push.yml | 32 ++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/.github/workflows/develop-push.yml b/.github/workflows/develop-push.yml index aff248d8f8..fa9220bf1b 100644 --- a/.github/workflows/develop-push.yml +++ b/.github/workflows/develop-push.yml @@ -15,6 +15,10 @@ jobs: buildAndPublishAlpha: # The type of runner that the job will run on runs-on: ['ARM64', 'self-hosted'] + outputs: + id: ${{ steps.changelog.outputs.id }} + timestamp: ${{ steps.changelog.outputs.timestamp }} + message: ${{ steps.changelog.outputs.message }} env: APP_NAME: "Monal.alpha" APP_DIR: "Monal.alpha.app" @@ -52,6 +56,7 @@ jobs: tar -cf "../$APP_NAME.tar" "$APP_DIR" cd ../../../.. - name: save changelog + id: changelog env: ID: ${{github.event.head_commit.id}} TIMESTAMP: ${{github.event.head_commit.timestamp}} @@ -60,6 +65,12 @@ jobs: echo "ID: $ID" > changes.txt echo "Timestamp: $TIMESTAMP" >> changes.txt echo "$MESSAGE" >> changes.txt + + echo "id=$ID" >> "$GITHUB_OUTPUT" + echo "timestamp=$TIMESTAMP" >> "$GITHUB_OUTPUT" + echo "message<<__EOF__" >> "$GITHUB_OUTPUT" + echo "$MESSAGE" >> "$GITHUB_OUTPUT" + echo "__EOF__" >> "$GITHUB_OUTPUT" - name: Uploading to alpha site run: ./scripts/uploadAlpha.sh - name: Notarize catalyst @@ -91,3 +102,24 @@ jobs: # chmod +x ./scripts/updateLocalization.sh # chmod +x ./scripts/xliff_extractor.py # ./scripts/updateLocalization.sh NOCOMMIT + notifyMuc: + name: Notify support MUC about new Alpharelease + runs-on: ubuntu-latest + needs: [buildAndPublishAlpha] + steps: + - name: Notify + uses: monal-im/xmpp-notifier@master + with: # Set the secrets as inputs + jid: ${{ secrets.BOT_JID }} + password: ${{ secrets.BOT_PASSWORD }} + server_host: ${{ secrets.BOT_SERVER }} + recipient: monal-alpha@chat.yax.im + recipient_is_room: true + bot_alias: "Monal Release Bot" + message: | + New alpha build based on the following commit: + ${{ needs.buildAndPublishAlpha.outputs.id }} + ${{ needs.buildAndPublishAlpha.outputs.timestamp }} + ${{ needs.buildAndPublishAlpha.outputs.message }} + + Download page: https://downloads.monal-im.org/monal-im/alpha/ From 1583bb1d78640aca53ca68a0f434e40cafed221b Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Fri, 19 Jul 2024 06:44:05 +0200 Subject: [PATCH 33/35] Allow optionals in defaultsDB (read AND write) --- Monal/Classes/SwiftHelpers.swift | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/Monal/Classes/SwiftHelpers.swift b/Monal/Classes/SwiftHelpers.swift index 2be6fb5ddc..bd1e974f73 100644 --- a/Monal/Classes/SwiftHelpers.swift +++ b/Monal/Classes/SwiftHelpers.swift @@ -280,7 +280,15 @@ public struct defaultsDB { } } set { - container.set(newValue, forKey: key) + if let optional = newValue as? OptionalProtocol { + if optional.isSome() { + container.set(newValue, forKey: key) + } else { + container.removeObject(forKey:key) + } + } else { + container.set(newValue, forKey: key) + } container.synchronize() } } @@ -302,6 +310,27 @@ public struct defaultsDB { } } +//see https://stackoverflow.com/a/32780793 +protocol OptionalProtocol { + func isSome() -> Bool + func unwrap() -> Any +} +extension Optional : OptionalProtocol { + func isSome() -> Bool { + switch self { + case .none: return false + case .some: return true + } + } + + func unwrap() -> Any { + switch self { + // If a nil is unwrapped it will crash! + case .none: preconditionFailure("nil unwrap!") + case .some(let unwrapped): return unwrapped + } + } +} @objcMembers public class SwiftHelpers: NSObject { From f7f1324c19d625178977927bd7c9428319ca95ce Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Fri, 19 Jul 2024 06:43:28 +0200 Subject: [PATCH 34/35] Allow customized next button text for BoardingCards --- Monal/Classes/BoardingCards.swift | 22 ++++++++++++++-------- Monal/Classes/MLXMPPManager.m | 8 ++++---- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/Monal/Classes/BoardingCards.swift b/Monal/Classes/BoardingCards.swift index c2d9153b4d..d48489fa34 100644 --- a/Monal/Classes/BoardingCards.swift +++ b/Monal/Classes/BoardingCards.swift @@ -20,6 +20,7 @@ struct OnboardingCard: Identifiable { let imageName: String? let articleText: Text? let customView: AnyView? + let nextText: String? } struct OnboardingView: View { @@ -99,7 +100,7 @@ struct OnboardingView: View { currentIndex += 1 } label: { HStack { - Text("Next") + Text(card.nextText ?? NSLocalizedString("Next", comment:"onboarding")) .fontWeight(.bold) Image(systemName: "chevron.right") } @@ -107,9 +108,9 @@ struct OnboardingView: View { } else { Button { onboardingState.hasCompletedOnboarding = true - delegate.dismiss() + delegate.dismissWithoutAnimation() } label: { - Text("Close") + Text(card.nextText ?? NSLocalizedString("Close", comment:"onboarding")) .fontWeight(.bold) .padding() .background(Color.blue) @@ -150,7 +151,8 @@ func createOnboardingView(delegate: SheetDismisserProtocol) -> some View { articleText: Text(""" Modern iOS and macOS XMPP chat client.\n\nXMPP is a federated network: Just like email, you can register your account on many servers and still talk to anyone, even if they signed up on a different server. """), - customView: nil + customView: nil, + nextText: nil ), OnboardingCard( title: Text("Features"), @@ -169,28 +171,32 @@ func createOnboardingView(delegate: SheetDismisserProtocol) -> some View { 👨‍💻 Open Source : The app's source code is publicly available for audit and contribution. """), - customView: nil + customView: nil, + nextText: nil ), OnboardingCard( title: Text("Settings"), description: Text("These are important privacy settings you may want to review!"), imageName: nil, articleText: nil, - customView: AnyView(PrivacySettingsSubview(onboardingPart:0)) + customView: AnyView(PrivacySettingsSubview(onboardingPart:0)), + nextText: nil ), OnboardingCard( title: Text("Settings"), description: Text("These are important privacy settings you may want to review!"), imageName: nil, articleText: nil, - customView: AnyView(PrivacySettingsSubview(onboardingPart:1)) + customView: AnyView(PrivacySettingsSubview(onboardingPart:1)), + nextText: nil ), OnboardingCard( title: Text("Even more to customize!"), description: Text("You can customize even more, just use the button below to open the settings."), imageName: "hand.wave", articleText: nil, - customView: AnyView(TakeMeToSettingsView(delegate:delegate)) + customView: AnyView(TakeMeToSettingsView(delegate:delegate)), + nextText: nil ), ] OnboardingView(delegate: delegate, cards: cards) diff --git a/Monal/Classes/MLXMPPManager.m b/Monal/Classes/MLXMPPManager.m index b2d2ad1297..1fb8697992 100644 --- a/Monal/Classes/MLXMPPManager.m +++ b/Monal/Classes/MLXMPPManager.m @@ -147,10 +147,10 @@ -(void) defaultSettings [self upgradeBoolUserSettingsIfUnset:@"hasCompletedOnboarding" toDefault:NO]; -//always show onboarding on simulator for now -#if TARGET_OS_SIMULATOR - [[HelperTools defaultsDB] setBool:NO forKey:@"hasCompletedOnboarding"]; -#endif +// //always show onboarding on simulator for now +// #if TARGET_OS_SIMULATOR +// [[HelperTools defaultsDB] setBool:NO forKey:@"hasCompletedOnboarding"]; +// #endif } -(void) upgradeFloatUserSettingsToInteger:(NSString*) settingsName From efa3fd0fbd0eb70f5eeed1faff7fe155450443b0 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Fri, 19 Jul 2024 10:00:30 +0200 Subject: [PATCH 35/35] Fix portrait mode on iPad When starting the app while in portrait mode, the screen would go grey. After rotating the device the UI would appear again (even if rotated back into portrait mode). This apparently was related to the old MLPlaceholderViewController setting self.splitViewController.preferredDisplayMode = UISplitViewControllerDisplayModeOneBesideSecondary; in its viewWillAppear: handler. I could not yet implement that using our swiftui chat placeholder view. But: the swiftui placeholder view will still be used, the MLPlaceholderViewController is only used to set the correct preferredDisplayMode. --- Monal/Classes/MLPlaceholderViewController.m | 22 +++++++++++++++++++ Monal/Monal.xcodeproj/project.pbxproj | 4 ++++ Monal/localization/Base.lproj/Main.storyboard | 13 +++++++++++ 3 files changed, 39 insertions(+) create mode 100644 Monal/Classes/MLPlaceholderViewController.m diff --git a/Monal/Classes/MLPlaceholderViewController.m b/Monal/Classes/MLPlaceholderViewController.m new file mode 100644 index 0000000000..450d5f133c --- /dev/null +++ b/Monal/Classes/MLPlaceholderViewController.m @@ -0,0 +1,22 @@ +// +// MLPlaceholderViewController.m +// Monal +// +// Created by Anurodh Pokharel on 1/5/20. +// Copyright © 2020 Monal.im. All rights reserved. +// + +#import + +@interface MLPlaceholderViewController : UIViewController +@end + +@implementation MLPlaceholderViewController + +-(void) viewWillAppear:(BOOL)animated +{ + [super viewWillAppear:animated]; + self.splitViewController.preferredDisplayMode = UISplitViewControllerDisplayModeOneBesideSecondary; +} + +@end diff --git a/Monal/Monal.xcodeproj/project.pbxproj b/Monal/Monal.xcodeproj/project.pbxproj index 29424f0190..dff94f2b8b 100644 --- a/Monal/Monal.xcodeproj/project.pbxproj +++ b/Monal/Monal.xcodeproj/project.pbxproj @@ -148,6 +148,7 @@ 845EFFBD2918721800C1E03E /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = C1E4654624EE517000CA5AAF /* Localizable.strings */; }; 845EFFBE2918723D00C1E03E /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = C1E4654624EE517000CA5AAF /* Localizable.strings */; }; 846DF27C2937FAA600AAB9C0 /* ChatPlaceholder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 846DF27B2937FAA600AAB9C0 /* ChatPlaceholder.swift */; }; + 848227912C4A6194003CCA33 /* MLPlaceholderViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 848227902C4A6194003CCA33 /* MLPlaceholderViewController.m */; }; 848717F3295ED64600B8D288 /* MLCall.m in Sources */ = {isa = PBXBuildFile; fileRef = 848717F1295ED64500B8D288 /* MLCall.m */; }; 848904A9289C82C30097E19C /* SCRAM.m in Sources */ = {isa = PBXBuildFile; fileRef = 848904A8289C82C30097E19C /* SCRAM.m */; }; 848C73E02BDF2014007035C9 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 848C73DF2BDF2014007035C9 /* PrivacyInfo.xcprivacy */; }; @@ -581,6 +582,7 @@ 844921EB2C29F9BE00B99A9C /* MLDelayableTimer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MLDelayableTimer.h; sourceTree = ""; }; 845D636A2AD4AEDA0066EFFB /* ImageViewer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageViewer.swift; sourceTree = ""; }; 846DF27B2937FAA600AAB9C0 /* ChatPlaceholder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatPlaceholder.swift; sourceTree = ""; }; + 848227902C4A6194003CCA33 /* MLPlaceholderViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MLPlaceholderViewController.m; sourceTree = ""; }; 848717F1295ED64500B8D288 /* MLCall.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = MLCall.m; path = Classes/MLCall.m; sourceTree = SOURCE_ROOT; }; 848717F2295ED64500B8D288 /* MLCall.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = MLCall.h; path = Classes/MLCall.h; sourceTree = SOURCE_ROOT; }; 848904A8289C82C30097E19C /* SCRAM.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SCRAM.m; sourceTree = ""; }; @@ -1051,6 +1053,7 @@ 26715E5E17650AF900684F3D /* View Controllers */ = { isa = PBXGroup; children = ( + 848227902C4A6194003CCA33 /* MLPlaceholderViewController.m */, 849248482AD4CEC400986C1A /* ZoomableContainer.swift */, 845D636A2AD4AEDA0066EFFB /* ImageViewer.swift */, 841B6F16297AFB340074F9B7 /* Calls */, @@ -2104,6 +2107,7 @@ 8441EFF92921B53500E851E9 /* BackgroundSettings.swift in Sources */, 841898AC2957DBAD00FEC77D /* RichAlert.swift in Sources */, 840E23CA28ADA56900A7FAC9 /* MLUploadQueueCell.m in Sources */, + 848227912C4A6194003CCA33 /* MLPlaceholderViewController.m in Sources */, 262E51921AD8CAC600788351 /* MLButtonCell.m in Sources */, 841EE4302A426F2300D3AF14 /* MLCrashReporter.m in Sources */, E8DED06225388BE8003167FF /* MLSearchViewController.m in Sources */, diff --git a/Monal/localization/Base.lproj/Main.storyboard b/Monal/localization/Base.lproj/Main.storyboard index e0b4db6b7a..c12ba625db 100644 --- a/Monal/localization/Base.lproj/Main.storyboard +++ b/Monal/localization/Base.lproj/Main.storyboard @@ -76,6 +76,14 @@ + + + + + + + + @@ -139,6 +147,7 @@ + @@ -169,6 +178,7 @@ + @@ -2497,6 +2507,9 @@ + + +