diff --git a/.github/workflows/beta.build-push.yml b/.github/workflows/beta.build-push.yml index f0c49a4117..49a3e73f71 100644 --- a/.github/workflows/beta.build-push.yml +++ b/.github/workflows/beta.build-push.yml @@ -15,8 +15,13 @@ jobs: buildAndPublishBeta: name: "Build and Publish Beta Release" runs-on: self-hosted + outputs: + release-tag: ${{ steps.releasenotes.outputs.tag }} + release-name: ${{ steps.releasenotes.outputs.name }} + release-notes: ${{ steps.releasenotes.outputs.notes }} env: APP_NAME: "Monal" + BUILD_SCHEME: "Monal" APP_DIR: "Monal.app" BUILD_TYPE: "Beta" EXPORT_OPTIONS_CATALYST_APPSTORE: "../scripts/exportOptions/Stable_Catalyst_ExportOptions.plist" @@ -28,8 +33,10 @@ jobs: with: clean: true submodules: true - - name: Fetch tags - run: git fetch --tags + fetch-depth: 100 + fetch-tags: true + show-progress: true + lfs: true - name: Checkout submodules run: git submodule update -f --init --remote - name: Get last build tag and increment it @@ -38,7 +45,56 @@ jobs: buildNumber=$(expr $oldBuildNumber + 1) echo "New buildNumber is $buildNumber" git tag Build_iOS_$buildNumber - - name: Insert buildNumber into plists + - name: Extract version number and changelog from newest merge commit + id: releasenotes + run: | + function repairNotes { + sed 's/\r//g' | awk '{ + if (NR == 1) { + printf("%s", $0) + } else { + if ($0 ~ /^[\t ]*(-|IOS_ONLY[\t ]*-|MACOS_ONLY[\t ]*-).*$/) { + printf("\n%s", $0) + } else { + printf(" %s", $0) + } + } + } + END { + printf("\n") + }' + } + buildNumber="$(git tag --sort="v:refname" | grep "Build_iOS" | tail -n1 | sed 's/Build_iOS_//g')" + version="$(git log -n 1 --merges --pretty=format:%s | sed -E 's/^[\t\n ]*([^\n\t ]+)[\t\n ]+\(([^\n\t ]+)\)[\t\n ]*$/\1/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=$version" | tee /dev/stderr >> "$OUTPUT_FILE" + echo "buildVersion=$(echo "$version" | grep -oE '^[0-9]+(\.[0-9]+){0,2}')" | 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 >> "$OUTPUT_FILE" + + echo "notes<<__EOF__" | tee /dev/stderr >> "$OUTPUT_FILE" + echo "$(git log -n 1 --merges --pretty=format:%b)" | repairNotes | sed -E 's/^[\t\n ]*IOS_ONLY[\t\n ]?(.*)$/\1/g' | sed -E 's/^[\t\n ]*MACOS_ONLY[\t\n ]?(.*)$/\1/g' | 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)" | repairNotes | grep -v '^[\t\n ]*MACOS_ONLY.*$' | sed -E 's/^[\t\n ]*IOS_ONLY[\t\n ]?(.*)$/\1/g' | 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)" | repairNotes | grep -v '^[\t\n ]*IOS_ONLY.*$' | sed -E 's/^[\t\n ]*MACOS_ONLY[\t\n ]?(.*)$/\1/g' | tee /dev/stderr >> "$OUTPUT_FILE" + echo "__EOF__" | tee /dev/stderr >> "$OUTPUT_FILE" + + cat "$OUTPUT_FILE" >> "$GITHUB_OUTPUT" + - name: Insert buildNumber and version into plists + env: + buildNumber: ${{ steps.releasenotes.outputs.buildNumber }} + buildVersion: ${{ steps.releasenotes.outputs.buildVersion }} run: sh ./scripts/set_version_number.sh - name: Import TURN secrets run: | @@ -49,14 +105,33 @@ jobs: run: chmod +x ./scripts/build.sh - name: Run build run: ./scripts/build.sh + - uses: actions/upload-artifact@v4 + with: + name: monal-ios + path: Monal/build/ipa/Monal.ipa + if-no-files-found: error + - uses: actions/upload-artifact@v4 + with: + name: monal-catalyst-dsym + path: Monal/build/macos_Monal.xcarchive/dSYMs + if-no-files-found: error + - uses: actions/upload-artifact@v4 + with: + name: monal-ios-dsym + path: Monal/build/ios_Monal.xcarchive/dSYMs + if-no-files-found: error - name: validate ios app 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 tag to beta repo + - 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: 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)" + #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_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 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 @@ -66,30 +141,40 @@ jobs: stapler validate "$APP_DIR" /usr/bin/ditto -c -k --sequesterRsrc --keepParent "$APP_DIR" "../$APP_NAME.zip" cd ../../../.. - - name: upload new catalyst beta to monal-im.org - run: ./scripts/uploadNonAlpha.sh beta - - name: Publish catalyst to appstore connect - run: xcrun altool --upload-app --file ./Monal/build/app/Monal.pkg --type macos --asc-provider S8D843U34Y -u "$(cat /Users/ci/apple_connect_upload_mail.txt)" -p "$(cat /Users/ci/apple_connect_upload_secret.txt)" --primary-bundle-id org.monal-im.prod.catalyst.monal - uses: actions/upload-artifact@v4 with: - name: monal-catalyst + name: monal-catalyst-zip path: Monal/build/app/Monal.zip if-no-files-found: error - uses: actions/upload-artifact@v4 with: - name: monal-ios - path: Monal/build/ipa/Monal.ipa + name: monal-catalyst-pkg + path: Monal/build/app/Monal.pkg if-no-files-found: error - - uses: actions/upload-artifact@v4 - with: - name: monal-catalyst-dsym - path: Monal/build/macos_Monal.xcarchive/dSYMs - if-no-files-found: error - - uses: actions/upload-artifact@v4 + - name: Upload new catalyst beta to monal-im.org + run: ./scripts/uploadNonAlpha.sh beta + - name: Publish catalyst to appstore connect + #run: xcrun altool --upload-app --file ./Monal/build/app/Monal.pkg --type macos --asc-provider S8D843U34Y -u "$(cat /Users/ci/apple_connect_upload_mail.txt)" -p "$(cat /Users/ci/apple_connect_upload_secret.txt)" --primary-bundle-id org.monal-im.prod.catalyst.monal + env: + 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 + - name: Release + uses: softprops/action-gh-release@v2 with: - name: monal-ios-dsym - path: Monal/build/ios_Monal.xcarchive/dSYMs - if-no-files-found: error + name: "${{ steps.releasenotes.outputs.name }}" + tag_name: "${{ steps.releasenotes.outputs.tag }}" + target_commitish: beta + generate_release_notes: false + body: "${{ steps.releasenotes.outputs.notes }}" + files: | + ./Monal/build/ipa/Monal.ipa + ./Monal/build/app/Monal.pkg + ./Monal/build/app/Monal.zip + fail_on_unmatched_files: true + token: ${{ secrets.GITHUB_TOKEN }} + draft: false + prerelease: true updateTranslations: name: Update Translations using Beta-Branch @@ -112,3 +197,21 @@ jobs: chmod +x ./scripts/updateLocalization.sh chmod +x ./scripts/xliff_extractor.py ./scripts/updateLocalization.sh BUILDSERVER + + notifyMuc: + name: Notify support MUC about new Betarelease + runs-on: ubuntu-latest + needs: [buildAndPublishBeta] + 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@chat.yax.im + recipient_is_room: true + bot_alias: "Monal Release Bot" + message: | + ${{ needs.buildAndPublishBeta.outputs.release-name }} was released + ${{ needs.buildAndPublishBeta.outputs.release-notes }} diff --git a/.github/workflows/develop-push.yml b/.github/workflows/develop-push.yml index 63c62120ca..ae1b7d989d 100644 --- a/.github/workflows/develop-push.yml +++ b/.github/workflows/develop-push.yml @@ -15,8 +15,13 @@ 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_NAME: "Monal.Alpha" + BUILD_SCHEME: "Monal Alpha" APP_DIR: "Monal.alpha.app" BUILD_TYPE: "Alpha" ALPHA_UPLOAD_SECRET: ${{ secrets.ALPHA_UPLOAD_SECRET }} @@ -28,8 +33,10 @@ jobs: with: clean: true submodules: true - - name: Fetch tags - run: git fetch --tags + fetch-depth: 100 + fetch-tags: true + show-progress: true + lfs: true - name: Checkout submodules run: git submodule update -f --init --remote - name: Import TURN secrets @@ -50,6 +57,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}} @@ -58,6 +66,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 @@ -89,3 +103,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/ diff --git a/.github/workflows/publish-quicksy-release.yml b/.github/workflows/publish-quicksy-release.yml new file mode 100644 index 0000000000..590edbaad0 --- /dev/null +++ b/.github/workflows/publish-quicksy-release.yml @@ -0,0 +1,79 @@ +name: Publish Quicksy 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 }} + # 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" + + # 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 #quicksy #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/publish-stable-release.yml b/.github/workflows/publish-stable-release.yml new file mode 100644 index 0000000000..13daf45042 --- /dev/null +++ b/.github/workflows/publish-stable-release.yml @@ -0,0 +1,95 @@ +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://api.github.com/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/quicksy.build-push.yml b/.github/workflows/quicksy.build-push.yml new file mode 100644 index 0000000000..d8562007b0 --- /dev/null +++ b/.github/workflows/quicksy.build-push.yml @@ -0,0 +1,134 @@ +# build a new stable release and push it to apple +name: quicksy.build-push + +# Controls when the action will run. +on: + # Triggers the workflow on push + push: + branches: [ stable ] + + workflow_dispatch: + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + # This workflow contains a single job called "build" + buildAndPublishStable: + # The type of runner that the job will run on + runs-on: self-hosted + env: + APP_NAME: "Quicksy" + BUILD_SCHEME: "Quicksy" + APP_DIR: "Monal.app" + BUILD_TYPE: "AppStore" + EXPORT_OPTIONS_IOS: "../scripts/exportOptions/Quicksy_Stable_iOS_ExportOptions.plist" + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + - uses: actions/checkout@v4 + with: + clean: true + submodules: true + fetch-depth: 100 + fetch-tags: true + show-progress: true + lfs: true + - name: Checkout submodules + run: git submodule update -f --init --remote + - name: Get last build tag and increment it + run: | + oldBuildNumber=$(git tag --sort="v:refname" |grep "Quicksy_Build_iOS" | tail -n1 | sed 's/Quicksy_Build_iOS_//g') + buildNumber=$(expr $oldBuildNumber + 1) + echo "New buildNumber is $buildNumber" + git tag Quicksy_Build_iOS_$buildNumber + - name: Extract version number and changelog from newest merge commit + id: releasenotes + run: | + function repairNotes { + sed 's/\r//g' | awk '{ + if (NR == 1) { + printf("%s", $0) + } else { + if ($0 ~ /^[\t ]*(-|IOS_ONLY[\t ]*-|MACOS_ONLY[\t ]*-).*$/) { + printf("\n%s", $0) + } else { + printf(" %s", $0) + } + } + } + END { + printf("\n") + }' + } + buildNumber="$(git tag --sort="v:refname" | grep "Build_iOS" | tail -n1 | sed 's/Build_iOS_//g')" + version="$(git log -n 1 --merges --pretty=format:%s | sed -E 's/^[\t\n ]*([^\n\t ]+)[\t\n ]+\(([^\n\t ]+)\)[\t\n ]*$/\1/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=$version" | tee /dev/stderr >> "$OUTPUT_FILE" + echo "buildVersion=$(echo "$version" | grep -oE '^[0-9]+(\.[0-9]+){0,2}')" | tee /dev/stderr >> "$OUTPUT_FILE" + + echo "name=Quicksy $(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)" | repairNotes | sed -E 's/^[\t\n ]*IOS_ONLY[\t\n ]?(.*)$/\1/g' | sed -E 's/^[\t\n ]*MACOS_ONLY[\t\n ]?(.*)$/\1/g' | 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)" | repairNotes | grep -v '^[\t\n ]*MACOS_ONLY.*$' | sed -E 's/^[\t\n ]*IOS_ONLY[\t\n ]?(.*)$/\1/g' | 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)" | repairNotes | grep -v '^[\t\n ]*IOS_ONLY.*$' | sed -E 's/^[\t\n ]*MACOS_ONLY[\t\n ]?(.*)$/\1/g' | tee /dev/stderr >> "$OUTPUT_FILE" + echo "__EOF__" | tee /dev/stderr >> "$OUTPUT_FILE" + + cat "$OUTPUT_FILE" >> "$GITHUB_OUTPUT" + - name: Insert buildNumber into plists + env: + buildNumber: ${{ steps.releasenotes.outputs.buildNumber }} + buildVersion: ${{ steps.releasenotes.outputs.buildVersion }} + run: sh ./scripts/set_version_number.sh + - name: Import TURN secrets + run: | + if [[ -e "/Users/ci/secrets.quicksy_stable" ]]; then + echo "#import \"/Users/ci/secrets.quicksy_stable\"" > Monal/Classes/secrets.h + fi + - name: Make our build scripts executable + run: chmod +x ./scripts/build.sh + - name: Run build + run: ./scripts/build.sh + - uses: actions/upload-artifact@v4 + with: + name: monal-ios + path: Monal/build/ipa/Monal.ipa + if-no-files-found: error + # - uses: actions/upload-artifact@v4 + # with: + # name: monal-ios-dsym + # path: Monal/build/ios_Monal.xcarchive/dSYMs + # if-no-files-found: error + - name: validate ios app + run: xcrun altool --validate-app --file ./Monal/build/ipa/Monal.ipa --type ios -u $(cat /Users/ci/apple_connect_upload_mail.txt) -p "$(cat /Users/ci/apple_connect_upload_secret.txt)" + - name: push tag to stable repo + run: | + buildNumber=$(git tag --sort="v:refname" | grep "Quicksy_Build_iOS" | tail -n1 | sed 's/Quicksy_Build_iOS_//g') + git push origin Build_iOS_$buildNumber + - 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)" + 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:false automatic_release:false skip_metadata: true skip_screenshots: true + - name: Remove fastlane metadata directory + run: | + rm -rf "${{ steps.metadata.outputs.path }}" diff --git a/.github/workflows/stable.build-push.yml b/.github/workflows/stable.build-push.yml index 251e3234cb..8c3cfcd809 100644 --- a/.github/workflows/stable.build-push.yml +++ b/.github/workflows/stable.build-push.yml @@ -17,6 +17,7 @@ jobs: runs-on: self-hosted env: APP_NAME: "Monal" + BUILD_SCHEME: "Monal" APP_DIR: "Monal.app" BUILD_TYPE: "AppStore" EXPORT_OPTIONS_CATALYST_APPSTORE: "../scripts/exportOptions/Stable_Catalyst_ExportOptions.plist" @@ -28,8 +29,10 @@ jobs: with: clean: true submodules: true - - name: Fetch tags - run: git fetch --tags + fetch-depth: 100 + fetch-tags: true + show-progress: true + lfs: true - name: Checkout submodules run: git submodule update -f --init --remote - name: Get last build tag and increment it @@ -38,7 +41,56 @@ jobs: buildNumber=$(expr $oldBuildNumber + 1) echo "New buildNumber is $buildNumber" git tag Build_iOS_$buildNumber - - name: Insert buildNumber into plists + - name: Extract version number and changelog from newest merge commit + id: releasenotes + run: | + function repairNotes { + sed 's/\r//g' | awk '{ + if (NR == 1) { + printf("%s", $0) + } else { + if ($0 ~ /^[\t ]*(-|IOS_ONLY[\t ]*-|MACOS_ONLY[\t ]*-).*$/) { + printf("\n%s", $0) + } else { + printf(" %s", $0) + } + } + } + END { + printf("\n") + }' + } + buildNumber="$(git tag --sort="v:refname" | grep "Build_iOS" | tail -n1 | sed 's/Build_iOS_//g')" + version="$(git log -n 1 --merges --pretty=format:%s | sed -E 's/^[\t\n ]*([^\n\t ]+)[\t\n ]+\(([^\n\t ]+)\)[\t\n ]*$/\1/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=$version" | tee /dev/stderr >> "$OUTPUT_FILE" + echo "buildVersion=$(echo "$version" | grep -oE '^[0-9]+(\.[0-9]+){0,2}')" | 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)" | repairNotes | sed -E 's/^[\t\n ]*IOS_ONLY[\t\n ]?(.*)$/\1/g' | sed -E 's/^[\t\n ]*MACOS_ONLY[\t\n ]?(.*)$/\1/g' | 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)" | repairNotes | grep -v '^[\t\n ]*MACOS_ONLY.*$' | sed -E 's/^[\t\n ]*IOS_ONLY[\t\n ]?(.*)$/\1/g' | 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)" | repairNotes | grep -v '^[\t\n ]*IOS_ONLY.*$' | sed -E 's/^[\t\n ]*MACOS_ONLY[\t\n ]?(.*)$/\1/g' | tee /dev/stderr >> "$OUTPUT_FILE" + echo "__EOF__" | tee /dev/stderr >> "$OUTPUT_FILE" + + cat "$OUTPUT_FILE" >> "$GITHUB_OUTPUT" + - name: Insert buildNumber and version into plists + env: + buildNumber: ${{ steps.releasenotes.outputs.buildNumber }} + buildVersion: ${{ steps.releasenotes.outputs.buildVersion }} run: sh ./scripts/set_version_number.sh - name: Import TURN secrets run: | @@ -47,17 +99,47 @@ jobs: fi - name: Make our build scripts executable run: chmod +x ./scripts/build.sh - - run: chmod +x ./scripts/push_xmpp.org.sh - name: Run build run: ./scripts/build.sh + - uses: actions/upload-artifact@v4 + with: + name: monal-ios + path: Monal/build/ipa/Monal.ipa + if-no-files-found: error + # - uses: actions/upload-artifact@v4 + # with: + # name: monal-catalyst-dsym + # path: Monal/build/macos_Monal.xcarchive/dSYMs + # if-no-files-found: error + # - uses: actions/upload-artifact@v4 + # with: + # name: monal-ios-dsym + # path: Monal/build/ios_Monal.xcarchive/dSYMs + # if-no-files-found: error - name: validate ios app run: xcrun altool --validate-app --file ./Monal/build/ipa/Monal.ipa --type ios -u $(cat /Users/ci/apple_connect_upload_mail.txt) -p "$(cat /Users/ci/apple_connect_upload_secret.txt)" - name: push tag to stable repo run: | buildNumber=$(git tag --sort="v:refname" |grep "Build_iOS" | tail -n1 | sed 's/Build_iOS_//g') git push origin Build_iOS_$buildNumber + - name: Create fastlane metadata directory + id: metadata + env: + CHANGELOG_IOS: ${{ steps.releasenotes.outputs.notes_ios }} + CHANGELOG_MACOS: ${{ steps.releasenotes.outputs.notes_macos }} + run: | + path_ios="$(mktemp -d)" + echo -n "$CHANGELOG_IOS" > "$path_ios/release_notes.txt" + echo "path_ios=$path_ios" | tee /dev/stderr >> "$GITHUB_OUTPUT" + path_macos="$(mktemp -d)" + echo -n "$CHANGELOG_MACOS" > "$path_macos/release_notes.txt" + echo "path_macos=$path_macos" | 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_ios }} + 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 @@ -67,50 +149,47 @@ 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 + - uses: actions/upload-artifact@v4 + with: + name: monal-catalyst-zip + path: Monal/build/app/Monal.zip + if-no-files-found: error + - uses: actions/upload-artifact@v4 + with: + name: monal-catalyst-pkg + path: Monal/build/app/Monal.pkg + if-no-files-found: error + - 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_macos }} + 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: Extract version number and changelog from newest merge commit - id: releasenotes + - name: Remove fastlane metadata directory run: | - buildNumber=$(git tag --sort="v:refname" | grep "Build_iOS" | tail -n1 | sed 's/Build_iOS_//g') - echo "tag=Build_iOS_$buildNumber" >> "$GITHUB_OUTPUT" - echo "name=$(git log -n 1 --merges --pretty=format:%s | sed -E 's/^\s*([^\s]+)\s+\(([^\s]+)\)$/\1 (Build '$buildNumber', PR \2)/g')" >> "$GITHUB_OUTPUT" - echo "notes=$(git log -n 1 --merges --pretty=format:%b)" >> "$GITHUB_OUTPUT" - - name: Release + rm -rf "${{ steps.metadata.outputs.path_ios }}" + rm -rf "${{ steps.metadata.outputs.path_macos }}" + - name: Create Draft Release + id: draftrelease uses: softprops/action-gh-release@v2 with: - name: "Release ${{ steps.releasenotes.outputs.name }}" + name: "${{ steps.releasenotes.outputs.name }}" tag_name: "${{ steps.releasenotes.outputs.tag }}" target_commitish: stable generate_release_notes: false body: "${{ steps.releasenotes.outputs.notes }}" files: | ./Monal/build/ipa/Monal.ipa + ./Monal/build/app/Monal.pkg ./Monal/build/app/Monal.zip fail_on_unmatched_files: true token: ${{ secrets.GITHUB_TOKEN }} - draft: false - - uses: actions/upload-artifact@v4 - with: - name: monal-catalyst-pkg - path: Monal/build/app/Monal.pkg - if-no-files-found: error - - uses: actions/upload-artifact@v4 - with: - name: monal-ios - path: Monal/build/ipa/Monal.ipa - if-no-files-found: error - # - uses: actions/upload-artifact@v4 - # with: - # name: monal-catalyst-dsym - # path: Monal/build/macos_Monal.xcarchive/dSYMs - # if-no-files-found: error - # - uses: actions/upload-artifact@v4 - # with: - # name: monal-ios-dsym - # path: Monal/build/ios_Monal.xcarchive/dSYMs - # if-no-files-found: error + 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/.github/workflows/update-translations.yml b/.github/workflows/update-translations.yml index a628f9ec12..fa3c998320 100644 --- a/.github/workflows/update-translations.yml +++ b/.github/workflows/update-translations.yml @@ -24,6 +24,10 @@ jobs: with: clean: true submodules: true + fetch-depth: 100 + fetch-tags: true + show-progress: true + lfs: true - name: Checkout submodules run: git submodule update -f --init --remote - name: Update translations 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/ActiveChatsViewController.h b/Monal/Classes/ActiveChatsViewController.h index ef345377a9..4a4aef98a3 100644 --- a/Monal/Classes/ActiveChatsViewController.h +++ b/Monal/Classes/ActiveChatsViewController.h @@ -14,6 +14,7 @@ NS_ASSUME_NONNULL_BEGIN +@class UIHostingControllerWorkaround; @class chatViewController; @class MLCall; @@ -25,6 +26,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,11 +40,14 @@ NS_ASSUME_NONNULL_BEGIN -(void) showContacts; -(void) deleteConversation; -(void) showSettings; --(void) showPrivacySettings; +-(void) showGeneralSettings; +-(void) showOnboarding; -(void) showNotificationSettings; -(void) showDetails; -(void) showRegisterWithUsername:(NSString*) username onHost:(NSString*) host withToken:(NSString* _Nullable) token usingCompletion:(monal_id_block_t _Nullable) callback; -(void) showAddContactWithJid:(NSString*) jid preauthToken:(NSString* _Nullable) preauthToken prefillAccount:(xmpp* _Nullable) account andOmemoFingerprints:(NSDictionary* _Nullable) fingerprints; +-(void) showAddContact; +-(void) sheetDismissed; @end diff --git a/Monal/Classes/ActiveChatsViewController.m b/Monal/Classes/ActiveChatsViewController.m index c09d3861bb..24771259ab 100755 --- a/Monal/Classes/ActiveChatsViewController.m +++ b/Monal/Classes/ActiveChatsViewController.m @@ -6,6 +6,7 @@ // // +#import #import "ActiveChatsViewController.h" #import "DataLayer.h" #import "xmpp.h" @@ -19,6 +20,8 @@ #import "MLSettingsAboutViewController.h" #import "MLVoIPProcessor.h" #import "MLCall.h" //for MLCallType +#import "XMPPIQ.h" +#import "MLIQProcessor.h" #import "UIColor+Theme.h" #import @@ -39,6 +42,7 @@ @interface ActiveChatsViewController() { int _startedOrientation; double _portraitTop; double _landscapeTop; + BOOL _loginAlreadyAutodisplayed; } @property (atomic, strong) NSMutableArray* unpinnedContacts; @property (atomic, strong) NSMutableArray* pinnedContacts; @@ -58,37 +62,36 @@ @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]; - return self; -} -(void) configureComposeButton { - UIImage* composeImage = [[UIImage systemImageNamed:@"person.2.fill"] imageWithTintColor:UIColor.monalGreen]; + UIImage* image = [[UIImage systemImageNamed:@"person.2.fill"] imageWithTintColor:UIColor.monalGreen]; + UITapGestureRecognizer* tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(showContacts:)]; + self.composeButton.customView = [HelperTools + buttonWithNotificationBadgeForImage:image + hasNotification:[[DataLayer sharedInstance] allContactRequests].count > 0 + withTapHandler:tapRecognizer]; + [self.composeButton setIsAccessibilityElement:YES]; if([[DataLayer sharedInstance] allContactRequests].count > 0) - { - self.composeButton.image = [HelperTools imageWithNotificationBadgeForImage:composeImage]; - } + [self.composeButton setAccessibilityLabel:NSLocalizedString(@"Open contact list (contact requests pending)", @"")]; else - { - self.composeButton.image = composeImage; - } - [self.composeButton setAccessibilityLabel:@"Open contacts list"]; - [self.composeButton setAccessibilityHint:NSLocalizedString(@"Open contact list", @"")]; + [self.composeButton setAccessibilityLabel:NSLocalizedString(@"Open contact list", @"")]; + [self.composeButton setAccessibilityTraits:UIAccessibilityTraitButton]; } -(void) viewDidLoad { + DDLogDebug(@"active chats view did load"); [super viewDidLoad]; + _loginAlreadyAutodisplayed = NO; _startedOrientation = 0; self.spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleMedium]; @@ -134,6 +137,7 @@ -(void) viewDidLoad -(void) dealloc { + DDLogDebug(@"active chats dealloc"); [[NSNotificationCenter defaultCenter] removeObserver:self]; } @@ -263,10 +267,8 @@ -(void) handleRefreshDisplayNotification:(NSNotification*) notification { // filter notifcations from within this class if([notification.object isKindOfClass:[ActiveChatsViewController class]]) - { return; - } - [self refreshDisplay]; + [self refresh]; } -(void) handleContactRemoved:(NSNotification*) notification @@ -380,9 +382,8 @@ -(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]; + + [self openConversationPlaceholder:nil]; } -(void) viewWillDisappear:(BOOL) animated @@ -395,6 +396,21 @@ -(void) viewDidAppear:(BOOL) animated { DDLogDebug(@"active chats view did appear"); [super viewDidAppear:animated]; + + [self refresh]; +} + +-(void) sheetDismissed +{ + [self refresh]; +} + +-(void) refresh +{ + dispatch_async(dispatch_get_main_queue(), ^{ + [self refreshDisplay]; // load contacts + [self segueToIntroScreensIfNeeded]; + }); } -(void) didReceiveMemoryWarning @@ -411,6 +427,26 @@ -(void) showAddContactWithJid:(NSString*) jid preauthToken:(NSString* _Nullable) [self presentChatWithContact:newContact]; }); }]; + addContactMenuView.ml_disposeCallback = ^{ + [self sheetDismissed]; + }; + [self presentViewController:addContactMenuView animated:NO completion:^{}]; + }]; + }); +} + +-(void) showAddContact +{ + dispatch_async(dispatch_get_main_queue(), ^{ + [self dismissCompleteViewChainWithAnimation:NO andCompletion:^{ + UIViewController* addContactMenuView = [[SwiftuiInterface new] makeAddContactViewWithDismisser:^(MLContact* _Nonnull newContact) { + dispatch_async(dispatch_get_main_queue(), ^{ + [self presentChatWithContact:newContact]; + }); + }]; + addContactMenuView.ml_disposeCallback = ^{ + [self sheetDismissed]; + }; [self presentViewController:addContactMenuView animated:NO completion:^{}]; }]; }); @@ -423,25 +459,114 @@ -(void) segueToIntroScreensIfNeeded if(needingMigration.count > 0) { UIViewController* passwordMigration = [[SwiftuiInterface new] makePasswordMigration:needingMigration]; + passwordMigration.ml_disposeCallback = ^{ + [self sheetDismissed]; + }; [self presentViewController:passwordMigration animated:YES completion:^{}]; return; } - // display quick start if the user never seen it or if there are 0 enabled accounts + + if(![[HelperTools defaultsDB] boolForKey:@"hasCompletedOnboarding"]) + { + [self showOnboarding]; + return; + } + + if(self.enqueueGeneralSettings) + { + self.enqueueGeneralSettings = NO; + [self showGeneralSettings]; + return; + } + +#ifdef IS_QUICKSY if([[DataLayer sharedInstance] enabledAccountCnts].intValue == 0) { - UIViewController* loginViewController = [[SwiftuiInterface new] makeViewWithName:@"WelcomeLogIn"]; - [self presentViewController:loginViewController animated:YES completion:^{}]; + UIViewController* view = [[SwiftuiInterface new] makeAccountRegistration:@{}]; + if(UIDevice.currentDevice.userInterfaceIdiom != UIUserInterfaceIdiomPad) + view.modalPresentationStyle = UIModalPresentationFullScreen; + else + view.ml_disposeCallback = ^{ + [self sheetDismissed]; + }; + [self presentViewController:view animated:NO completion:^{}]; return; } - if(![[HelperTools defaultsDB] boolForKey:@"HasSeenPrivacySettings"]) +#else + // display quick start if the user never seen it or if there are 0 enabled accounts + if([[DataLayer sharedInstance] enabledAccountCnts].intValue == 0 && !_loginAlreadyAutodisplayed) { - [self showPrivacySettings]; + UIViewController* loginViewController = [[SwiftuiInterface new] makeViewWithName:@"WelcomeLogIn"]; + loginViewController.ml_disposeCallback = ^{ + self->_loginAlreadyAutodisplayed = YES; + [self sheetDismissed]; + }; + [self presentViewController:loginViewController animated:YES completion:^{}]; return; } +#endif [self showWarningsIfNeeded]; + +#ifdef IS_QUICKSY + [self syncContacts]; +#endif } +#ifdef IS_QUICKSY +-(void) syncContacts +{ + CNContactStore* store = [[CNContactStore alloc] init]; + [store requestAccessForEntityType:CNEntityTypeContacts completionHandler:^(BOOL granted, NSError* _Nullable error) { + if(granted) + { + NSString* countryCode = [[HelperTools defaultsDB] objectForKey:@"Quicksy_countryCode"]; + NSCharacterSet* allowedCharacters = [[NSCharacterSet characterSetWithCharactersInString:@"+0123456789"] invertedSet]; + NSMutableDictionary* numbers = [NSMutableDictionary new]; + + CNContactFetchRequest* request = [[CNContactFetchRequest alloc] initWithKeysToFetch:@[CNContactPhoneNumbersKey, CNContactNicknameKey, CNContactGivenNameKey, CNContactFamilyNameKey]]; + NSError* error; + [store enumerateContactsWithFetchRequest:request error:&error usingBlock:^(CNContact* _Nonnull contact, BOOL* _Nonnull stop) { + if(!error) + { + NSString* name = [[NSString stringWithFormat:@"%@ %@", contact.givenName, contact.familyName] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; + for(CNLabeledValue* phone in contact.phoneNumbers) + { + //add country code if missing + NSString* number = [[phone.value.stringValue componentsSeparatedByCharactersInSet:allowedCharacters] componentsJoinedByString:@""]; + if(countryCode != nil && ![number hasPrefix:@"+"] && ![number hasPrefix:@"00"]) + { + DDLogVerbose(@"Adding country code '%@' to number: %@", countryCode, number); + number = [NSString stringWithFormat:@"%@%@", countryCode, [number hasPrefix:@"0"] ? [number substringFromIndex:1] : number]; + } + numbers[number] = name; + } + } + else + DDLogWarn(@"Error fetching contacts: %@", error); + }]; + + DDLogDebug(@"Got list of contact phone numbers: %@", numbers); + + NSArray* connectedAccounts = [MLXMPPManager sharedInstance].connectedXMPP; + if(connectedAccounts.count == 0) + { + DDLogError(@"No connected account while trying to send quicksy phonebook!"); + return; + } + else if(connectedAccounts.count > 1) + DDLogWarn(@"More than 1 connected account while trying to send quicksy phonebook, using first one!"); + + XMPPIQ* iqNode = [[XMPPIQ alloc] initWithType:kiqGetType to:@"api.quicksy.im"]; + [iqNode setQuicksyPhoneBook:numbers.allKeys]; + [connectedAccounts[0] sendIq:iqNode withHandler:$newHandler(MLIQProcessor, handleQuicksyPhoneBook, $ID(numbers))]; + } + else + DDLogError(@"Access to contacts not granted!"); + }]; +} +#endif + -(void) showWarningsIfNeeded { dispatch_async(dispatch_get_main_queue(), ^{ @@ -510,14 +635,38 @@ -(void) openConversationPlaceholder:(MLContact*) contact -(void) showNotificationSettings { - UIViewController* view = [[SwiftuiInterface new] makeViewWithName:@"ActiveChatsNotificatioSettings"]; - [self presentViewController:view animated:YES completion:^{}]; + [self dismissCompleteViewChainWithAnimation:NO andCompletion:^{ + UIViewController* view = [[SwiftuiInterface new] makeViewWithName:@"ActiveChatsNotificationSettings"]; + view.ml_disposeCallback = ^{ + [self sheetDismissed]; + }; + [self presentViewController:view animated:YES completion:^{}]; + }]; +} + +-(void) showGeneralSettings +{ + [self dismissCompleteViewChainWithAnimation:NO andCompletion:^{ + UIViewController* view = [[SwiftuiInterface new] makeViewWithName:@"ActiveChatsGeneralSettings"]; + view.ml_disposeCallback = ^{ + [self sheetDismissed]; + }; + [self presentViewController:view animated:YES completion:^{}]; + }]; } --(void) showPrivacySettings +-(void) showOnboarding { - UIViewController* view = [[SwiftuiInterface new] makeViewWithName:@"ActiveChatsPrivacySettings"]; - [self presentViewController:view animated:YES completion:^{}]; + [self dismissCompleteViewChainWithAnimation:NO andCompletion:^{ + 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:^{}]; + }]; } -(void) showSettings @@ -580,6 +729,9 @@ -(void) presentAccountPickerForContacts:(NSArray*) contacts andCallT { [self dismissCompleteViewChainWithAnimation:NO andCompletion:^{ UIViewController* accountPickerController = [[SwiftuiInterface new] makeAccountPickerForContacts:contacts andCallType:callType]; + accountPickerController.ml_disposeCallback = ^{ + [self sheetDismissed]; + }; [self presentViewController:accountPickerController animated:YES completion:^{}]; }]; } @@ -694,6 +846,9 @@ -(void) prepareForSegue:(UIStoryboardSegue*) segue sender:(id) sender else if([segue.identifier isEqualToString:@"showDetails"]) { UIViewController* detailsViewController = [[SwiftuiInterface new] makeContactDetails:sender]; + detailsViewController.ml_disposeCallback = ^{ + [self sheetDismissed]; + }; [self presentViewController:detailsViewController animated:YES completion:^{}]; } else if([segue.identifier isEqualToString:@"showContacts"]) @@ -838,6 +993,9 @@ -(void) tableView:(UITableView*) tableView accessoryButtonTappedForRowWithIndexP selected = self.unpinnedContacts[indexPath.row]; } UIViewController* detailsViewController = [[SwiftuiInterface new] makeContactDetails:selected]; + detailsViewController.ml_disposeCallback = ^{ + [self sheetDismissed]; + }; [self presentViewController:detailsViewController animated:YES completion:^{}]; } @@ -947,7 +1105,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])}; @@ -957,7 +1115,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; @@ -1014,6 +1172,9 @@ -(void) showRegisterWithUsername:(NSString*) username onHost:(NSString*) host wi DDLogWarn(@"Dummy reg completion called for accountNo: %@", accountNo); })), }]; + registerViewController.ml_disposeCallback = ^{ + [self sheetDismissed]; + }; [self presentViewController:registerViewController animated:YES completion:^{}]; }]; } @@ -1023,6 +1184,9 @@ -(void) showDetails if([MLNotificationManager sharedInstance].currentContact != nil) { UIViewController* detailsViewController = [[SwiftuiInterface new] makeContactDetails:[MLNotificationManager sharedInstance].currentContact]; + detailsViewController.ml_disposeCallback = ^{ + [self sheetDismissed]; + }; [self presentViewController:detailsViewController animated:YES completion:^{}]; } } diff --git a/Monal/Classes/AddContactMenu.swift b/Monal/Classes/AddContactMenu.swift index 94011b2dc9..c2ee2385a0 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 { @@ -172,6 +172,10 @@ struct AddContactMenu: View { } else { + if DataLayer.sharedInstance().allContactRequests().count > 0 { + ContactRequestsMenu() + } + Section(header:Text("Contact and Group/Channel Jids are usually in the format: name@domain.tld")) { if connectedAccounts.count > 1 { Picker("Use account", selection: $selectedAccount) { @@ -190,18 +194,16 @@ struct AddContactMenu: View { .addClearButton(isEditing: isEditingJid, text:$toAdd) .disabled(scannedFingerprints != nil) .foregroundColor(scannedFingerprints != nil ? .secondary : .primary) - .onChange(of: toAdd) { _ in - toAdd = toAdd.replacingOccurrences(of: " ", with: "") - } - } - if scannedFingerprints != nil && scannedFingerprints!.count > 0 { - Section(header: Text("A contact was scanned through the QR code scanner")) { - Toggle(isOn: $importScannedFingerprints) { - Text("Import and trust OMEMO fingerprints from QR code") + .onChange(of: toAdd) { _ in toAdd = toAdd.replacingOccurrences(of: " ", with: "") } + + if scannedFingerprints != nil && scannedFingerprints!.count > 0 { + Section(header: Text("A contact was scanned through the QR code scanner")) { + Toggle(isOn: $importScannedFingerprints) { + Text("Import and trust OMEMO fingerprints from QR code") + } } } - } - Section { + if scannedFingerprints != nil { Button(action: { toAdd = "" @@ -212,26 +214,43 @@ struct AddContactMenu: View { .foregroundColor(.red) }) } - Button(action: { - showAlert = toAddEmptyAlert || toAddInvalidAlert + + HStack { + Spacer() + + Button(action: { + showAlert = toAddEmptyAlert || toAddInvalidAlert - if !showAlert { - let jidComponents = HelperTools.splitJid(toAdd) - if jidComponents["host"] == nil || jidComponents["host"]!.isEmpty { - errorAlert(title: Text("Error"), message: Text("Something went wrong while parsing your input...")) - showAlert = true - return + if !showAlert { + let jidComponents = HelperTools.splitJid(toAdd) + if jidComponents["host"] == nil || jidComponents["host"]!.isEmpty { + errorAlert(title: Text("Error"), message: Text("Something went wrong while parsing your input...")) + showAlert = true + return + } + // use the canonized jid from now on (lowercased, resource removed etc.) + addJid(jid: jidComponents["user"]!) } - // use the canonized jid from now on (lowercased, resource removed etc.) - addJid(jid: jidComponents["user"]!) + }) { + scannedFingerprints == nil ? Text("Add") : Text("Add scanned contact") } - }, label: { - scannedFingerprints == nil ? Text("Add Group/Channel or Contact") : Text("Add scanned Group/Channel or Contact") - }) - .disabled(toAddEmpty || toAddInvalid) + //.fontWeight(.bold) + .padding(10) + .background(toAddEmpty || toAddInvalid ? Color.gray : Color.blue) + .foregroundColor(.white) + .cornerRadius(10) + .disabled(toAddEmpty || toAddInvalid) + } + } + + if DataLayer.sharedInstance().allContactRequests().count == 0 { + Section { + ContactRequestsMenu() + } } } } + .padding() .alert(isPresented: $showAlert) { Alert(title: alertPrompt.title, message: alertPrompt.message, dismissButton:.default(Text("Close"), action: { showAlert = false diff --git a/Monal/Classes/BoardingCards.swift b/Monal/Classes/BoardingCards.swift new file mode 100644 index 0000000000..b3186e2b98 --- /dev/null +++ b/Monal/Classes/BoardingCards.swift @@ -0,0 +1,259 @@ +// +// BoardingCards.swift +// Monal +// +// Created by Vaidik Dubey on 05/06/24. +// Copyright © 2024 monal-im.org. All rights reserved. +// + +import FrameUp + +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? + let nextText: String? +} + +struct OnboardingView: View { + var delegate: SheetDismisserProtocol + let cards: [OnboardingCard] + @ObservedObject var onboardingState = OnboardingState() + @State private var currentIndex = 0 + + var body: some View { + ZStack { + /// 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 { + currentIndex -= 1 + } label: { + Label("Back", systemImage: "chevron.left") + .labelStyle(.iconOnly) + .foregroundColor(.blue) + .padding(10) + } + } + + HStack { + if let imageName = card.imageName { + Image(systemName: imageName) + .font(.custom("MarkerFelt-Wide", size: 80)) + .foregroundColor(.blue) + .accessibilityHidden(true) + + } + + card.title? + .font(.title) + .fontWeight(.bold) + .foregroundColor(.primary) + .padding(.bottom, 4) + //needed for ios < 16, see https://stackoverflow.com/a/59684944 + .fixedSize(horizontal: false, vertical: true) + } + .accessibilityElement(children: .combine) + .accessibilityAddTraits(.isHeader) + + 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) + } + + if card.imageName != nil || card.description != nil || card.imageName != nil { + Spacer().frame(height: 1) + Divider() + Spacer().frame(height: 1) + } + + card.articleText? + .font(.custom("HelveticaNeue-Medium", size: 20)) + .foregroundColor(.primary) + .multilineTextAlignment(.leading) + + card.customView + + Spacer() + + Group { + if index < cards.count - 1 { + Button { + currentIndex += 1 + } label: { + HStack { + Text(card.nextText ?? NSLocalizedString("Next", comment:"onboarding")) + .fontWeight(.bold) + Image(systemName: "chevron.right") + } + } + } else { + Button { + onboardingState.hasCompletedOnboarding = true + delegate.dismissWithoutAnimation() + } label: { + Text(card.nextText ?? NSLocalizedString("Close", comment:"onboarding")) + .fontWeight(.bold) + .padding(10) + .background(Color.blue) + .foregroundColor(.white) + .cornerRadius(10) + } + } + } + .frame(maxWidth: .infinity, alignment: .trailing) + } + .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) + } + } + .accessibilityAddTraits(.isModal) + } + } + } + .onAppear { + 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 + } + } + } +} + +@ViewBuilder +func createOnboardingView(delegate: SheetDismisserProtocol) -> some View { +#if IS_QUICKSY + let cards = [ + OnboardingCard( + title: Text("Welcome to Quicksy !"), + description: nil, + imageName: "hand.wave", + articleText: Text(""" + Quicksy syncs your contact list in regular intervals to make suggestions about possible contacts who are already on Quicksy. + + Quicksy shares and stores images, audio recordings, videos and other media to deliver them to the intended recipients. Files will be stored for up to 30 days. + + Find more Information in our [Privacy Policy](https://quicksy.im/privacy.htm). + """), + customView: nil, + nextText: "Accept and continue" + ), + ] +#else + let cards = [ + OnboardingCard( + title: Text("Welcome to Monal !"), + description: Text("Become part of a worldwide decentralized chat network!"), + imageName: "hand.wave", + 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, + nextText: nil + ), + OnboardingCard( + title: Text("Features"), + description: nil, + imageName: "sparkles", + articleText: Text(""" + 🛜 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). + + 🔐 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. + """), + customView: nil, + nextText: nil + ), + OnboardingCard( + title: Text("Settings"), + description: Text("These are important privacy settings you may want to review!"), + imageName: "gear", + articleText: nil, + customView: AnyView(PrivacySettingsSubview(onboardingPart:0)), + nextText: nil + ), + OnboardingCard( + title: Text("Settings"), + description: Text("These are important privacy settings you may want to review!"), + imageName: "gear", + articleText: nil, + 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)), + nextText: nil + ), + ] +#endif + 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(10) + .background(Color.blue) + .foregroundColor(.white) + .cornerRadius(10) + } + Spacer() + } + } +} + +struct OnboardingView_Previews: PreviewProvider { + static var delegate = SheetDismisserProtocol() + static var previews: some View { + createOnboardingView(delegate: delegate) + .environmentObject(OnboardingState()) + } +} diff --git a/Monal/Classes/ContactDetails.swift b/Monal/Classes/ContactDetails.swift index 6b16fc8798..87ce059d64 100644 --- a/Monal/Classes/ContactDetails.swift +++ b/Monal/Classes/ContactDetails.swift @@ -86,91 +86,83 @@ struct ContactDetails: View { Form { Section { VStack(spacing: 20) { - Image(uiImage: contact.avatar) - .resizable() - .scaledToFit() - .applyClosure {view in - if contact.isGroup { - if ownAffiliation == "owner" { - view.accessibilityLabel((contact.mucType == "group") ? Text("Change Group Avatar") : Text("Change Channel Avatar")) - .onTapGesture { - showImagePicker() - } - .addTopRight { - if contact.hasAvatar { - Button(action: { - showingRemoveAvatarConfirmation = true - }, label: { - Image(systemName: "xmark.circle.fill") - .resizable() - .frame(width: 24.0, height: 24.0) - .accessibilityLabel((contact.mucType == "group") ? Text("Remove Group Avatar") : Text("Remove Channel Avatar")) - .applyClosure { view in - if #available(iOS 15, *) { - view - .symbolRenderingMode(.palette) - .foregroundStyle(.white, .red) - } else { - view.foregroundColor(.red) + if !contact.isSelfChat { + Image(uiImage: contact.avatar) + .resizable() + .scaledToFit() + .applyClosure {view in + if contact.isGroup { + if ownAffiliation == "owner" { + view.accessibilityLabel((contact.mucType == "group") ? Text("Change Group Avatar") : Text("Change Channel Avatar")) + .onTapGesture { + showImagePicker() + } + .addTopRight { + if contact.hasAvatar { + Button(action: { + showingRemoveAvatarConfirmation = true + }, label: { + Image(systemName: "xmark.circle.fill") + .resizable() + .frame(width: 24.0, height: 24.0) + .accessibilityLabel((contact.mucType == "group") ? Text("Remove Group Avatar") : Text("Remove Channel Avatar")) + .applyClosure { view in + if #available(iOS 15, *) { + view + .symbolRenderingMode(.palette) + .foregroundStyle(.white, .red) + } else { + view.foregroundColor(.red) + } } - } - }) - .buttonStyle(.borderless) - .offset(x: 8, y: -8) - } else { - Button(action: { - showImagePicker() - }, label: { - Image(systemName: "pencil.circle.fill") - .resizable() - .frame(width: 24.0, height: 24.0) - .accessibilityLabel((contact.mucType == "group") ? Text("Change Group Avatar") : Text("Change Channel Avatar")) -// .applyClosure { view in -// if #available(iOS 15, *) { -// view -// .symbolRenderingMode(.palette) -// .foregroundStyle(.primary, .secondary) -// } else { -// view.foregroundColor(.primary) -// } -// } - }) - .buttonStyle(.borderless) - .offset(x: 8, y: -8) + }) + .buttonStyle(.borderless) + .offset(x: 8, y: -8) + } else { + Button(action: { + showImagePicker() + }, label: { + Image(systemName: "pencil.circle.fill") + .resizable() + .frame(width: 24.0, height: 24.0) + .accessibilityLabel((contact.mucType == "group") ? Text("Change Group Avatar") : Text("Change Channel Avatar")) + }) + .buttonStyle(.borderless) + .offset(x: 8, y: -8) + } } - } + } else { + view.accessibilityLabel((contact.mucType == "group") ? Text("Group Avatar") : Text("Channel Avatar")) + } } else { - view.accessibilityLabel((contact.mucType == "group") ? Text("Group Avatar") : Text("Channel Avatar")) + view.accessibilityLabel(Text("Avatar")) } - } else { - view.accessibilityLabel(Text("Avatar")) } - } - .frame(width: 150, height: 150, alignment: .center) - .shadow(radius: 7) - .sheet(isPresented:$showingImagePicker) { - ImagePicker(image:$inputImage) - } - .actionSheet(isPresented: $showingRemoveAvatarConfirmation) { - ActionSheet( - title: Text("Really remove avatar?"), - message: Text("This will remove the current avatar image and revert this group/channel to the default one."), - buttons: [ - .cancel(), - .destructive( - Text("Yes"), - action: { - performMucAction(account:account, mucJid:contact.contactJid, overlay:overlay, headlineView:Text("Removing avatar..."), descriptionView:Text("")) { - self.account.mucProcessor.publishAvatar(nil, forMuc: contact.contactJid) - }.catch { error in - errorAlert(title: Text("Error removing avatar!"), message: Text("\(String(describing:error))")) - hideLoadingOverlay(overlay) + .frame(width: 150, height: 150, alignment: .center) + .shadow(radius: 7) + .actionSheet(isPresented: $showingRemoveAvatarConfirmation) { + ActionSheet( + title: Text("Really remove avatar?"), + message: Text("This will remove the current avatar image and revert this group/channel to the default one."), + buttons: [ + .cancel(), + .destructive( + Text("Yes"), + action: { + 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) + } } - } - ) - ] - ) - } + ) + ] + ) + } + } Button { UIPasteboard.general.setValue(contact.contactJid as String, forPasteboardType:UTType.utf8PlainText.identifier as String) @@ -186,7 +178,18 @@ struct ContactDetails: View { .accessibilityHint("Copies JID") } .buttonStyle(.borderless) - + +// //TODO: wait for account edit to become swiftui +// if contact.isSelfChat { +// Button { +// //TODO: open account edit +// } label: { +// Text("Open account settings") +// .accessibilityHint("Open account settings") +// } +// .buttonStyle(.borderless) +// } + //only show account jid if more than one is configured if MLXMPPManager.sharedInstance().connectedXMPP.count > 1 && !contact.isSelfChat { @@ -241,9 +244,6 @@ struct ContactDetails: View { } } .buttonStyle(.borderless) - .sheet(isPresented: $showingSheetEditSubject) { - LazyClosureView(EditGroupSubject(contact: contact)) - } } else { Text("Group subject:") } @@ -260,40 +260,42 @@ struct ContactDetails: View { // info/nondestructive buttons Section { - Button { - if contact.isGroup { - if !contact.isMuted && !contact.isMentionOnly { - contact.obj.toggleMentionOnly(true) - } else if !contact.isMuted && contact.isMentionOnly { - contact.obj.toggleMentionOnly(false) - contact.obj.toggleMute(true) + if !contact.isSelfChat { + Button { + if contact.isGroup { + if !contact.isMuted && !contact.isMentionOnly { + contact.obj.toggleMentionOnly(true) + } else if !contact.isMuted && contact.isMentionOnly { + contact.obj.toggleMentionOnly(false) + contact.obj.toggleMute(true) + } else { + contact.obj.toggleMentionOnly(false) + contact.obj.toggleMute(false) + } } else { - contact.obj.toggleMentionOnly(false) - contact.obj.toggleMute(false) + contact.obj.toggleMute(!contact.isMuted) } - } else { - contact.obj.toggleMute(!contact.isMuted) - } - } label: { - if contact.isMuted { - Label { - contact.isGroup ? Text("Notifications disabled") : Text("Contact is muted") - } icon: { - Image(systemName: "bell.slash.fill") - .foregroundColor(.red) - } - } else if contact.isGroup && contact.isMentionOnly { - Label { - Text("Notify only when mentioned") - } icon: { - Image(systemName: "bell.badge") - } - } else { - Label { - contact.isGroup ? Text("Notify on all messages") : Text("Contact is not muted") - } icon: { - Image(systemName: "bell.fill") - .foregroundColor(.green) + } label: { + if contact.isMuted { + Label { + contact.isGroup ? Text("Notifications disabled") : Text("Contact is muted") + } icon: { + Image(systemName: "bell.slash.fill") + .foregroundColor(.red) + } + } else if contact.isGroup && contact.isMentionOnly { + Label { + Text("Notify only when mentioned") + } icon: { + Image(systemName: "bell.badge") + } + } else { + Label { + contact.isGroup ? Text("Notify on all messages") : Text("Contact is not muted") + } icon: { + Image(systemName: "bell.fill") + .foregroundColor(.green) + } } } } @@ -372,12 +374,8 @@ struct ContactDetails: View { } #if !DISABLE_OMEMO - if !HelperTools.isContactBlacklisted(forEncryption:contact.obj) { - if !contact.isGroup { - NavigationLink(destination: LazyClosureView(OmemoKeys(contact: contact))) { - contact.isSelfChat ? Text("Own Encryption Keys") : Text("Encryption Keys") - } - } else if contact.mucType == "group" { + if !HelperTools.isContactBlacklisted(forEncryption:contact.obj) && !contact.isSelfChat { + if !contact.isGroup || contact.mucType == "group" { NavigationLink(destination: LazyClosureView(OmemoKeys(contact: contact))) { Text("Encryption Keys") } @@ -546,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 @@ -555,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) } } ) @@ -644,12 +642,29 @@ struct ContactDetails: View { } })) } - .onChange(of:inputImage) { _ in - performMucAction(account:account, mucJid:contact.contactJid, overlay:overlay, headlineView:Text("Uploading avatar..."), descriptionView:Text("")) { - self.account.mucProcessor.publishAvatar(inputImage, forMuc: contact.contactJid) - }.catch { error in - errorAlert(title: Text("Error changing avatar!"), message: Text("\(String(describing:error))")) - hideLoadingOverlay(overlay) + .sheet(isPresented: $showingSheetEditSubject) { + LazyClosureView(EditGroupSubject(contact: contact)) + } + .sheet(isPresented:$showingImagePicker) { + ImagePicker(image:$inputImage) + } + .sheet(isPresented: $inputImage.optionalMappedToBool()) { + ImageCropView(originalImage: inputImage!, configureBlock: { cropViewController in + cropViewController.aspectRatioPreset = .presetSquare + cropViewController.aspectRatioLockEnabled = true + cropViewController.aspectRatioPickerButtonHidden = true + cropViewController.resetAspectRatioEnabled = false + }, onCanceled: { + inputImage = nil + }) { (image, cropRect, angle) in + 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) + } } } .onChange(of:contact.avatar as UIImage) { _ in diff --git a/Monal/Classes/ContactEntry.swift b/Monal/Classes/ContactEntry.swift index ce3ccea0c9..55069e7cc3 100644 --- a/Monal/Classes/ContactEntry.swift +++ b/Monal/Classes/ContactEntry.swift @@ -9,19 +9,33 @@ struct ContactEntry: View { let contact: ObservableKVOWrapper let selfnotesPrefix: Bool + let fallback: String? @ViewBuilder let additionalContent: () -> AdditionalContent - init(contact:ObservableKVOWrapper, selfnotesPrefix: Bool = true) where AdditionalContent == EmptyView { - self.init(contact:contact, selfnotesPrefix:selfnotesPrefix, additionalContent:{ EmptyView() }) + init(contact:ObservableKVOWrapper, selfnotesPrefix: Bool = true, fallback: String? = nil) where AdditionalContent == EmptyView { + self.init(contact:contact, selfnotesPrefix:selfnotesPrefix, fallback:fallback, additionalContent:{ EmptyView() }) + } + + init(contact:ObservableKVOWrapper, fallback: String?) where AdditionalContent == EmptyView { + self.init(contact:contact, selfnotesPrefix:true, fallback:fallback, additionalContent:{ EmptyView() }) } init(contact:ObservableKVOWrapper, @ViewBuilder additionalContent: @escaping () -> AdditionalContent) { self.init(contact:contact, selfnotesPrefix:true, additionalContent:additionalContent) } + init(contact:ObservableKVOWrapper, fallback: String?, @ViewBuilder additionalContent: @escaping () -> AdditionalContent) { + self.init(contact:contact, selfnotesPrefix:true, fallback:fallback, additionalContent:additionalContent) + } + init(contact:ObservableKVOWrapper, selfnotesPrefix: Bool, @ViewBuilder additionalContent: @escaping () -> AdditionalContent) { + self.init(contact:contact, selfnotesPrefix:selfnotesPrefix, fallback:nil, additionalContent:additionalContent) + } + + init(contact:ObservableKVOWrapper, selfnotesPrefix: Bool, fallback: String?, @ViewBuilder additionalContent: @escaping () -> AdditionalContent) { self.contact = contact self.selfnotesPrefix = selfnotesPrefix + self.fallback = fallback self.additionalContent = additionalContent } @@ -33,9 +47,17 @@ struct ContactEntry: View { .frame(width: 40, height: 40, alignment: .center) VStack(alignment: .leading) { if selfnotesPrefix { - Text(contact.contactDisplayName as String) + // use the if to make sure this view gets updated if the contact display name changes + // (the condition is never false, because contactDisplayName can not be nil) + if (contact.contactDisplayName as String?) != nil { + Text(contact.obj.contactDisplayName(withFallback:fallback)) + } } else { - Text(contact.contactDisplayNameWithoutSelfnotesPrefix as String) + // use the if to make sure this view gets updated if the contact display name changes + // (the condition is never false, because contactDisplayNameWithoutSelfnotesPrefix can not be nil) + if (contact.contactDisplayNameWithoutSelfnotesPrefix as String?) != nil { + Text(contact.obj.contactDisplayName(withFallback:fallback, andSelfnotesPrefix:false)) + } } additionalContent() Text(contact.contactJid as String) diff --git a/Monal/Classes/ContactRequestsMenu.swift b/Monal/Classes/ContactRequestsMenu.swift index 040d3283af..cb03d545b4 100644 --- a/Monal/Classes/ContactRequestsMenu.swift +++ b/Monal/Classes/ContactRequestsMenu.swift @@ -8,34 +8,28 @@ struct ContactRequestsMenuEntry: View { let contact : MLContact - let doDelete: () -> () @State private var isDeleted = false - private func delete() { - if(isDeleted == false) { - isDeleted = true - doDelete() - } - } - var body: some View { HStack { Text(contact.contactJid) + .foregroundColor(.secondary) + Spacer() + Group { Button { let appDelegate = UIApplication.shared.delegate as! MonalAppDelegate appDelegate.openChat(of:contact) } label: { Image(systemName: "text.bubble") - .accentColor(.black) + .accentColor(.primary) } //see https://www.hackingwithswift.com/forums/swiftui/tap-button-in-hstack-activates-all-button-actions-ios-14-swiftui-2/2952 .buttonStyle(BorderlessButtonStyle()) Button { // deny request - self.delete() //update ui first because the array index can change afterwards MLXMPPManager.sharedInstance().remove(contact) } label: { Image(systemName: "trash.circle") @@ -46,7 +40,6 @@ struct ContactRequestsMenuEntry: View { Button { // accept request - self.delete() //update ui first because the array index can change afterwards MLXMPPManager.sharedInstance().add(contact) let appDelegate = UIApplication.shared.delegate as! MonalAppDelegate appDelegate.openChat(of:contact) @@ -63,47 +56,65 @@ struct ContactRequestsMenuEntry: View { } struct ContactRequestsMenu: View { - var delegate: SheetDismisserProtocol - @State private var pendingRequests: [MLContact] - + @State var pendingRequests: [xmpp:[MLContact]] = [:] + @State var connectedAccounts: [Int:xmpp] = [:] + + func updateRequests() { + let requests = DataLayer.sharedInstance().allContactRequests() as! [MLContact] + connectedAccounts.removeAll() + for account in MLXMPPManager.sharedInstance().connectedXMPP as! [xmpp] { + connectedAccounts[account.accountNo.intValue] = account + } + pendingRequests.removeAll() + for contact in requests { + //add only requests having an enabled (dubbed connected) account + //(should be a noop because allContactRequests() returns only enabled accounts) + if let account = connectedAccounts[contact.accountId.intValue] { + if pendingRequests[account] == nil { + pendingRequests[account] = [] + } + pendingRequests[account]!.append(contact) + } + } + } + var body: some View { - Form { - List { - Section(header: Text("Allowing someone to add you as a contact lets them see your profile picture and when you are online.")) { - if(pendingRequests.isEmpty) { - Text("No pending requests") - .foregroundColor(.secondary) - } - ForEach(pendingRequests.indices, id: \.self) { idx in - ContactRequestsMenuEntry( - contact: pendingRequests[idx], - doDelete: { - self.pendingRequests.remove(at: idx) + Section(header: Text("Allowing someone to add you as a contact lets them see your profile picture and when you are online.")) { + if(pendingRequests.isEmpty) { + Text("No pending constact requests") + .foregroundColor(.secondary) + } else { + List { + ForEach(pendingRequests.sorted(by:{ $0.0.connectionProperties.identity.jid < $1.0.connectionProperties.identity.jid }), id: \.key) { account, requests in + if connectedAccounts.count == 1 { + ForEach(requests.indices, id: \.self) { idx in + ContactRequestsMenuEntry(contact: requests[idx]) } - ) + } else { + Section(header: Text("Account: \(account.connectionProperties.identity.jid)")) { + ForEach(requests.indices, id: \.self) { idx in + ContactRequestsMenuEntry(contact: requests[idx]) + } + } + } } } } } - .navigationBarTitle(Text("Contact Requests"), displayMode: .inline) - .navigationViewStyle(.stack) .onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("kMonalContactRefresh")).receive(on: RunLoop.main)) { notification in - self.pendingRequests = DataLayer.sharedInstance().allContactRequests() as! [MLContact] + updateRequests() } .onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("kMonalContactRemoved")).receive(on: RunLoop.main)) { notification in - self.pendingRequests = DataLayer.sharedInstance().allContactRequests() as! [MLContact] + updateRequests() + } + .onAppear { + updateRequests() } - } - - init(delegate: SheetDismisserProtocol) { - self.delegate = delegate - 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/ContactsViewController.m b/Monal/Classes/ContactsViewController.m index 95339d6c64..674d60df5b 100644 --- a/Monal/Classes/ContactsViewController.m +++ b/Monal/Classes/ContactsViewController.m @@ -53,23 +53,20 @@ -(void) openCreateGroup:(id) sender [self presentViewController:createGroupView animated:YES completion:^{}]; } --(void) openContactRequests:(id) sender +-(void) configureAddContactImage { - UIViewController* contactRequestsView = [[SwiftuiInterface new] makeViewWithName:@"ContactRequests"]; - [self presentViewController:contactRequestsView animated:YES completion:^{}]; -} - --(void) configureContactRequestsImage -{ - UIImage* requestsImage = [[UIImage systemImageNamed:@"person.crop.circle.fill.badge.questionmark"] imageWithTintColor:UIColor.monalGreen]; - UITapGestureRecognizer* requestsTapRecoginzer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(openContactRequests:)]; - self.navigationItem.rightBarButtonItems[1].customView = [HelperTools - buttonWithNotificationBadgeForImage:requestsImage + UIImage* image = [[UIImage systemImageNamed:@"person.fill.badge.plus"] imageWithTintColor:UIColor.monalGreen]; + UITapGestureRecognizer* tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(openAddContacts:)]; + self.navigationItem.rightBarButtonItems[0].customView = [HelperTools + buttonWithNotificationBadgeForImage:image 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", @"")]; - + withTapHandler:tapRecognizer]; + [self.navigationItem.rightBarButtonItems[0] setIsAccessibilityElement:YES]; + if([[DataLayer sharedInstance] allContactRequests].count > 0) + [self.navigationItem.rightBarButtonItems[0] setAccessibilityLabel:NSLocalizedString(@"Add contact (contact requests pending)", @"")]; + else + [self.navigationItem.rightBarButtonItems[0] setAccessibilityLabel:NSLocalizedString(@"Add contact", @"")]; + [self.navigationItem.rightBarButtonItems[0] setAccessibilityTraits:UIAccessibilityTraitButton]; } #pragma mark view life cycle @@ -104,16 +101,15 @@ -(void) viewDidLoad self.tableView.emptyDataSetSource = self; self.tableView.emptyDataSetDelegate = self; - UIBarButtonItem* addContact = [UIBarButtonItem new]; - addContact.image = [UIImage systemImageNamed:@"person.fill.badge.plus"]; - [addContact setAction:@selector(openAddContacts:)]; - UIBarButtonItem* createGroup = [[UIBarButtonItem alloc] init]; - createGroup.image = [UIImage systemImageNamed:@"person.3.fill"]; - [createGroup setAction:@selector(openCreateGroup:)]; - self.navigationItem.rightBarButtonItems = [[NSArray alloc] initWithObjects:addContact, [[UIBarButtonItem alloc] init], createGroup, nil]; + createGroup.image = [UIImage systemImageNamed:@"person.3.fill"]; + createGroup.accessibilityLabel = @"Create contact group"; + [createGroup setAction:@selector(openCreateGroup:)]; + [createGroup setTarget:self]; + + self.navigationItem.rightBarButtonItems = @[[[UIBarButtonItem alloc] init], createGroup]; - [self configureContactRequestsImage]; + [self configureAddContactImage]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleContactUpdate) name:kMonalContactRemoved object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleContactUpdate) name:kMonalContactRefresh object:nil]; @@ -189,7 +185,7 @@ -(BOOL) canBecomeFirstResponder -(void) reloadTable { - [self configureContactRequestsImage]; + [self configureAddContactImage]; if(self.contactsTable.hasUncommittedUpdates) return; [self.contactsTable reloadData]; @@ -239,9 +235,14 @@ -(void) loadContactsWithFilter:(NSString*) filter { if(!contact.isSelfChat) onlySelfChats = NO; - //ignore all contacts not at least in subscribedTo or asking state - if(contact.isInRoster) +#ifdef IS_QUICKSY + [contactsToDisplay addObject:contact]; +#else + //ignore all contacts not at least in any roster state: e.g. subscribedTo or asking state + //OR is subscribedFrom (e.g. we approved them already, bit they don't approve us) + if((contact.isSubscribedTo || contact.hasOutgoingContactRequest) || contact.isSubscribedFrom) [contactsToDisplay addObject:contact]; +#endif } if(!onlySelfChats) self.contacts = contactsToDisplay; diff --git a/Monal/Classes/CountryCodes.swift b/Monal/Classes/CountryCodes.swift new file mode 100644 index 0000000000..8d306d0e82 --- /dev/null +++ b/Monal/Classes/CountryCodes.swift @@ -0,0 +1,243 @@ +// This file was automatically generated by scripts/itu_pdf_to_swift.py +// Please run this python script again to update this file +// Example ../scripts/itu_pdf_to_swift.py > Classes/CountryCodes.swift + +public struct Quicksy_Country: Identifiable, Hashable { + public let id = UUID() + public let name: String + public let code: String + public let pattern: String +} + +public let COUNTRY_CODES: [Quicksy_Country] = [ + Quicksy_Country(name: NSLocalizedString("Afghanistan", comment:"quicksy country"), code: "+93", pattern: "^([0-9]{9})$") , + Quicksy_Country(name: NSLocalizedString("Albania", comment:"quicksy country"), code: "+355", pattern: "^([0-9]{3,9})$") , + Quicksy_Country(name: NSLocalizedString("Algeria", comment:"quicksy country"), code: "+213", pattern: "^([0-9]{8}|[0-9]{9})$") , + Quicksy_Country(name: NSLocalizedString("American Samoa", comment:"quicksy country"), code: "+1", pattern: "^(684)([0-9]{7})$") , + Quicksy_Country(name: NSLocalizedString("Andorra", comment:"quicksy country"), code: "+376", pattern: "^([0-9]{6}|[0-9]{8}|[0-9]{9})$") , + Quicksy_Country(name: NSLocalizedString("Angola", comment:"quicksy country"), code: "+244", pattern: "^([0-9]{9})$") , + Quicksy_Country(name: NSLocalizedString("Anguilla", comment:"quicksy country"), code: "+1", pattern: "^(264)([0-9]{7})$") , + Quicksy_Country(name: NSLocalizedString("Antigua and Barbuda", comment:"quicksy country"), code: "+1", pattern: "^(268)([0-9]{7})$") , + Quicksy_Country(name: NSLocalizedString("Argentina", comment:"quicksy country"), code: "+54", pattern: "^([0-9]{10})$") , + Quicksy_Country(name: NSLocalizedString("Armenia", comment:"quicksy country"), code: "+374", pattern: "^([0-9]{8})$") , + Quicksy_Country(name: NSLocalizedString("Aruba", comment:"quicksy country"), code: "+297", pattern: "^([0-9]{7})$") , + Quicksy_Country(name: NSLocalizedString("Australia", comment:"quicksy country"), code: "+61", pattern: "^([0-9]{5,15})$") , + Quicksy_Country(name: NSLocalizedString("Australian External Territories", comment:"quicksy country"), code: "+672", pattern: "^([0-9]{6})$") , + Quicksy_Country(name: NSLocalizedString("Austria", comment:"quicksy country"), code: "+43", pattern: "^([0-9]{4,13})$") , + Quicksy_Country(name: NSLocalizedString("Azerbaijan", comment:"quicksy country"), code: "+994", pattern: "^([0-9]{8,9})$") , + Quicksy_Country(name: NSLocalizedString("Bahamas", comment:"quicksy country"), code: "+1", pattern: "^(242)([0-9]{7})$") , + Quicksy_Country(name: NSLocalizedString("Bahrain", comment:"quicksy country"), code: "+973", pattern: "^([0-9]{8})$") , + Quicksy_Country(name: NSLocalizedString("Bangladesh", comment:"quicksy country"), code: "+880", pattern: "^([0-9]{6,10})$") , + Quicksy_Country(name: NSLocalizedString("Barbados", comment:"quicksy country"), code: "+1", pattern: "^(246)([0-9]{7})$") , + Quicksy_Country(name: NSLocalizedString("Belarus", comment:"quicksy country"), code: "+375", pattern: "^([0-9]{9,10})$") , + Quicksy_Country(name: NSLocalizedString("Belgium", comment:"quicksy country"), code: "+32", pattern: "^([0-9]{8,9})$") , + Quicksy_Country(name: NSLocalizedString("Belize", comment:"quicksy country"), code: "+501", pattern: "^([0-9]{7})$") , + Quicksy_Country(name: NSLocalizedString("Benin", comment:"quicksy country"), code: "+229", pattern: "^([0-9]{8})$") , + Quicksy_Country(name: NSLocalizedString("Bermuda", comment:"quicksy country"), code: "+1", pattern: "^(441)([0-9]{7})$") , + Quicksy_Country(name: NSLocalizedString("Bhutan", comment:"quicksy country"), code: "+975", pattern: "^([0-9]{7,8})$") , + Quicksy_Country(name: NSLocalizedString("Bolivia (Plurinational State of)", comment:"quicksy country"), code: "+591", pattern: "^([0-9]{8})$") , + Quicksy_Country(name: NSLocalizedString("Bonaire, Sint Eustatius and Saba", comment:"quicksy country"), code: "+599", pattern: "^([0-9]{7})$") , + Quicksy_Country(name: NSLocalizedString("Bosnia and Herzegovina", comment:"quicksy country"), code: "+387", pattern: "^([0-9]{8})$") , + Quicksy_Country(name: NSLocalizedString("Botswana", comment:"quicksy country"), code: "+267", pattern: "^([0-9]{7,8})$") , + Quicksy_Country(name: NSLocalizedString("Brazil", comment:"quicksy country"), code: "+55", pattern: "^([0-9]{10})$") , + Quicksy_Country(name: NSLocalizedString("British Virgin Islands", comment:"quicksy country"), code: "+1", pattern: "^(284)([0-9]{7})$") , + Quicksy_Country(name: NSLocalizedString("Brunei Darussalam", comment:"quicksy country"), code: "+673", pattern: "^([0-9]{7})$") , + Quicksy_Country(name: NSLocalizedString("Bulgaria", comment:"quicksy country"), code: "+359", pattern: "^([0-9]{7,9})$") , + Quicksy_Country(name: NSLocalizedString("Burkina Faso", comment:"quicksy country"), code: "+226", pattern: "^([0-9]{8})$") , + Quicksy_Country(name: NSLocalizedString("Burundi", comment:"quicksy country"), code: "+257", pattern: "^([0-9]{8})$") , + Quicksy_Country(name: NSLocalizedString("Cambodia", comment:"quicksy country"), code: "+855", pattern: "^([0-9]{8})$") , + Quicksy_Country(name: NSLocalizedString("Cameroon", comment:"quicksy country"), code: "+237", pattern: "^([0-9]{8})$") , + Quicksy_Country(name: NSLocalizedString("Canada", comment:"quicksy country"), code: "+1", pattern: "^([0-9]{10})$") , + Quicksy_Country(name: NSLocalizedString("Cape Verde", comment:"quicksy country"), code: "+238", pattern: "^([0-9]{7})$") , + Quicksy_Country(name: NSLocalizedString("Cayman Islands", comment:"quicksy country"), code: "+1", pattern: "^(345)([0-9]{7})$") , + Quicksy_Country(name: NSLocalizedString("Central African Rep.", comment:"quicksy country"), code: "+236", pattern: "^([0-9]{8})$") , + Quicksy_Country(name: NSLocalizedString("Chad", comment:"quicksy country"), code: "+235", pattern: "^([0-9]{8})$") , + Quicksy_Country(name: NSLocalizedString("Chile", comment:"quicksy country"), code: "+56", pattern: "^([0-9]{8,9})$") , + Quicksy_Country(name: NSLocalizedString("China", comment:"quicksy country"), code: "+86", pattern: "^([0-9]{5,12})$") , + Quicksy_Country(name: NSLocalizedString("Colombia", comment:"quicksy country"), code: "+57", pattern: "^([0-9]{8}|[0-9]{10})$") , + Quicksy_Country(name: NSLocalizedString("Comoros", comment:"quicksy country"), code: "+269", pattern: "^([0-9]{7})$") , + Quicksy_Country(name: NSLocalizedString("Congo", comment:"quicksy country"), code: "+242", pattern: "^([0-9]{9})$") , + Quicksy_Country(name: NSLocalizedString("Cook Islands", comment:"quicksy country"), code: "+682", pattern: "^([0-9]{5})$") , + Quicksy_Country(name: NSLocalizedString("Costa Rica", comment:"quicksy country"), code: "+506", pattern: "^([0-9]{8})$") , + Quicksy_Country(name: NSLocalizedString("Côte d'Ivoire", comment:"quicksy country"), code: "+225", pattern: "^([0-9]{8})$") , + Quicksy_Country(name: NSLocalizedString("Croatia", comment:"quicksy country"), code: "+385", pattern: "^([0-9]{8,12})$") , + Quicksy_Country(name: NSLocalizedString("Cuba", comment:"quicksy country"), code: "+53", pattern: "^([0-9]{6,8})$") , + Quicksy_Country(name: NSLocalizedString("Curaçao", comment:"quicksy country"), code: "+599", pattern: "^([0-9]{7,8})$") , + Quicksy_Country(name: NSLocalizedString("Cyprus", comment:"quicksy country"), code: "+357", pattern: "^([0-9]{8}|[0-9]{11})$") , + Quicksy_Country(name: NSLocalizedString("Czech Rep.", comment:"quicksy country"), code: "+420", pattern: "^([0-9]{4,12})$") , + Quicksy_Country(name: NSLocalizedString("Dem. People's Rep. of Korea", comment:"quicksy country"), code: "+850", pattern: "^([0-9]{6,17})$") , + Quicksy_Country(name: NSLocalizedString("Dem. Rep. of the Congo", comment:"quicksy country"), code: "+243", pattern: "^([0-9]{5,9})$") , + Quicksy_Country(name: NSLocalizedString("Denmark", comment:"quicksy country"), code: "+45", pattern: "^([0-9]{8})$") , + Quicksy_Country(name: NSLocalizedString("Diego Garcia", comment:"quicksy country"), code: "+246", pattern: "^([0-9]{7})$") , + Quicksy_Country(name: NSLocalizedString("Djibouti", comment:"quicksy country"), code: "+253", pattern: "^([0-9]{6})$") , + Quicksy_Country(name: NSLocalizedString("Dominica", comment:"quicksy country"), code: "+1", pattern: "^(767)([0-9]{7})$") , + Quicksy_Country(name: NSLocalizedString("Dominican Rep.", comment:"quicksy country"), code: "+1", pattern: "^(809|829)([0-9]{7})$") , + Quicksy_Country(name: NSLocalizedString("Ecuador", comment:"quicksy country"), code: "+593", pattern: "^([0-9]{8})$") , + Quicksy_Country(name: NSLocalizedString("Egypt", comment:"quicksy country"), code: "+20", pattern: "^([0-9]{7,9})$") , + Quicksy_Country(name: NSLocalizedString("El Salvador", comment:"quicksy country"), code: "+503", pattern: "^([0-9]{7}|[0-9]{8}|[0-9]{11})$") , + Quicksy_Country(name: NSLocalizedString("Equatorial Guinea", comment:"quicksy country"), code: "+240", pattern: "^([0-9]{9})$") , + Quicksy_Country(name: NSLocalizedString("Eritrea", comment:"quicksy country"), code: "+291", pattern: "^([0-9]{7})$") , + Quicksy_Country(name: NSLocalizedString("Estonia", comment:"quicksy country"), code: "+372", pattern: "^([0-9]{7,10})$") , + Quicksy_Country(name: NSLocalizedString("Ethiopia", comment:"quicksy country"), code: "+251", pattern: "^([0-9]{9})$") , + Quicksy_Country(name: NSLocalizedString("Falkland Islands (Malvinas)", comment:"quicksy country"), code: "+500", pattern: "^([0-9]{5})$") , + Quicksy_Country(name: NSLocalizedString("Faroe Islands", comment:"quicksy country"), code: "+298", pattern: "^([0-9]{6})$") , + Quicksy_Country(name: NSLocalizedString("Fiji", comment:"quicksy country"), code: "+679", pattern: "^([0-9]{7})$") , + Quicksy_Country(name: NSLocalizedString("Finland", comment:"quicksy country"), code: "+358", pattern: "^([0-9]{5,12})$") , + Quicksy_Country(name: NSLocalizedString("France", comment:"quicksy country"), code: "+33", pattern: "^([0-9]{9})$") , + Quicksy_Country(name: NSLocalizedString("French Departments and Territories in the Indian Ocean", comment:"quicksy country"), code: "+262", pattern: "^([0-9]{9})$") , + Quicksy_Country(name: NSLocalizedString("French Guiana", comment:"quicksy country"), code: "+594", pattern: "^([0-9]{9})$") , + Quicksy_Country(name: NSLocalizedString("French Polynesia", comment:"quicksy country"), code: "+689", pattern: "^([0-9]{6})$") , + Quicksy_Country(name: NSLocalizedString("Gabon", comment:"quicksy country"), code: "+241", pattern: "^([0-9]{6}|[0-9]{7})$") , + Quicksy_Country(name: NSLocalizedString("Gambia", comment:"quicksy country"), code: "+220", pattern: "^([0-9]{7})$") , + Quicksy_Country(name: NSLocalizedString("Georgia", comment:"quicksy country"), code: "+995", pattern: "^([0-9]{9})$") , + Quicksy_Country(name: NSLocalizedString("Germany", comment:"quicksy country"), code: "+49", pattern: "^([0-9]{6,13})$") , + Quicksy_Country(name: NSLocalizedString("Ghana", comment:"quicksy country"), code: "+233", pattern: "^([0-9]{5,9})$") , + Quicksy_Country(name: NSLocalizedString("Gibraltar", comment:"quicksy country"), code: "+350", pattern: "^([0-9]{8})$") , + Quicksy_Country(name: NSLocalizedString("Greece", comment:"quicksy country"), code: "+30", pattern: "^([0-9]{10})$") , + Quicksy_Country(name: NSLocalizedString("Greenland", comment:"quicksy country"), code: "+299", pattern: "^([0-9]{6})$") , + Quicksy_Country(name: NSLocalizedString("Grenada", comment:"quicksy country"), code: "+1", pattern: "^(473)([0-9]{7})$") , + Quicksy_Country(name: NSLocalizedString("Guadeloupe", comment:"quicksy country"), code: "+590", pattern: "^([0-9]{9})$") , + Quicksy_Country(name: NSLocalizedString("Guam", comment:"quicksy country"), code: "+1", pattern: "^(671)([0-9]{7})$") , + Quicksy_Country(name: NSLocalizedString("Guatemala", comment:"quicksy country"), code: "+502", pattern: "^([0-9]{8})$") , + Quicksy_Country(name: NSLocalizedString("Guinea", comment:"quicksy country"), code: "+224", pattern: "^([0-9]{8})$") , + Quicksy_Country(name: NSLocalizedString("Guinea-Bissau", comment:"quicksy country"), code: "+245", pattern: "^([0-9]{7})$") , + Quicksy_Country(name: NSLocalizedString("Guyana", comment:"quicksy country"), code: "+592", pattern: "^([0-9]{7})$") , + Quicksy_Country(name: NSLocalizedString("Haiti", comment:"quicksy country"), code: "+509", pattern: "^([0-9]{8})$") , + Quicksy_Country(name: NSLocalizedString("Honduras", comment:"quicksy country"), code: "+504", pattern: "^([0-9]{8})$") , + Quicksy_Country(name: NSLocalizedString("Hong Kong, China", comment:"quicksy country"), code: "+852", pattern: "^([0-9]{4}|[0-9]{8,9})$") , + Quicksy_Country(name: NSLocalizedString("Hungary", comment:"quicksy country"), code: "+36", pattern: "^([0-9]{8,9})$") , + Quicksy_Country(name: NSLocalizedString("Iceland", comment:"quicksy country"), code: "+354", pattern: "^([0-9]{7}|[0-9]{9})$") , + Quicksy_Country(name: NSLocalizedString("India", comment:"quicksy country"), code: "+91", pattern: "^([0-9]{7,10})$") , + Quicksy_Country(name: NSLocalizedString("Indonesia", comment:"quicksy country"), code: "+62", pattern: "^([0-9]{5,10})$") , + Quicksy_Country(name: NSLocalizedString("Iran (Islamic Republic of)", comment:"quicksy country"), code: "+98", pattern: "^([0-9]{6,10})$") , + Quicksy_Country(name: NSLocalizedString("Iraq", comment:"quicksy country"), code: "+964", pattern: "^([0-9]{8,10})$") , + Quicksy_Country(name: NSLocalizedString("Ireland", comment:"quicksy country"), code: "+353", pattern: "^([0-9]{7,11})$") , + Quicksy_Country(name: NSLocalizedString("Israel", comment:"quicksy country"), code: "+972", pattern: "^([0-9]{8,9})$") , + Quicksy_Country(name: NSLocalizedString("Italy", comment:"quicksy country"), code: "+39", pattern: "^([0-9]{1,11})$") , + Quicksy_Country(name: NSLocalizedString("Jamaica", comment:"quicksy country"), code: "+1", pattern: "^(876)([0-9]{7})$") , + Quicksy_Country(name: NSLocalizedString("Japan", comment:"quicksy country"), code: "+81", pattern: "^([0-9]{5,13})$") , + Quicksy_Country(name: NSLocalizedString("Jordan", comment:"quicksy country"), code: "+962", pattern: "^([0-9]{5,9})$") , + Quicksy_Country(name: NSLocalizedString("Kazakhstan", comment:"quicksy country"), code: "+7", pattern: "^([0-9]{10})$") , + Quicksy_Country(name: NSLocalizedString("Kenya", comment:"quicksy country"), code: "+254", pattern: "^([0-9]{6,10})$") , + Quicksy_Country(name: NSLocalizedString("Kiribati", comment:"quicksy country"), code: "+686", pattern: "^([0-9]{5})$") , + Quicksy_Country(name: NSLocalizedString("Korea (Rep. of)", comment:"quicksy country"), code: "+82", pattern: "^([0-9]{8,11})$") , + Quicksy_Country(name: NSLocalizedString("Kuwait", comment:"quicksy country"), code: "+965", pattern: "^([0-9]{7}|[0-9]{8})$") , + Quicksy_Country(name: NSLocalizedString("Kyrgyzstan", comment:"quicksy country"), code: "+996", pattern: "^([0-9]{9})$") , + Quicksy_Country(name: NSLocalizedString("Lao P.D.R.", comment:"quicksy country"), code: "+856", pattern: "^([0-9]{8,10})$") , + Quicksy_Country(name: NSLocalizedString("Latvia", comment:"quicksy country"), code: "+371", pattern: "^([0-9]{7}|[0-9]{8})$") , + Quicksy_Country(name: NSLocalizedString("Lebanon", comment:"quicksy country"), code: "+961", pattern: "^([0-9]{7,8})$") , + Quicksy_Country(name: NSLocalizedString("Lesotho", comment:"quicksy country"), code: "+266", pattern: "^([0-9]{8})$") , + Quicksy_Country(name: NSLocalizedString("Liberia", comment:"quicksy country"), code: "+231", pattern: "^([0-9]{7,8})$") , + Quicksy_Country(name: NSLocalizedString("Libya", comment:"quicksy country"), code: "+218", pattern: "^([0-9]{8,9})$") , + Quicksy_Country(name: NSLocalizedString("Liechtenstein", comment:"quicksy country"), code: "+423", pattern: "^([0-9]{7,9})$") , + Quicksy_Country(name: NSLocalizedString("Lithuania", comment:"quicksy country"), code: "+370", pattern: "^([0-9]{8})$") , + Quicksy_Country(name: NSLocalizedString("Luxembourg", comment:"quicksy country"), code: "+352", pattern: "^([0-9]{4,11})$") , + Quicksy_Country(name: NSLocalizedString("Macao, China", comment:"quicksy country"), code: "+853", pattern: "^([0-9]{7,8})$") , + Quicksy_Country(name: NSLocalizedString("Madagascar", comment:"quicksy country"), code: "+261", pattern: "^([0-9]{9,10})$") , + Quicksy_Country(name: NSLocalizedString("Malawi", comment:"quicksy country"), code: "+265", pattern: "^([0-9]{7}|[0-9]{8})$") , + Quicksy_Country(name: NSLocalizedString("Malaysia", comment:"quicksy country"), code: "+60", pattern: "^([0-9]{7,9})$") , + Quicksy_Country(name: NSLocalizedString("Maldives", comment:"quicksy country"), code: "+960", pattern: "^([0-9]{7})$") , + Quicksy_Country(name: NSLocalizedString("Mali", comment:"quicksy country"), code: "+223", pattern: "^([0-9]{8})$") , + Quicksy_Country(name: NSLocalizedString("Malta", comment:"quicksy country"), code: "+356", pattern: "^([0-9]{8})$") , + Quicksy_Country(name: NSLocalizedString("Marshall Islands", comment:"quicksy country"), code: "+692", pattern: "^([0-9]{7})$") , + Quicksy_Country(name: NSLocalizedString("Martinique", comment:"quicksy country"), code: "+596", pattern: "^([0-9]{9})$") , + Quicksy_Country(name: NSLocalizedString("Mauritania", comment:"quicksy country"), code: "+222", pattern: "^([0-9]{7})$") , + Quicksy_Country(name: NSLocalizedString("Mauritius", comment:"quicksy country"), code: "+230", pattern: "^([0-9]{7})$") , + Quicksy_Country(name: NSLocalizedString("Mexico", comment:"quicksy country"), code: "+52", pattern: "^([0-9]{10})$") , + Quicksy_Country(name: NSLocalizedString("Micronesia", comment:"quicksy country"), code: "+691", pattern: "^([0-9]{7})$") , + Quicksy_Country(name: NSLocalizedString("Moldova (Republic of)", comment:"quicksy country"), code: "+373", pattern: "^([0-9]{8})$") , + Quicksy_Country(name: NSLocalizedString("Monaco", comment:"quicksy country"), code: "+377", pattern: "^([0-9]{5,9})$") , + Quicksy_Country(name: NSLocalizedString("Mongolia", comment:"quicksy country"), code: "+976", pattern: "^([0-9]{7,8})$") , + Quicksy_Country(name: NSLocalizedString("Montenegro", comment:"quicksy country"), code: "+382", pattern: "^([0-9]{4,12})$") , + Quicksy_Country(name: NSLocalizedString("Montserrat", comment:"quicksy country"), code: "+1", pattern: "^(664)([0-9]{7})$") , + Quicksy_Country(name: NSLocalizedString("Morocco", comment:"quicksy country"), code: "+212", pattern: "^([0-9]{9})$") , + Quicksy_Country(name: NSLocalizedString("Mozambique", comment:"quicksy country"), code: "+258", pattern: "^([0-9]{8,9})$") , + Quicksy_Country(name: NSLocalizedString("Myanmar", comment:"quicksy country"), code: "+95", pattern: "^([0-9]{7,9})$") , + Quicksy_Country(name: NSLocalizedString("Namibia", comment:"quicksy country"), code: "+264", pattern: "^([0-9]{6,10})$") , + Quicksy_Country(name: NSLocalizedString("Nauru", comment:"quicksy country"), code: "+674", pattern: "^([0-9]{4}|[0-9]{7})$") , + Quicksy_Country(name: NSLocalizedString("Nepal", comment:"quicksy country"), code: "+977", pattern: "^([0-9]{8,9})$") , + Quicksy_Country(name: NSLocalizedString("Netherlands", comment:"quicksy country"), code: "+31", pattern: "^([0-9]{9})$") , + Quicksy_Country(name: NSLocalizedString("New Caledonia", comment:"quicksy country"), code: "+687", pattern: "^([0-9]{6})$") , + Quicksy_Country(name: NSLocalizedString("New Zealand", comment:"quicksy country"), code: "+64", pattern: "^([0-9]{3,10})$") , + Quicksy_Country(name: NSLocalizedString("Nicaragua", comment:"quicksy country"), code: "+505", pattern: "^([0-9]{8})$") , + Quicksy_Country(name: NSLocalizedString("Niger", comment:"quicksy country"), code: "+227", pattern: "^([0-9]{8})$") , + Quicksy_Country(name: NSLocalizedString("Nigeria", comment:"quicksy country"), code: "+234", pattern: "^([0-9]{7,10})$") , + Quicksy_Country(name: NSLocalizedString("Niue", comment:"quicksy country"), code: "+683", pattern: "^([0-9]{4})$") , + Quicksy_Country(name: NSLocalizedString("Northern Marianas", comment:"quicksy country"), code: "+1", pattern: "^(670)([0-9]{7})$") , + Quicksy_Country(name: NSLocalizedString("Norway", comment:"quicksy country"), code: "+47", pattern: "^([0-9]{5}|[0-9]{8})$") , + Quicksy_Country(name: NSLocalizedString("Oman", comment:"quicksy country"), code: "+968", pattern: "^([0-9]{7,8})$") , + Quicksy_Country(name: NSLocalizedString("Pakistan", comment:"quicksy country"), code: "+92", pattern: "^([0-9]{8,11})$") , + Quicksy_Country(name: NSLocalizedString("Palau", comment:"quicksy country"), code: "+680", pattern: "^([0-9]{7})$") , + Quicksy_Country(name: NSLocalizedString("Panama", comment:"quicksy country"), code: "+507", pattern: "^([0-9]{7}|[0-9]{8})$") , + Quicksy_Country(name: NSLocalizedString("Papua New Guinea", comment:"quicksy country"), code: "+675", pattern: "^([0-9]{4,11})$") , + Quicksy_Country(name: NSLocalizedString("Paraguay", comment:"quicksy country"), code: "+595", pattern: "^([0-9]{5,9})$") , + Quicksy_Country(name: NSLocalizedString("Peru", comment:"quicksy country"), code: "+51", pattern: "^([0-9]{8,11})$") , + Quicksy_Country(name: NSLocalizedString("Philippines", comment:"quicksy country"), code: "+63", pattern: "^([0-9]{8,10})$") , + Quicksy_Country(name: NSLocalizedString("Poland", comment:"quicksy country"), code: "+48", pattern: "^([0-9]{6,9})$") , + Quicksy_Country(name: NSLocalizedString("Portugal", comment:"quicksy country"), code: "+351", pattern: "^([0-9]{9}|[0-9]{11})$") , + Quicksy_Country(name: NSLocalizedString("Puerto Rico", comment:"quicksy country"), code: "+1", pattern: "^(787|939)([0-9]{7})$") , + Quicksy_Country(name: NSLocalizedString("Qatar", comment:"quicksy country"), code: "+974", pattern: "^([0-9]{3,8})$") , + Quicksy_Country(name: NSLocalizedString("Romania", comment:"quicksy country"), code: "+40", pattern: "^([0-9]{9})$") , + Quicksy_Country(name: NSLocalizedString("Russian Federation", comment:"quicksy country"), code: "+7", pattern: "^([0-9]{10})$") , + Quicksy_Country(name: NSLocalizedString("Rwanda", comment:"quicksy country"), code: "+250", pattern: "^([0-9]{9})$") , + Quicksy_Country(name: NSLocalizedString("Saint Helena, Ascension and Tristan da Cunha", comment:"quicksy country"), code: "+247", pattern: "^([0-9]{4})$") , + Quicksy_Country(name: NSLocalizedString("Saint Helena, Ascension and Tristan da Cunha", comment:"quicksy country"), code: "+290", pattern: "^([0-9]{4})$") , + Quicksy_Country(name: NSLocalizedString("Saint Kitts and Nevis", comment:"quicksy country"), code: "+1", pattern: "^(869)([0-9]{7})$") , + Quicksy_Country(name: NSLocalizedString("Saint Lucia", comment:"quicksy country"), code: "+1", pattern: "^(758)([0-9]{7})$") , + Quicksy_Country(name: NSLocalizedString("Saint Pierre and Miquelon", comment:"quicksy country"), code: "+508", pattern: "^([0-9]{6})$") , + Quicksy_Country(name: NSLocalizedString("Saint Vincent and the Grenadines", comment:"quicksy country"), code: "+1", pattern: "^(784)([0-9]{7})$") , + Quicksy_Country(name: NSLocalizedString("Samoa", comment:"quicksy country"), code: "+685", pattern: "^([0-9]{3,7})$") , + Quicksy_Country(name: NSLocalizedString("San Marino", comment:"quicksy country"), code: "+378", pattern: "^([0-9]{6,10})$") , + Quicksy_Country(name: NSLocalizedString("Sao Tome and Principe", comment:"quicksy country"), code: "+239", pattern: "^([0-9]{7})$") , + Quicksy_Country(name: NSLocalizedString("Saudi Arabia", comment:"quicksy country"), code: "+966", pattern: "^([0-9]{8,9})$") , + Quicksy_Country(name: NSLocalizedString("Senegal", comment:"quicksy country"), code: "+221", pattern: "^([0-9]{9})$") , + Quicksy_Country(name: NSLocalizedString("Serbia", comment:"quicksy country"), code: "+381", pattern: "^([0-9]{4,12})$") , + Quicksy_Country(name: NSLocalizedString("Seychelles", comment:"quicksy country"), code: "+248", pattern: "^([0-9]{7})$") , + Quicksy_Country(name: NSLocalizedString("Sierra Leone", comment:"quicksy country"), code: "+232", pattern: "^([0-9]{8})$") , + Quicksy_Country(name: NSLocalizedString("Singapore", comment:"quicksy country"), code: "+65", pattern: "^([0-9]{8,12})$") , + Quicksy_Country(name: NSLocalizedString("Sint Maarten (Dutch part)", comment:"quicksy country"), code: "+1", pattern: "^(721)([0-9]{7})$") , + Quicksy_Country(name: NSLocalizedString("Slovakia", comment:"quicksy country"), code: "+421", pattern: "^([0-9]{4,9})$") , + Quicksy_Country(name: NSLocalizedString("Slovenia", comment:"quicksy country"), code: "+386", pattern: "^([0-9]{8})$") , + Quicksy_Country(name: NSLocalizedString("Solomon Islands", comment:"quicksy country"), code: "+677", pattern: "^([0-9]{5})$") , + Quicksy_Country(name: NSLocalizedString("Somalia", comment:"quicksy country"), code: "+252", pattern: "^([0-9]{5,8})$") , + Quicksy_Country(name: NSLocalizedString("South Africa", comment:"quicksy country"), code: "+27", pattern: "^([0-9]{9})$") , + Quicksy_Country(name: NSLocalizedString("Spain", comment:"quicksy country"), code: "+34", pattern: "^([0-9]{9})$") , + Quicksy_Country(name: NSLocalizedString("Sri Lanka", comment:"quicksy country"), code: "+94", pattern: "^([0-9]{9})$") , + Quicksy_Country(name: NSLocalizedString("Sudan", comment:"quicksy country"), code: "+249", pattern: "^([0-9]{9})$") , + Quicksy_Country(name: NSLocalizedString("Suriname", comment:"quicksy country"), code: "+597", pattern: "^([0-9]{6,7})$") , + Quicksy_Country(name: NSLocalizedString("Swaziland", comment:"quicksy country"), code: "+268", pattern: "^([0-9]{7,8})$") , + Quicksy_Country(name: NSLocalizedString("Sweden", comment:"quicksy country"), code: "+46", pattern: "^([0-9]{7,13})$") , + Quicksy_Country(name: NSLocalizedString("Switzerland", comment:"quicksy country"), code: "+41", pattern: "^([0-9]{4,12})$") , + Quicksy_Country(name: NSLocalizedString("Syrian Arab Republic", comment:"quicksy country"), code: "+963", pattern: "^([0-9]{8,10})$") , + Quicksy_Country(name: NSLocalizedString("Taiwan, China", comment:"quicksy country"), code: "+886", pattern: "^([0-9]{8,9})$") , + Quicksy_Country(name: NSLocalizedString("Tajikistan", comment:"quicksy country"), code: "+992", pattern: "^([0-9]{9})$") , + Quicksy_Country(name: NSLocalizedString("Tanzania", comment:"quicksy country"), code: "+255", pattern: "^([0-9]{9})$") , + Quicksy_Country(name: NSLocalizedString("Thailand", comment:"quicksy country"), code: "+66", pattern: "^([0-9]{8}|[0-9]{9})$") , + Quicksy_Country(name: NSLocalizedString("The Former Yugoslav Republic of Macedonia", comment:"quicksy country"), code: "+389", pattern: "^([0-9]{8})$") , + Quicksy_Country(name: NSLocalizedString("Timor-Leste", comment:"quicksy country"), code: "+670", pattern: "^([0-9]{7})$") , + Quicksy_Country(name: NSLocalizedString("Togo", comment:"quicksy country"), code: "+228", pattern: "^([0-9]{8})$") , + Quicksy_Country(name: NSLocalizedString("Tokelau", comment:"quicksy country"), code: "+690", pattern: "^([0-9]{4})$") , + Quicksy_Country(name: NSLocalizedString("Tonga", comment:"quicksy country"), code: "+676", pattern: "^([0-9]{5}|[0-9]{7})$") , + Quicksy_Country(name: NSLocalizedString("Trinidad and Tobago", comment:"quicksy country"), code: "+1", pattern: "^(868)([0-9]{7})$") , + Quicksy_Country(name: NSLocalizedString("Tunisia", comment:"quicksy country"), code: "+216", pattern: "^([0-9]{8})$") , + Quicksy_Country(name: NSLocalizedString("Turkey", comment:"quicksy country"), code: "+90", pattern: "^([0-9]{10})$") , + Quicksy_Country(name: NSLocalizedString("Turkmenistan", comment:"quicksy country"), code: "+993", pattern: "^([0-9]{8})$") , + Quicksy_Country(name: NSLocalizedString("Turks and Caicos Islands", comment:"quicksy country"), code: "+1", pattern: "^(649)([0-9]{7})$") , + Quicksy_Country(name: NSLocalizedString("Tuvalu", comment:"quicksy country"), code: "+688", pattern: "^([0-9]{5}|[0-9]{6})$") , + Quicksy_Country(name: NSLocalizedString("Uganda", comment:"quicksy country"), code: "+256", pattern: "^([0-9]{9})$") , + Quicksy_Country(name: NSLocalizedString("Ukraine", comment:"quicksy country"), code: "+380", pattern: "^([0-9]{9})$") , + Quicksy_Country(name: NSLocalizedString("United Arab Emirates", comment:"quicksy country"), code: "+971", pattern: "^([0-9]{8,9})$") , + Quicksy_Country(name: NSLocalizedString("United Kingdom", comment:"quicksy country"), code: "+44", pattern: "^([0-9]{7,10})$") , + Quicksy_Country(name: NSLocalizedString("United States", comment:"quicksy country"), code: "+1", pattern: "^([0-9]{10})$") , + Quicksy_Country(name: NSLocalizedString("United States Virgin Islands", comment:"quicksy country"), code: "+1", pattern: "^(340)([0-9]{7})$") , + Quicksy_Country(name: NSLocalizedString("Uruguay", comment:"quicksy country"), code: "+598", pattern: "^([0-9]{4,11})$") , + Quicksy_Country(name: NSLocalizedString("Uzbekistan", comment:"quicksy country"), code: "+998", pattern: "^([0-9]{9})$") , + Quicksy_Country(name: NSLocalizedString("Vanuatu", comment:"quicksy country"), code: "+678", pattern: "^([0-9]{5}|[0-9]{7})$") , + Quicksy_Country(name: NSLocalizedString("Vatican", comment:"quicksy country"), code: "+39", pattern: "^([0-9]{1,11})$") , + Quicksy_Country(name: NSLocalizedString("Venezuela (Bolivarian Republic of)", comment:"quicksy country"), code: "+58", pattern: "^([0-9]{10})$") , + Quicksy_Country(name: NSLocalizedString("Viet Nam", comment:"quicksy country"), code: "+84", pattern: "^([0-9]{7,10})$") , + Quicksy_Country(name: NSLocalizedString("Wallis and Futuna", comment:"quicksy country"), code: "+681", pattern: "^([0-9]{6})$") , + Quicksy_Country(name: NSLocalizedString("Yemen", comment:"quicksy country"), code: "+967", pattern: "^([0-9]{6,9})$") , + Quicksy_Country(name: NSLocalizedString("Zambia", comment:"quicksy country"), code: "+260", pattern: "^([0-9]{9})$") , + Quicksy_Country(name: NSLocalizedString("Zimbabwe", comment:"quicksy country"), code: "+263", pattern: "^([0-9]{5,10})$") , +] diff --git a/Monal/Classes/CreateGroupMenu.swift b/Monal/Classes/CreateGroupMenu.swift index 12e9938a72..f3692a7740 100644 --- a/Monal/Classes/CreateGroupMenu.swift +++ b/Monal/Classes/CreateGroupMenu.swift @@ -45,12 +45,14 @@ struct CreateGroupMenu: View { else { Section() { - Picker(selection: $selectedAccount, label: Text("Use account")) { - ForEach(Array(self.connectedAccounts.enumerated()), id: \.element) { idx, account in - Text(account.connectionProperties.identity.jid).tag(account as xmpp?) + if connectedAccounts.count > 1 { + Picker(selection: $selectedAccount, label: Text("Use account")) { + ForEach(Array(self.connectedAccounts.enumerated()), id: \.element) { idx, account in + Text(account.connectionProperties.identity.jid).tag(account as xmpp?) + } } + .pickerStyle(.menu) } - .pickerStyle(.menu) TextField(NSLocalizedString("Group Name (optional)", comment: "placeholder when creating new group"), text: $groupName, onEditingChanged: { isEditingGroupName = $0 }) .autocorrectionDisabled() @@ -77,6 +79,7 @@ struct CreateGroupMenu: View { self.selectedAccount!.mucProcessor.addUIHandler({_data in let data = _data as! NSDictionary let success : Bool = data["success"] as! Bool; if success { + DataLayer.sharedInstance().setFullName(self.groupName, forContact:roomJid, andAccount:self.selectedAccount!.accountNo) self.selectedAccount!.mucProcessor.changeName(ofMuc: roomJid, to: self.groupName) for user in self.selectedContacts { self.selectedAccount!.mucProcessor.setAffiliation("member", ofUser: user.contactJid, inMuc: roomJid) diff --git a/Monal/Classes/DataLayer.h b/Monal/Classes/DataLayer.h index 2fe0fbec21..ef9b77818c 100644 --- a/Monal/Classes/DataLayer.h +++ b/Monal/Classes/DataLayer.h @@ -43,6 +43,7 @@ extern NSString* const kMessageTypeFiletransfer; +(DataLayer*) sharedInstance; -(NSString* _Nullable) exportDB; -(void) createTransaction:(monal_void_block_t) block; +-(void) vacuum; //Roster -(NSString *) getRosterVersionForAccount:(NSNumber*) accountNo; @@ -109,12 +110,14 @@ extern NSString* const kMessageTypeFiletransfer; #pragma mark - MUC -(BOOL) initMuc:(NSString*) room forAccountId:(NSNumber*) accountNo andMucNick:(NSString* _Nullable) mucNick; --(void) cleanupMembersAndParticipantsListFor:(NSString*) room forAccountId:(NSNumber*) accountNo; +-(void) cleanupParticipantsListFor:(NSString*) room andType:(NSString*) type onAccountId:(NSNumber*) accountNo; +-(void) cleanupMembersListFor:(NSString*) room andType:(NSString*) type onAccountId:(NSNumber*) accountNo; -(void) addMember:(NSDictionary*) member toMuc:(NSString*) room forAccountId:(NSNumber*) accountNo; -(void) removeMember:(NSDictionary*) member fromMuc:(NSString*) room forAccountId:(NSNumber*) accountNo; -(void) addParticipant:(NSDictionary*) participant toMuc:(NSString*) room forAccountId:(NSNumber*) accountNo; -(void) removeParticipant:(NSDictionary*) participant fromMuc:(NSString*) room forAccountId:(NSNumber*) accountNo; -(NSDictionary* _Nullable) getParticipantForNick:(NSString*) nick inRoom:(NSString*) room forAccountId:(NSNumber*) accountNo; +-(NSDictionary* _Nullable) getParticipantForOccupant:(NSString*) occupant inRoom:(NSString*) room forAccountId:(NSNumber*) accountNo; -(NSArray*>*) getMembersAndParticipantsOfMuc:(NSString*) room forAccountId:(NSNumber*) accountNo; -(NSString* _Nullable) getOwnAffiliationInGroupOrChannel:(MLContact*) contact; -(NSString* _Nullable) getOwnRoleInGroupOrChannel:(MLContact*) contact; @@ -179,12 +182,12 @@ extern NSString* const kMessageTypeFiletransfer; -(NSNumber*) getSmallestHistoryId; -(NSNumber*) getBiggestHistoryId; --(NSNumber* _Nullable) hasMessageForStanzaId:(NSString*) stanzaId orMessageID:(NSString*) messageId withInboundDir:(BOOL) inbound andJid:(NSString*) jid onAccount:(NSNumber*) accountNo; +-(NSNumber* _Nullable) hasMessageForStanzaId:(NSString*) stanzaId orMessageID:(NSString*) messageId withInboundDir:(BOOL) inbound occupantId:(NSString* _Nullable) occupantId andJid:(NSString*) jid onAccount:(NSNumber*) accountNo; /* adds a specified message to the database */ --(NSNumber*) addMessageToChatBuddy:(NSString*) buddyName withInboundDir:(BOOL) inbound forAccount:(NSNumber*) accountNo withBody:(NSString*) message actuallyfrom:(NSString*) actualfrom participantJid:(NSString*_Nullable) participantJid sent:(BOOL) sent unread:(BOOL) unread messageId:(NSString*) messageid serverMessageId:(NSString*) stanzaid messageType:(NSString*) messageType andOverrideDate:(NSDate*) messageDate encrypted:(BOOL) encrypted displayMarkerWanted:(BOOL) displayMarkerWanted usingHistoryId:(NSNumber* _Nullable) historyId checkForDuplicates:(BOOL) checkForDuplicates; +-(NSNumber*) addMessageToChatBuddy:(NSString*) buddyName withInboundDir:(BOOL) inbound forAccount:(NSNumber*) accountNo withBody:(NSString*) message actuallyfrom:(NSString*) actualfrom occupantId:(NSString* _Nullable) occupantId participantJid:(NSString*_Nullable) participantJid sent:(BOOL) sent unread:(BOOL) unread messageId:(NSString*) messageid serverMessageId:(NSString*) stanzaid messageType:(NSString*) messageType andOverrideDate:(NSDate*) messageDate encrypted:(BOOL) encrypted displayMarkerWanted:(BOOL) displayMarkerWanted usingHistoryId:(NSNumber* _Nullable) historyId checkForDuplicates:(BOOL) checkForDuplicates; /* Marks a message as sent. When the server acked it @@ -212,12 +215,12 @@ extern NSString* const kMessageTypeFiletransfer; -(void) clearMessages:(NSNumber*) accountNo; -(void) clearMessagesWithBuddy:(NSString*) buddy onAccount:(NSNumber*) accountNo; --(void) autodeleteAllMessagesAfter3Days; --(void) deleteMessageHistory:(NSNumber *) messageNo; +-(NSNumber*) autoDeleteMessagesAfterInterval:(NSTimeInterval)interval; +-(void) retractMessageHistory:(NSNumber *) messageNo; -(void) deleteMessageHistoryLocally:(NSNumber*) messageNo; -(void) updateMessageHistory:(NSNumber*) messageNo withText:(NSString*) newText; --(NSNumber* _Nullable) getLMCHistoryIDForMessageId:(NSString*) messageid from:(NSString*) from actualFrom:(NSString* _Nullable) actualFrom participantJid:(NSString* _Nullable) participantJid andAccount:(NSNumber*) accountNo; --(NSNumber* _Nullable) getRetractionHistoryIDForMessageId:(NSString*) messageid from:(NSString*) from actualFrom:(NSString* _Nullable) actualFrom participantJid:(NSString* _Nullable) participantJid andAccount:(NSNumber*) accountNo; +-(NSNumber* _Nullable) getLMCHistoryIDForMessageId:(NSString*) messageid from:(NSString*) from occupantId:(NSString* _Nullable) occupantId participantJid:(NSString* _Nullable) participantJid andAccount:(NSNumber*) accountNo; +-(NSNumber* _Nullable) getRetractionHistoryIDForMessageId:(NSString*) messageid from:(NSString*) from participantJid:(NSString* _Nullable) participantJid occupantId:(NSString* _Nullable) occupantId andAccount:(NSNumber*) accountNo; -(NSNumber* _Nullable) getRetractionHistoryIDForModeratedStanzaId:(NSString*) stanzaId from:(NSString*) from andAccount:(NSNumber*) accountNo; -(NSDate* _Nullable) returnTimestampForQuote:(NSNumber*) historyID; diff --git a/Monal/Classes/DataLayer.m b/Monal/Classes/DataLayer.m index e22fb7deef..c8d8d80f4d 100644 --- a/Monal/Classes/DataLayer.m +++ b/Monal/Classes/DataLayer.m @@ -157,6 +157,11 @@ -(void) createTransaction:(monal_void_block_t) block [self.db voidWriteTransaction:block]; } +-(void) vacuum +{ + return [self.db vacuum]; +} + #pragma mark account commands -(NSArray*) accountList @@ -933,11 +938,16 @@ -(BOOL) initMuc:(NSString*) room forAccountId:(NSNumber*) accountNo andMucNick:( nick = [self ownNickNameforMuc:room forAccount:accountNo]; MLAssert(nick != nil, @"Could not determine muc nick when adding muc"); - [self cleanupMembersAndParticipantsListFor:room forAccountId:accountNo]; + for(NSString* type in @[@"member", @"admin", @"owner"]) + { + [self cleanupParticipantsListFor:room andType:type onAccountId:accountNo]; + [self cleanupMembersListFor:room andType:type onAccountId:accountNo]; + } BOOL encrypt = NO; #ifndef DISABLE_OMEMO // omemo for non group MUCs is disabled once the type of the muc is set + // (for channel type mucs this will be disabled while creating the muc shortly after this function is called) encrypt = [[HelperTools defaultsDB] boolForKey:@"OMEMODefaultOn"]; #endif// DISABLE_OMEMO @@ -945,11 +955,16 @@ -(BOOL) initMuc:(NSString*) room forAccountId:(NSNumber*) accountNo andMucNick:( }]; } --(void) cleanupMembersAndParticipantsListFor:(NSString*) room forAccountId:(NSNumber*) accountNo +-(void) cleanupParticipantsListFor:(NSString*) room andType:(NSString*) type onAccountId:(NSNumber*) accountNo +{ + //clean up old muc data (will be refilled by incoming presences and/or disco queries) + [self.db executeNonQuery:@"DELETE FROM muc_participants WHERE account_id=? AND room=? AND affiliation=?;" andArguments:@[accountNo, room, type]]; +} + +-(void) cleanupMembersListFor:(NSString*) room andType:(NSString*) type onAccountId:(NSNumber*) accountNo { //clean up old muc data (will be refilled by incoming presences and/or disco queries) - [self.db executeNonQuery:@"DELETE FROM muc_participants WHERE account_id=? AND room=?;" andArguments:@[accountNo, room]]; - [self.db executeNonQuery:@"DELETE FROM muc_members WHERE account_id=? AND room=?;" andArguments:@[accountNo, room]]; + [self.db executeNonQuery:@"DELETE FROM muc_members WHERE account_id=? AND room=? AND affiliation=?;" andArguments:@[accountNo, room, type]]; } -(void) addParticipant:(NSDictionary*) participant toMuc:(NSString*) room forAccountId:(NSNumber*) accountNo @@ -959,7 +974,7 @@ -(void) addParticipant:(NSDictionary*) participant toMuc:(NSString*) room forAcc [self.db voidWriteTransaction:^{ //create entry if not already existing - [self.db executeNonQuery:@"INSERT OR IGNORE INTO muc_participants ('account_id', 'room', 'room_nick') VALUES(?, ?, ?);" andArguments:@[accountNo, room, participant[@"nick"]]]; + [self.db executeNonQuery:@"INSERT OR IGNORE INTO muc_participants ('account_id', 'room', 'room_nick', 'occupant_id') VALUES(?, ?, ?, ?);" andArguments:@[accountNo, room, participant[@"nick"], nilWrapper(participant[@"occupant_id"])]]; //update entry with optional fields (the first two fields are for members that are not just participants) if(participant[@"jid"]) @@ -1007,7 +1022,7 @@ -(void) removeMember:(NSDictionary*) member fromMuc:(NSString*) room forAccountI }]; } --(NSDictionary* _Nullable) getParticipantForNick:(NSString*) nick inRoom:(NSString*) room forAccountId:(NSString*) accountNo +-(NSDictionary* _Nullable) getParticipantForNick:(NSString*) nick inRoom:(NSString*) room forAccountId:(NSNumber*) accountNo { if(!nick || !room || accountNo == nil) return nil; @@ -1017,6 +1032,16 @@ -(NSDictionary* _Nullable) getParticipantForNick:(NSString*) nick inRoom:(NSStri }]; } +-(NSDictionary* _Nullable) getParticipantForOccupant:(NSString*) occupant inRoom:(NSString*) room forAccountId:(NSNumber*) accountNo +{ + if(!occupant || !occupant || accountNo == nil) + return nil; + return [self.db idReadTransaction:^{ + NSArray* result = [self.db executeReader:@"SELECT * FROM muc_participants WHERE account_id=? AND room=? AND occupant_id=?;" andArguments:@[accountNo, room, occupant]]; + return result.count > 0 ? result[0] : nil; + }]; +} + -(NSArray*>*) getMembersAndParticipantsOfMuc:(NSString*) room forAccountId:(NSNumber*) accountNo { if(!room || accountNo == nil) @@ -1230,13 +1255,13 @@ -(NSNumber*) getBiggestHistoryId }]; } --(NSNumber*) addMessageToChatBuddy:(NSString*) buddyName withInboundDir:(BOOL) inbound forAccount:(NSNumber*) accountNo withBody:(NSString*) message actuallyfrom:(NSString*) actualfrom participantJid:(NSString*) participantJid sent:(BOOL) sent unread:(BOOL) unread messageId:(NSString*) messageid serverMessageId:(NSString*) stanzaid messageType:(NSString*) messageType andOverrideDate:(NSDate*) messageDate encrypted:(BOOL) encrypted displayMarkerWanted:(BOOL) displayMarkerWanted usingHistoryId:(NSNumber* _Nullable) historyId checkForDuplicates:(BOOL) checkForDuplicates +-(NSNumber*) addMessageToChatBuddy:(NSString*) buddyName withInboundDir:(BOOL) inbound forAccount:(NSNumber*) accountNo withBody:(NSString*) message actuallyfrom:(NSString*) actualfrom occupantId:(NSString* _Nullable) occupantId participantJid:(NSString*_Nullable) participantJid sent:(BOOL) sent unread:(BOOL) unread messageId:(NSString*) messageid serverMessageId:(NSString*) stanzaid messageType:(NSString*) messageType andOverrideDate:(NSDate*) messageDate encrypted:(BOOL) encrypted displayMarkerWanted:(BOOL) displayMarkerWanted usingHistoryId:(NSNumber* _Nullable) historyId checkForDuplicates:(BOOL) checkForDuplicates; { if(!buddyName || !message) return nil; return [self.db idWriteTransaction:^{ - if(!checkForDuplicates || [self hasMessageForStanzaId:stanzaid orMessageID:messageid withInboundDir:inbound andJid:buddyName onAccount:accountNo] == nil) + if(!checkForDuplicates || [self hasMessageForStanzaId:stanzaid orMessageID:messageid withInboundDir:inbound occupantId:occupantId andJid:buddyName onAccount:accountNo] == nil) { //this is always from a contact NSDateFormatter* formatter = [NSDateFormatter new]; @@ -1268,14 +1293,14 @@ -(NSNumber*) addMessageToChatBuddy:(NSString*) buddyName withInboundDir:(BOOL) i if(historyId != nil) { DDLogVerbose(@"Inserting backwards with history id %@", historyId); - query = @"insert into message_history (message_history_id, account_id, buddy_name, inbound, timestamp, message, actual_from, unread, sent, displayMarkerWanted, messageid, messageType, encrypted, stanzaid, participant_jid) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);"; - params = @[historyId, accountNo, buddyName, [NSNumber numberWithBool:inbound], dateString, message, actualfrom, [NSNumber numberWithBool:unread], [NSNumber numberWithBool:sent], [NSNumber numberWithBool:displayMarkerWanted], messageid?messageid:@"", messageType, [NSNumber numberWithBool:encrypted], stanzaid?stanzaid:@"", participantJid != nil ? participantJid : [NSNull null]]; + query = @"insert into message_history (message_history_id, account_id, buddy_name, inbound, timestamp, message, actual_from, unread, sent, displayMarkerWanted, messageid, messageType, encrypted, stanzaid, participant_jid, occupant_id) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);"; + params = @[historyId, accountNo, buddyName, [NSNumber numberWithBool:inbound], dateString, message, actualfrom, [NSNumber numberWithBool:unread], [NSNumber numberWithBool:sent], [NSNumber numberWithBool:displayMarkerWanted], messageid?messageid:@"", messageType, [NSNumber numberWithBool:encrypted], stanzaid?stanzaid:@"", nilWrapper(participantJid), nilWrapper(occupantId)]; } else { //we use autoincrement here instead of MAX(message_history_id) + 1 to be a little bit faster (but at the cost of "duplicated code") - query = @"insert into message_history (account_id, buddy_name, inbound, timestamp, message, actual_from, unread, sent, displayMarkerWanted, messageid, messageType, encrypted, stanzaid, participant_jid) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);"; - params = @[accountNo, buddyName, [NSNumber numberWithBool:inbound], dateString, message, actualfrom, [NSNumber numberWithBool:unread], [NSNumber numberWithBool:sent], [NSNumber numberWithBool:displayMarkerWanted], messageid?messageid:@"", messageType, [NSNumber numberWithBool:encrypted], stanzaid?stanzaid:@"", participantJid != nil ? participantJid : [NSNull null]]; + query = @"insert into message_history (account_id, buddy_name, inbound, timestamp, message, actual_from, unread, sent, displayMarkerWanted, messageid, messageType, encrypted, stanzaid, participant_jid, occupant_id) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);"; + params = @[accountNo, buddyName, [NSNumber numberWithBool:inbound], dateString, message, actualfrom, [NSNumber numberWithBool:unread], [NSNumber numberWithBool:sent], [NSNumber numberWithBool:displayMarkerWanted], messageid?messageid:@"", messageType, [NSNumber numberWithBool:encrypted], stanzaid?stanzaid:@"", nilWrapper(participantJid), nilWrapper(occupantId)]; } DDLogVerbose(@"%@ params:%@", query, params); BOOL success = [self.db executeNonQuery:query andArguments:params]; @@ -1293,7 +1318,7 @@ -(NSNumber*) addMessageToChatBuddy:(NSString*) buddyName withInboundDir:(BOOL) i }]; } --(NSNumber* _Nullable) hasMessageForStanzaId:(NSString*) stanzaId orMessageID:(NSString*) messageId withInboundDir:(BOOL) inbound andJid:(NSString*) jid onAccount:(NSNumber*) accountNo +-(NSNumber* _Nullable) hasMessageForStanzaId:(NSString*) stanzaId orMessageID:(NSString*) messageId withInboundDir:(BOOL) inbound occupantId:(NSString* _Nullable) occupantId andJid:(NSString*) jid onAccount:(NSNumber*) accountNo { if(accountNo == nil) return (NSNumber*)nil; @@ -1322,12 +1347,18 @@ -(NSNumber* _Nullable) hasMessageForStanzaId:(NSString*) stanzaId orMessageID:(N if(historyId != nil) { DDLogVerbose(@"found by origin-id or messageid"); - if(stanzaId) + if(stanzaId!=nil) { DDLogDebug(@"Updating stanzaid of message_history_id %@ to %@ for (account=%@, messageid=%@, inbound=%d)...", historyId, stanzaId, accountNo, messageId, inbound); //this entry needs an update of its stanzaid [self.db executeNonQuery:@"UPDATE message_history SET stanzaid=? WHERE message_history_id=?" andArguments:@[stanzaId, historyId]]; } + if(occupantId!=nil) + { + DDLogDebug(@"Updating occupant_id of message_history_id %@ to %@ for (account=%@, messageid=%@, inbound=%d)...", historyId, occupantId, accountNo, messageId, inbound); + //only update occupant id if not set yet + [self.db executeNonQuery:@"UPDATE message_history SET occupant_id=? WHERE occupant_id IS NULL AND message_history_id=?" andArguments:@[nilWrapper(occupantId), historyId]]; + } return historyId; } } @@ -1455,28 +1486,34 @@ -(void) clearMessagesWithBuddy:(NSString*) buddy onAccount:(NSNumber*) accountNo }]; } - --(void) autodeleteAllMessagesAfter3Days +-(NSNumber*) autoDeleteMessagesAfterInterval:(NSTimeInterval) interval { - [self.db voidWriteTransaction:^{ + return [self.db idWriteTransaction:^{ [self.db executeNonQuery:@"PRAGMA secure_delete=on;"]; - //3 days before now - NSString* pastDate = [dbFormatter stringFromDate:[[NSCalendar currentCalendar] dateByAddingUnit:NSCalendarUnitDay value:-3 toDate:[NSDate date] options:0]]; - //delete all transferred files old enough - NSArray* messageHistoryIDs = [self.db executeScalarReader:@"SELECT message_history_id FROM message_history WHERE messageType=? AND timestamp= 15 + autodeleteOptions[-1] = NSLocalizedString("Custom", comment:"Message autdelete time") + //check if we have a custom value and change picker value accordingly + if autodeleteOptions[autodeleteInterval] == nil { + _autodeleteIntervalSelection = State(wrappedValue:-1) + } + } else { + //check if we have a custom value, this should never happen because custom values should only be settable in ios >= 15 + if autodeleteOptions[autodeleteInterval] == nil { + //turn autodelete off int his case (sane value) + _autodeleteIntervalSelection = State(wrappedValue:0) + _autodeleteInterval = State(wrappedValue:0) + } + } + } var body: some View { Form { @@ -273,12 +305,41 @@ like hotel wifi, ugly mobile carriers etc. } Section(header: Text("On this device")) { - SettingsToggle(isOn: $generalSettingsDefaultsDB.autodeleteAllMessagesAfter3Days) { - Text("Autodelete all messages after 3 days") + VStack(alignment: .leading, spacing: 0) { + Picker("Autodelete all messages older than", selection: $autodeleteIntervalSelection) { + ForEach(autodeleteOptions.keys.sorted(), id: \.self) { key in + Text(autodeleteOptions[key]!).tag(key) + } + } + if #available(iOS 15, *) { + //custom interval requested explicitly + if autodeleteIntervalSelection == -1 { + HStack { + Text("Custom Time: ") + Stepper(String(format:NSLocalizedString("%@ hours", comment:""), String(describing:(max(1, autodeleteInterval / 3600)).formatted())), value: Binding( + get: { max(1, autodeleteInterval / 3600) /*clamp to 1 ... .max*/ }, + set: { autodeleteInterval = $0 * 3600 } + ), in: 1 ... .max) + } + } + } + Text("Be warned: Message will only be deleted on incoming pushes or if you open the app! This is especially true for shorter time intervals!").foregroundColor(Color(UIColor.secondaryLabel)).font(.footnote) + Text("Also beware: You won't be able to load older history from your server, Monal will immediately delete it after fetching it!").foregroundColor(Color(UIColor.secondaryLabel)).font(.footnote) } } } .navigationBarTitle(Text("Security"), displayMode: .inline) + //save only when closing view to not delete messages while the user is selecting a (custom) value + .onDisappear { + if autodeleteIntervalSelection == -1 { + //make sure our custom value is stored clamped, too + autodeleteInterval = max(1, autodeleteInterval / 3600) + } else { + //copy over picker value if not set to custom + autodeleteInterval = autodeleteIntervalSelection + } + generalSettingsDefaultsDB.AutodeleteInterval = autodeleteInterval + } } } @@ -287,13 +348,24 @@ struct PrivacySettings: View { var body: some View { Form { + PrivacySettingsSubview(onboardingPart:-1) + } + .navigationBarTitle(Text("Privacy"), displayMode: .inline) + } +} +struct PrivacySettingsSubview: View { + @ObservedObject var generalSettingsDefaultsDB = GeneralSettingsDefaultsDB() + var onboardingPart: Int + + var body: some 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) { @@ -305,18 +377,23 @@ struct PrivacySettings: View { 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: $generalSettingsDefaultsDB.allowCallsFromNonRosterContacts) { + 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") @@ -328,7 +405,6 @@ struct PrivacySettings: View { } } } - .navigationBarTitle(Text("Privacy"), displayMode: .inline) } } @@ -396,7 +472,7 @@ struct AttachmentSettings: View { Text("Load over wifi") } ) - Text("Load over WiFi up to: \(UInt(generalSettingsDefaultsDB.autodownloadFiletransfersWifiMaxSize/(1024*1024))) MiB") + Text("Load over WiFi up to: \(String(describing:UInt(generalSettingsDefaultsDB.autodownloadFiletransfersWifiMaxSize/(1024*1024)))) MiB") } Section { @@ -413,7 +489,7 @@ struct AttachmentSettings: View { Text("Load over Cellular") } ) - Text("Load over cellular up to: \(Int(generalSettingsDefaultsDB.autodownloadFiletransfersMobileMaxSize/(1024*1024))) MiB") + Text("Load over cellular up to: \(String(describing:UInt(generalSettingsDefaultsDB.autodownloadFiletransfersMobileMaxSize/(1024*1024)))) MiB") } Section(header: Text("Upload Settings")) { @@ -439,6 +515,6 @@ struct AttachmentSettings: View { struct ContentView_Previews: PreviewProvider { static var previews: some View { - PrivacySettings() + GeneralSettings() } } diff --git a/Monal/Classes/HelperTools.h b/Monal/Classes/HelperTools.h index fa953819fa..c927a03f1a 100644 --- a/Monal/Classes/HelperTools.h +++ b/Monal/Classes/HelperTools.h @@ -8,12 +8,17 @@ #import #import "MLConstants.h" +#import "MLDelayableTimer.h" #include "metamacros.h" -#define createTimer(timeout, handler, ...) createQueuedTimer(timeout, nil, handler, __VA_ARGS__) -#define createQueuedTimer(timeout, queue, handler, ...) metamacro_if_eq(0, metamacro_argcount(__VA_ARGS__))([HelperTools startQueuedTimer:timeout withHandler:handler andCancelHandler:nil andFile:(char*)__FILE__ andLine:__LINE__ andFunc:(char*)__func__ onQueue:queue])(_createQueuedTimer(timeout, queue, handler, __VA_ARGS__)) -#define _createQueuedTimer(timeout, queue, handler, cancelHandler, ...) [HelperTools startQueuedTimer:timeout withHandler:handler andCancelHandler:cancelHandler andFile:(char*)__FILE__ andLine:__LINE__ andFunc:(char*)__func__ onQueue:queue] +#define createDelayableTimer(timeout, handler, ...) createDelayableQueuedTimer(timeout, nil, handler, __VA_ARGS__) +#define createDelayableQueuedTimer(timeout, queue, handler, ...) metamacro_if_eq(0, metamacro_argcount(__VA_ARGS__))([HelperTools startDelayableQueuedTimer:timeout withHandler:handler andCancelHandler:nil andFile:(char*)__FILE__ andLine:__LINE__ andFunc:(char*)__func__ onQueue:queue])(_createDelayableQueuedTimer(timeout, queue, handler, __VA_ARGS__)) +#define _createDelayableQueuedTimer(timeout, queue, handler, cancelHandler, ...) [HelperTools startDelayableQueuedTimer:timeout withHandler:handler andCancelHandler:cancelHandler andFile:(char*)__FILE__ andLine:__LINE__ andFunc:(char*)__func__ onQueue:queue] + +#define createTimer(timeout, handler, ...) createQueuedTimer(timeout, nil, handler, __VA_ARGS__) +#define createQueuedTimer(timeout, queue, handler, ...) metamacro_if_eq(0, metamacro_argcount(__VA_ARGS__))([HelperTools startQueuedTimer:timeout withHandler:handler andCancelHandler:nil andFile:(char*)__FILE__ andLine:__LINE__ andFunc:(char*)__func__ onQueue:queue])(_createQueuedTimer(timeout, queue, handler, __VA_ARGS__)) +#define _createQueuedTimer(timeout, queue, handler, cancelHandler, ...) [HelperTools startQueuedTimer:timeout withHandler:handler andCancelHandler:cancelHandler andFile:(char*)__FILE__ andLine:__LINE__ andFunc:(char*)__func__ onQueue:queue] #define MLAssert(check, text, ...) do { if(!(check)) { metamacro_if_eq(0, metamacro_argcount(__VA_ARGS__))([HelperTools MLAssertWithText:text andUserData:nil andFile:(char*)__FILE__ andLine:__LINE__ andFunc:(char*)__func__];)([HelperTools MLAssertWithText:text andUserData:metamacro_head(__VA_ARGS__) andFile:(char*)__FILE__ andLine:__LINE__ andFunc:(char*)__func__];) while(YES); } } while(0) #define unreachable(...) do { metamacro_if_eq(0, metamacro_argcount(__VA_ARGS__))(MLAssert(NO, @"unreachable", __VA_ARGS__);)(MLAssert(NO, __VA_ARGS__);); } while(0) @@ -23,6 +28,7 @@ NS_ASSUME_NONNULL_BEGIN +@class AnyPromise; @class MLXMLNode; @class xmpp; @class XMPPStanza; @@ -51,6 +57,7 @@ typedef NS_ENUM(NSUInteger, MLDefinedIdentifier) { typedef NS_ENUM(NSUInteger, MLRunLoopIdentifier) { MLRunLoopIdentifierNetwork, + MLRunLoopIdentifierTimer, }; void logException(NSException* exception); @@ -58,7 +65,7 @@ void swizzle(Class c, SEL orig, SEL new); //weak container holding an object as weak pointer (needed to not create retain circles in NSCache @interface WeakContainer : NSObject -@property (nonatomic, weak) id obj; +@property (atomic, weak) id obj; -(id) initWithObj:(id) obj; @end @@ -140,8 +147,10 @@ void swizzle(Class c, SEL orig, SEL new); +(NSSet*) getOwnFeatureSet; +(NSString*) getEntityCapsHashForIdentities:(NSArray*) identities andFeatures:(NSSet*) features andForms:(NSArray*) forms; +(NSString* _Nullable) formatLastInteraction:(NSDate*) lastInteraction; ++(NSString*) stringFromTimeInterval:(NSUInteger) interval; +(NSDate*) parseDateTimeString:(NSString*) datetime; +(NSString*) generateDateTimeString:(NSDate*) datetime; ++(NSString*) generateRandomPassword; +(NSString*) encodeRandomResource; +(NSData* _Nullable) sha1:(NSData* _Nullable) data; @@ -174,7 +183,10 @@ 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; +(NSString*) appBuildVersionInfoFor:(MLVersionType) type; diff --git a/Monal/Classes/HelperTools.m b/Monal/Classes/HelperTools.m index d271e2ac8a..5d1b97b80d 100644 --- a/Monal/Classes/HelperTools.m +++ b/Monal/Classes/HelperTools.m @@ -57,6 +57,7 @@ #import "commithash.h" #import "MLContactSoftwareVersionInfo.h" #import "IPC.h" +#import "MLDelayableTimer.h" @import UserNotifications; @import CoreImage; @@ -70,6 +71,9 @@ @interface KSCrash() @property(nonatomic,readwrite,retain) NSString* basePath; @end +@interface MLDelayableTimer() +-(void) invalidate; +@end static char* _crashBundleName = "UnifiedReport"; static NSString* _processID; @@ -590,6 +594,7 @@ +(NSRunLoop*) getExtraRunloopWithIdentifier:(MLRunLoopIdentifier) identifier switch(identifier) { case MLRunLoopIdentifierNetwork: priority = DISPATCH_QUEUE_PRIORITY_BACKGROUND; name = "im.monal.runloop.networking"; break; + case MLRunLoopIdentifierTimer: priority = DISPATCH_QUEUE_PRIORITY_BACKGROUND; name = "im.monal.runloop.timer"; break; default: unreachable(@"unknown runloop identifier!"); } @@ -1712,8 +1717,9 @@ +(BOOL) isNotInFocus +(void) dispatchAsync:(BOOL) async reentrantOnQueue:(dispatch_queue_t _Nullable) queue withBlock:(monal_void_block_t) block { + dispatch_queue_t main_queue = dispatch_get_main_queue(); if(!queue) - queue = dispatch_get_main_queue(); + queue = main_queue; //apple docs say that enqueueing blocks for synchronous execution will execute this blocks in the thread the enqueueing came from //(e.g. the tread we are already in). @@ -1728,7 +1734,9 @@ +(void) dispatchAsync:(BOOL) async reentrantOnQueue:(dispatch_queue_t _Nullable) #pragma clang diagnostic ignored "-Wdeprecated-declarations" dispatch_queue_t current_queue = dispatch_get_current_queue(); #pragma clang diagnostic pop - if(current_queue == queue || (queue == dispatch_get_main_queue() && [NSThread isMainThread])) + if(queue == main_queue && [NSThread isMainThread]) + block(); + else if(current_queue == queue) block(); else { @@ -2211,6 +2219,15 @@ +(NSString* _Nullable) formatLastInteraction:(NSDate*) lastInteraction } } ++(NSString*) stringFromTimeInterval:(NSUInteger) interval +{ + NSUInteger hours = interval / 3600; + NSUInteger minutes = (interval % 3600) / 60; + NSUInteger seconds = interval % 60; + + return [NSString stringWithFormat:@"%luh %lumin and %lusec", hours, minutes, seconds]; +} + +(NSDate*) parseDateTimeString:(NSString*) datetime { static NSDateFormatter* rfc3339DateFormatter; @@ -2252,8 +2269,8 @@ +(NSString*) generateDateTimeString:(NSDate*) datetime return [rfc3339DateFormatter stringFromDate:datetime]; } -//don't use this directly, but via createTimer() makro -+(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 +//don't use this directly, but via createDelayableTimer() makros ++(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 { if(queue == nil) queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); @@ -2263,50 +2280,57 @@ +(monal_void_block_t) startQueuedTimer:(double) timeout withHandler:(monal_void_ if([filePathComponents count]>1) fileStr = [NSString stringWithFormat:@"%@/%@", filePathComponents[[filePathComponents count]-2], filePathComponents[[filePathComponents count]-1]]; - if(timeout<=0.001) - { - //DDLogVerbose(@"Timer timeout is smaller than 0.001, dispatching handler directly."); + MLDelayableTimer* timer = [[MLDelayableTimer alloc] initWithHandler:^(MLDelayableTimer* timer){ if(handler) dispatch_async(queue, ^{ + DDLogDebug(@"calling handler for timer: %@", timer); handler(); }); - return ^{ }; //empty cancel block because this "timer" already triggered - } - - NSString* uuid = [[NSUUID UUID] UUIDString]; - - //DDLogDebug(@"setting up timer %@(%G)", uuid, timeout); - dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue); - dispatch_source_set_timer(timer, - dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeout*NSEC_PER_SEC)), - DISPATCH_TIME_FOREVER, - (uint64_t) (0.1 * NSEC_PER_SEC)); //leeway of 100ms - - dispatch_source_set_event_handler(timer, ^{ - DDLogDebug(@"timer %@ %@(%G) triggered (created at %@:%d in %s)", timer, uuid, timeout, fileStr, line, func); - dispatch_source_cancel(timer); - if(handler) - handler(); - }); - - dispatch_source_set_cancel_handler(timer, ^{ - //DDLogDebug(@"timer %@ %@(%G) cancelled (created at %@:%d)", timer, uuid, timeout, fileName, line); + } andCancelHandler:^(MLDelayableTimer* timer){ if(cancelHandler) - cancelHandler(); - }); + dispatch_async(queue, ^{ + DDLogDebug(@"calling cancel block for timer: %@", timer); + cancelHandler(); + }); + } timeout:timeout tolerance:0.1 andDescription:[NSString stringWithFormat:@"created at %@:%d in %s", fileStr, line, func]]; - //start timer - DDLogDebug(@"starting timer %@ %@(%G) (created at %@:%d in %s)", timer, uuid, timeout, fileStr, line, func); - dispatch_resume(timer); + if(timeout < 0.001) + { + //DDLogVerbose(@"Timer timeout is smaller than 0.001, dispatching handler directly: %@", timer); + [timer invalidate]; + if(handler) + dispatch_async(queue, ^{ + handler(); + }); + return timer; //this timer is not added to a runloop and invalid because the handler already got called + } - //return block that can be used to cancel the timer + [timer start]; + return timer; +} + +//don't use this directly, but via createTimer() makros ++(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 +{ + MLDelayableTimer* timer = [self startDelayableQueuedTimer:timeout withHandler:handler andCancelHandler:cancelHandler andFile:file andLine:line andFunc:func onQueue:queue]; return ^{ - DDLogDebug(@"cancel block for timer %@ %@(%G) called (created at %@:%d in %s)", timer, uuid, timeout, fileStr, line, func); - if(!dispatch_source_testcancel(timer)) - dispatch_source_cancel(timer); + [timer cancel]; }; } ++(AnyPromise*) waitAtLeastSeconds:(NSTimeInterval) seconds forPromise:(AnyPromise*) promise +{ + return PMKWhen(@[promise, PMKAfter(seconds)]).then(^{ + return promise; + }); +} + ++(NSString*) generateRandomPassword +{ + u_int32_t i=arc4random(); + return [self hexadecimalString:[NSData dataWithBytes: &i length: sizeof(i)]]; +} + +(NSString*) encodeRandomResource { u_int32_t i=arc4random(); 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/MLConstants.h b/Monal/Classes/MLConstants.h index b6c26bd941..f512d23fb7 100644 --- a/Monal/Classes/MLConstants.h +++ b/Monal/Classes/MLConstants.h @@ -21,6 +21,11 @@ static const DDLogLevel ddLogLevel = LOG_LEVEL_STDOUT; #import "MLLogFileManager.h" +@import PromiseKit; +#define PMKHangEnum(promise) (((NSNumber*)PMKHang(promise)).integerValue) +#define PMKHangBool(promise) (((NSNumber*)PMKHang(promise)).boolValue) +#define PMKHangInt(promise) (((NSNumber*)PMKHang(promise)).intValue) +#define PMKHangDouble(promise) (((NSNumber*)PMKHang(promise)).doubleValue) //configure app group constants #ifdef IS_ALPHA @@ -64,6 +69,7 @@ static const DDLogLevel ddLogLevel = LOG_LEVEL_STDOUT; // #endif @class MLContact; +@class MLDelayableTimer; //some typedefs used throughout the project typedef void (^contactCompletion)(MLContact* _Nonnull selectedContact) NS_SWIFT_UNAVAILABLE("To be redefined in swift."); @@ -158,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" @@ -211,4 +218,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})?)(;.*)?([?].*)?$" diff --git a/Monal/Classes/MLContact.h b/Monal/Classes/MLContact.h index 80a2b90b11..767f310ca2 100644 --- a/Monal/Classes/MLContact.h +++ b/Monal/Classes/MLContact.h @@ -95,6 +95,7 @@ FOUNDATION_EXPORT NSString* const kAskSubscribe; @property (nonatomic, readonly) NSString* contactDisplayNameWithoutSelfnotesPrefix; -(NSString*) contactDisplayNameWithFallback:(NSString* _Nullable) fallbackName; +-(NSString*) contactDisplayNameWithFallback:(NSString* _Nullable) fallbackName andSelfnotesPrefix:(BOOL) hasSelfnotesPrefix; -(void) updateWithContact:(MLContact*) contact; -(void) refresh; -(void) updateUnreadCount; diff --git a/Monal/Classes/MLContact.m b/Monal/Classes/MLContact.m index c9be560dd8..5532729a75 100644 --- a/Monal/Classes/MLContact.m +++ b/Monal/Classes/MLContact.m @@ -237,8 +237,9 @@ +(MLContact*) createContactFromJid:(NSString*) jid andAccountNo:(NSNumber*) acco @synchronized(_singletonCache) { if(_singletonCache[cacheKey] != nil) { - if(((WeakContainer*)_singletonCache[cacheKey]).obj != nil) - return ((WeakContainer*)_singletonCache[cacheKey]).obj; + MLContact* obj = ((WeakContainer*)_singletonCache[cacheKey]).obj; + if(obj != nil) + return obj; else [_singletonCache removeObjectForKey:cacheKey]; } @@ -344,7 +345,7 @@ -(NSString*) contactDisplayNameWithFallback:(NSString* _Nullable) fallbackName; -(NSString*) contactDisplayNameWithFallback:(NSString* _Nullable) fallbackName andSelfnotesPrefix:(BOOL) hasSelfnotesPrefix { - DDLogVerbose(@"Calculating contact display name..."); + //DDLogVerbose(@"Calculating contact display name..."); NSString* displayName; if(!self.isSelfChat) { @@ -359,17 +360,17 @@ -(NSString*) contactDisplayNameWithFallback:(NSString* _Nullable) fallbackName a if(self.nickName && self.nickName.length > 0) { - DDLogVerbose(@"Using nickName: %@", self.nickName); + //DDLogVerbose(@"Using nickName: %@", self.nickName); displayName = self.nickName; } else if(self.fullName && self.fullName.length > 0) { - DDLogVerbose(@"Using fullName: %@", self.fullName); + //DDLogVerbose(@"Using fullName: %@", self.fullName); displayName = self.fullName; } else { - DDLogVerbose(@"Using fallback: %@", fallbackName); + //DDLogVerbose(@"Using fallback: %@", fallbackName); displayName = fallbackName; } } @@ -507,10 +508,10 @@ +(NSSet*) keyPathsForValuesAffectingIsSelfChat -(BOOL) isInRoster { - // mucs have a subscription of both (ensured by the datalayer) - return [self.subscription isEqualToString:kSubBoth] - || [self.subscription isEqualToString:kSubTo] - || [self.ask isEqualToString:kAskSubscribe]; + //either we already allowed each other or we allow this contact and asked them to allow us + //--> if isInRoster is true this is displayed as "remove contact" in contact details, otherwise it will be displayed as "add contact" + //(mucs have a subscription of 'both', ensured by the datalayer) + return [self.subscription isEqualToString:kSubBoth] || ([self.subscription isEqualToString:kSubFrom] && [self.ask isEqualToString:kAskSubscribe]); } +(NSSet*) keyPathsForValuesAffectingIsInRoster diff --git a/Monal/Classes/MLDelayableTimer.h b/Monal/Classes/MLDelayableTimer.h new file mode 100644 index 0000000000..2df5949eb2 --- /dev/null +++ b/Monal/Classes/MLDelayableTimer.h @@ -0,0 +1,33 @@ +// +// MLDelayableTimer.h +// monalxmpp +// +// Created by Thilo Molitor on 24.06.24. +// Copyright © 2024 monal-im.org. All rights reserved. +// + +#import + +#ifndef MLDelayableTimer_h +#define MLDelayableTimer_h + +NS_ASSUME_NONNULL_BEGIN + +@class MLDelayableTimer; +typedef void (^monal_timer_block_t)(MLDelayableTimer* _Nonnull) NS_SWIFT_UNAVAILABLE("To be redefined in swift."); + +@interface MLDelayableTimer : NSObject + +-(instancetype) initWithHandler:(monal_timer_block_t) handler andCancelHandler:(monal_timer_block_t _Nullable) cancelHandler timeout:(NSTimeInterval) timeout tolerance:(NSTimeInterval) tolerance andDescription:(NSString* _Nullable) description; + +-(void) start; +-(void) trigger; +-(void) pause; +-(void) resume; +-(void) cancel; + +@end + +NS_ASSUME_NONNULL_END + +#endif /* MLDelayableTimer_h */ diff --git a/Monal/Classes/MLDelayableTimer.m b/Monal/Classes/MLDelayableTimer.m new file mode 100644 index 0000000000..cb627dbea0 --- /dev/null +++ b/Monal/Classes/MLDelayableTimer.m @@ -0,0 +1,127 @@ +// +// MLDelayableTimer.m +// monalxmpp +// +// Created by Thilo Molitor on 24.06.24. +// Copyright © 2024 monal-im.org. All rights reserved. +// + +#import "MLConstants.h" +#import "HelperTools.h" +#import "MLDelayableTimer.h" + +@interface MLDelayableTimer() +{ + NSTimer* _wrappedTimer; + monal_timer_block_t _Nullable _cancelHandler; + NSString* _Nullable _description; + NSTimeInterval _timeout; + NSTimeInterval _remainingTime; + NSUUID* _uuid; +} +@end + +@implementation MLDelayableTimer + +-(instancetype) initWithHandler:(monal_timer_block_t) handler andCancelHandler:(monal_timer_block_t _Nullable) cancelHandler timeout:(NSTimeInterval) timeout tolerance:(NSTimeInterval) tolerance andDescription:(NSString* _Nullable) description +{ + self = [super init]; + _wrappedTimer = [NSTimer timerWithTimeInterval:timeout repeats:NO block:^(NSTimer* _) { + handler(self); + }]; + _cancelHandler = cancelHandler; + _timeout = timeout; + _wrappedTimer.tolerance = tolerance; + _description = description; + _remainingTime = 0; + _uuid = [NSUUID UUID]; + return self; +} + +-(NSString*) description +{ + return [NSString stringWithFormat:@"%@(%G|%G) %@", [_uuid UUIDString], _timeout, _wrappedTimer.fireDate.timeIntervalSinceNow, _description]; +} + +-(void) start +{ + @synchronized(self) { + if(!_wrappedTimer.valid) + { + unreachable(@"Could not start already fired timer!", @{@"timer": self}); + return; + } + DDLogDebug(@"Starting timer: %@", self); + [[HelperTools getExtraRunloopWithIdentifier:MLRunLoopIdentifierTimer] addTimer:_wrappedTimer forMode:NSRunLoopCommonModes]; + } +} + +-(void) trigger +{ + @synchronized(self) { + if(!_wrappedTimer.valid) + { + unreachable(@"Could not trigger already fired timer!", @{@"timer": self}); + return; + } + DDLogDebug(@"Triggering timer: %@", self); + [_wrappedTimer fire]; + } +} + +-(void) pause +{ + @synchronized(self) { + if(!_wrappedTimer.valid) + { + DDLogWarn(@"Tried to pause already fired timer: %@", self); + return; + } + DDLogDebug(@"Pausing timer: %@", self); + _remainingTime = _wrappedTimer.fireDate.timeIntervalSinceNow; + _wrappedTimer.fireDate = NSDate.distantFuture; //postpone timer virtually indefinitely + } +} + +-(void) resume +{ + @synchronized(self) { + if(!_wrappedTimer.valid) + { + DDLogWarn(@"Tried to resume already fired timer: %@", self); + return; + } + DDLogDebug(@"Resuming timer: %@", self); + _wrappedTimer.fireDate = [NSDate dateWithTimeIntervalSinceNow:_remainingTime]; + _remainingTime = 0; + } +} + +-(void) cancel +{ + @synchronized(self) { + if(!_wrappedTimer.valid) + { + DDLogWarn(@"Tried to cancel already fired timer: %@", self); + return; + } + DDLogDebug(@"Canceling timer: %@", self); + [self invalidate]; + } + _cancelHandler(self); +} + +-(void) invalidate +{ + @synchronized(self) { + if(!_wrappedTimer.valid) + { + unreachable(@"Could not invalidate already invalid timer!", @{@"timer": self}); + return; + } + //DDLogVerbose(@"Invalidating timer: %@", self); + [_wrappedTimer invalidate]; + } +} + +@end diff --git a/Monal/Classes/MLFiletransfer.m b/Monal/Classes/MLFiletransfer.m index 804d565f94..78ef4f6347 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, }; } @@ -620,8 +623,17 @@ +(MLHandler*) prepareUIImageUpload:(UIImage*) image NSString* tempname = [NSString stringWithFormat:@"tmp.%@", [[NSUUID UUID] UUIDString]]; NSError* error; NSString* file = [_documentCacheDir stringByAppendingPathComponent:tempname]; - DDLogDebug(@"Tempstoring jpeg encoded file having quality %f at %@", imageQuality, file); - NSData* imageData = UIImageJPEGRepresentation(image, imageQuality); + NSData* imageData = nil; + if(imageQuality == 1.0) + { + DDLogDebug(@"Image upload quality was set to 100%%, tempstoring png encoded file at %@", file); + imageData = UIImagePNGRepresentation(image); + } + else + { + DDLogDebug(@"Tempstoring jpeg encoded file having quality %f at %@", imageQuality, file); + imageData = UIImageJPEGRepresentation(image, imageQuality); + } [imageData writeToFile:file options:NSDataWritingAtomic error:&error]; if(error) { diff --git a/Monal/Classes/MLIQProcessor.m b/Monal/Classes/MLIQProcessor.m index ce67ab5d39..45bb7f99df 100644 --- a/Monal/Classes/MLIQProcessor.m +++ b/Monal/Classes/MLIQProcessor.m @@ -649,8 +649,7 @@ +(BOOL) processRosterWithAccount:(xmpp*) account andIqNode:(XMPPIQ*) iqNode $$class_handler(handleSetMamPrefs, $$ID(xmpp*, account), $$ID(XMPPIQ*, iqNode)) if([iqNode check:@"/"]) { - DDLogError(@"Setting MAM prefs returned an error: %@", [iqNode findFirst:@"error"]); - [HelperTools postError:NSLocalizedString(@"XMPP mam preferences error", @"") withNode:iqNode andAccount:account andIsSevere:NO]; + DDLogError(@"Setting MAM prefs returned an error, ignoring: %@", [iqNode findFirst:@"error"]); return; } $$ @@ -746,7 +745,7 @@ +(BOOL) processRosterWithAccount:(xmpp*) account andIqNode:(XMPPIQ*) iqNode } DDLogInfo(@"Successfully moderated message in muc: %@", msg); - [[DataLayer sharedInstance] deleteMessageHistory:msg.messageDBId]; + [[DataLayer sharedInstance] retractMessageHistory:msg.messageDBId]; //update ui DDLogInfo(@"Sending out kMonalDeletedMessageNotice notification for historyId %@", msg.messageDBId); @@ -763,6 +762,32 @@ +(BOOL) processRosterWithAccount:(xmpp*) account andIqNode:(XMPPIQ*) iqNode }]; $$ +#ifdef IS_QUICKSY +$$class_handler(handleQuicksyPhoneBook, $$ID(xmpp*, account), $$ID(XMPPIQ*, iqNode), $$ID(NSDictionary*, numbers)) + if([iqNode check:@"/"]) + { + DDLogError(@"Quicksy phonebook synchronize returned an error: %@", [iqNode findFirst:@"error"]); + [HelperTools postError:NSLocalizedString(@"Failed to synchronize phonebook", @"") withNode:iqNode andAccount:account andIsSevere:NO]; + return; + } + + for(MLXMLNode* entry in [iqNode find:@"{im.quicksy.synchronization:0}phone-book/entry"]) + { + NSString* nick = numbers[[entry findFirst:@"/@number"]]; + for(NSString* jid in [entry find:@"jid#"]) + { + DDLogDebug(@"Adding '%@' with nick '%@' to local roster...", jid, nick); + [[DataLayer sharedInstance] addContact:jid forAccount:account.accountNo nickname:nick]; +#ifndef DISABLE_OMEMO + // Request omemo devicelist + [account.omemo subscribeAndFetchDevicelistIfNoSessionExistsForJid:jid]; +#endif// DISABLE_OMEMO + + } + } +$$ +#endif + +(void) respondWithErrorTo:(XMPPIQ*) iqNode onAccount:(xmpp*) account { XMPPIQ* errorIq = [[XMPPIQ alloc] initAsErrorTo:iqNode]; diff --git a/Monal/Classes/MLMAMPrefTableViewController.m b/Monal/Classes/MLMAMPrefTableViewController.m index 743435154b..f296fc627c 100644 --- a/Monal/Classes/MLMAMPrefTableViewController.m +++ b/Monal/Classes/MLMAMPrefTableViewController.m @@ -36,7 +36,7 @@ -(void) viewWillAppear:(BOOL)animated self.mamPref = [NSMutableArray new]; [self.mamPref addObject:@{@"Title":NSLocalizedString(@"Always archive", @""), @"Description":NSLocalizedString(@"All messages are archived by default.", @""), @"value":@"always"}]; [self.mamPref addObject:@{@"Title":NSLocalizedString(@"Never archive", @""), @"Description":NSLocalizedString(@"Messages never archived by default.", @""), @"value":@"never"}]; - [self.mamPref addObject:@{@"Title":NSLocalizedString(@"Only contacts", @""), @"Description":NSLocalizedString(@"Archive only if the contact is in contact list", @""), @"value":@"roster"}]; + [self.mamPref addObject:@{@"Title":NSLocalizedString(@"Only contacts", @""), @"Description":NSLocalizedString(@"Archive only if the contact is in contact list.", @""), @"value":@"roster"}]; } -(void) dealloc diff --git a/Monal/Classes/MLMessageProcessor.m b/Monal/Classes/MLMessageProcessor.m index 51141e8708..b7059261ed 100644 --- a/Monal/Classes/MLMessageProcessor.m +++ b/Monal/Classes/MLMessageProcessor.m @@ -312,6 +312,7 @@ +(MLMessage* _Nullable) processMessage:(XMPPMessage*) messageNode andOuterMessag NSString* ownNick; NSString* actualFrom = messageNode.fromUser; NSString* participantJid = nil; + NSString* occupantId = nil; if([messageNode check:@"/"] && messageNode.fromResource) { ownNick = [[DataLayer sharedInstance] ownNickNameforMuc:messageNode.fromUser forAccount:account.accountNo]; @@ -322,8 +323,17 @@ +(MLMessage* _Nullable) processMessage:(XMPPMessage*) messageNode andOuterMessag participantJid = [messageNode findFirst:@"//{http://jabber.org/protocol/muc#user}x/item@jid"]; if(![outerMessageNode check:@"{urn:xmpp:mam:2}result"] || participantJid == nil) { - NSDictionary* mucParticipant = [[DataLayer sharedInstance] getParticipantForNick:actualFrom inRoom:messageNode.fromUser forAccountId:account.accountNo]; - participantJid = mucParticipant ? mucParticipant[@"participant_jid"] : nil; + if([[account.mucProcessor getRoomFeaturesForMuc:messageNode.fromUser] containsObject:@"urn:xmpp:occupant-id:0"] && [messageNode check:@"{urn:xmpp:occupant-id:0}occupant-id@id"]) + { + occupantId = [messageNode findFirst:@"{urn:xmpp:occupant-id:0}occupant-id@id"]; + NSDictionary* mucParticipant = [[DataLayer sharedInstance] getParticipantForOccupant:occupantId inRoom:messageNode.fromUser forAccountId:account.accountNo]; + participantJid = mucParticipant ? mucParticipant[@"participant_jid"] : nil; + } + else + { + NSDictionary* mucParticipant = [[DataLayer sharedInstance] getParticipantForNick:actualFrom inRoom:messageNode.fromUser forAccountId:account.accountNo]; + participantJid = mucParticipant ? mucParticipant[@"participant_jid"] : nil; + } } //make sure this is not the full jid if(participantJid != nil) @@ -446,13 +456,13 @@ +(MLMessage* _Nullable) processMessage:(XMPPMessage*) messageNode andOuterMessag } else { - //this checks for all spelled out in the business rules of XEP-0424 - historyIdToRetract = [[DataLayer sharedInstance] getRetractionHistoryIDForMessageId:idToRetract from:messageNode.fromUser actualFrom:actualFrom participantJid:participantJid andAccount:account.accountNo]; + //this checks for everything spelled out in the business rules of XEP-0424 + historyIdToRetract = [[DataLayer sharedInstance] getRetractionHistoryIDForMessageId:idToRetract from:messageNode.fromUser participantJid:participantJid occupantId:occupantId andAccount:account.accountNo]; } if(historyIdToRetract != nil) { - [[DataLayer sharedInstance] deleteMessageHistory:historyIdToRetract]; + [[DataLayer sharedInstance] retractMessageHistory:historyIdToRetract]; //update ui DDLogInfo(@"Sending out kMonalDeletedMessageNotice notification for historyId %@", historyIdToRetract); @@ -486,6 +496,7 @@ +(MLMessage* _Nullable) processMessage:(XMPPMessage*) messageNode andOuterMessag forAccount:account.accountNo withBody:@"" actuallyfrom:actualFrom + occupantId:occupantId participantJid:participantJid sent:YES unread:NO @@ -500,7 +511,7 @@ +(MLMessage* _Nullable) processMessage:(XMPPMessage*) messageNode andOuterMessag ]; //...then retract this message (e.g. mark as retracted) - [[DataLayer sharedInstance] deleteMessageHistory:historyIdToRetract]; + [[DataLayer sharedInstance] retractMessageHistory:historyIdToRetract]; //update ui DDLogInfo(@"Sending out kMonalDeletedMessageNotice notification for historyId %@", historyIdToRetract); @@ -560,6 +571,7 @@ +(MLMessage* _Nullable) processMessage:(XMPPMessage*) messageNode andOuterMessag if(body) { + BOOL LMCReplaced = NO; NSNumber* historyId = nil; //handle LMC @@ -568,12 +580,15 @@ +(MLMessage* _Nullable) processMessage:(XMPPMessage*) messageNode andOuterMessag NSString* messageIdToReplace = [messageNode findFirst:@"{urn:xmpp:message-correct:0}replace@id"]; DDLogVerbose(@"Message id to LMC-replace: %@", messageIdToReplace); //this checks if this message is from the same jid as the message it tries to do the LMC for (e.g. inbound can only correct inbound and outbound only outbound) - historyId = [[DataLayer sharedInstance] getLMCHistoryIDForMessageId:messageIdToReplace from:messageNode.fromUser actualFrom:actualFrom participantJid:participantJid andAccount:account.accountNo]; + historyId = [[DataLayer sharedInstance] getLMCHistoryIDForMessageId:messageIdToReplace from:messageNode.fromUser occupantId:occupantId participantJid:participantJid andAccount:account.accountNo]; DDLogVerbose(@"History id to LMC-replace: %@", historyId); //now check if the LMC is allowed (we use historyIdToUse for MLhistory mam queries to only check LMC for the 3 messages coming before this ID in this converastion) //historyIdToUse will be nil, for messages going forward in time which means (check for the newest 3 messages in this conversation) if(historyId != nil && [[DataLayer sharedInstance] checkLMCEligible:historyId encrypted:encrypted historyBaseID:historyIdToUse]) + { [[DataLayer sharedInstance] updateMessageHistory:historyId withText:body]; + LMCReplaced = YES; + } else historyId = nil; } @@ -588,6 +603,7 @@ +(MLMessage* _Nullable) processMessage:(XMPPMessage*) messageNode andOuterMessag forAccount:account.accountNo withBody:[body copy] actuallyfrom:actualFrom + occupantId:occupantId participantJid:participantJid sent:YES unread:unread @@ -663,6 +679,7 @@ +(MLMessage* _Nullable) processMessage:(XMPPMessage*) messageNode andOuterMessag @"message": message, @"showAlert": @(showAlert), @"contact": possiblyUnknownContact, + @"LMCReplaced": @(LMCReplaced), }]; //try to automatically determine content type of filetransfers @@ -675,7 +692,7 @@ +(MLMessage* _Nullable) processMessage:(XMPPMessage*) messageNode andOuterMessag { //just try to use the probably reflected message to update the stanzaid of our message in the db //messageId is always a proper origin-id in this case, because inbound == NO and Monal uses origin-ids - NSNumber* historyId = [[DataLayer sharedInstance] hasMessageForStanzaId:stanzaid orMessageID:messageId withInboundDir:inbound andJid:buddyName onAccount:account.accountNo]; + NSNumber* historyId = [[DataLayer sharedInstance] hasMessageForStanzaId:stanzaid orMessageID:messageId withInboundDir:inbound occupantId:occupantId andJid:buddyName onAccount:account.accountNo]; if(historyId != nil) { message = [[DataLayer sharedInstance] messageForHistoryID:historyId]; diff --git a/Monal/Classes/MLMucProcessor.m b/Monal/Classes/MLMucProcessor.m index 5d41ca388c..0235708d84 100644 --- a/Monal/Classes/MLMucProcessor.m +++ b/Monal/Classes/MLMucProcessor.m @@ -189,7 +189,7 @@ -(void) handleSentMessage:(NSNotification*) notification XMPPMessage* msg = notification.userInfo[@"message"]; NSString* callUiHandlerFor = nil; - //check if this is a direct invite (direct invites always follow indirect ones, so we don't have to check for indirect ones) + //check if this is a direct invite if([msg check:@"/{jabber:client}message/{jabber:x:conference}x@jid"]) callUiHandlerFor = [msg findFirst:@"/{jabber:client}message/{jabber:x:conference}x@jid"]; @@ -361,6 +361,8 @@ -(void) processPresence:(XMPPPresence*) presenceNode if(item[@"jid"]) item[@"jid"] = [HelperTools splitJid:item[@"jid"]][@"user"]; item[@"nick"] = presenceNode.fromResource; + if([_roomFeatures[presenceNode.fromUser] containsObject:@"urn:xmpp:occupant-id:0"]) + item[@"occupant_id"] = [presenceNode findFirst:@"{urn:xmpp:occupant-id:0}occupant-id@id"]; //handle participant updates if([presenceNode check:@"/"] || item[@"affiliation"] == nil) @@ -420,7 +422,7 @@ -(BOOL) processMessage:(XMPPMessage*) messageNode } MLContact* inviteFrom = [MLContact createContactFromJid:invitedMucJid andAccountNo:_account.accountNo]; DDLogInfo(@"Got mediated muc invite from %@ for %@...", inviteFrom, messageNode.fromUser); - if(!inviteFrom.isSubscribedFrom) + if(![[HelperTools defaultsDB] boolForKey: @"allowNonRosterContacts"] && !inviteFrom.isSubscribedFrom) { DDLogWarn(@"Ignoring invite from %@, this jid isn't at least marked as susbscribedFrom in our roster...", inviteFrom); return YES; //don't process this further @@ -439,7 +441,7 @@ -(BOOL) processMessage:(XMPPMessage*) messageNode MLContact* inviteFrom = [MLContact createContactFromJid:messageNode.fromUser andAccountNo:_account.accountNo]; DDLogInfo(@"Got direct muc invite from %@ for %@ --> joining...", inviteFrom, [messageNode findFirst:@"{jabber:x:conference}x@jid"]); - if(!inviteFrom.isSubscribedFrom) + if(![[HelperTools defaultsDB] boolForKey: @"allowNonRosterContacts"] && !inviteFrom.isSubscribedFrom) { DDLogWarn(@"Ignoring invite from %@, this jid isn't at least marked as susbscribedFrom in our roster...", inviteFrom); return YES; //don't process this further @@ -622,14 +624,15 @@ -(void) configureMuc:(NSString*) roomJid withMandatoryOptions:(NSDictionary*) ma @"roomJid": [NSString stringWithFormat:@"%@", roomJid], })); - [self callSuccessUIHandlerForMuc:iqNode.fromUser]; - + //don't call success handler if we are only "half-joined" (see comments below for what that means) if(joinOnSuccess) { //group is now properly configured and we are joined, but all the code handling a proper join was not run //--> join again to make sure everything is sane [self join:roomJid]; } + else + [self callSuccessUIHandlerForMuc:iqNode.fromUser]; $$ -(void) handleStatusCodes:(XMPPStanza*) node @@ -1233,22 +1236,13 @@ -(void) ping:(NSString*) roomJid withLastPing:(NSDate* _Nullable) lastPing -(void) inviteUser:(NSString*) jid inMuc:(NSString*) roomJid { - DDLogInfo(@"Inviting user '%@' to '%@' directly & indirectly", jid, roomJid); - - XMPPMessage* indirectInviteMsg = [[XMPPMessage alloc] initWithType:kMessageNormalType to:roomJid]; - [indirectInviteMsg addChildNode:[[MLXMLNode alloc] initWithElement:@"x" andNamespace:@"http://jabber.org/protocol/muc#user" withAttributes:@{} andChildren:@[ - [[MLXMLNode alloc] initWithElement:@"invite" withAttributes:@{ - @"to": jid - } andChildren:@[] andData:nil] - ] andData:nil]]; - [self->_account send:indirectInviteMsg]; - + DDLogInfo(@"Directly inviting user '%@' to '%@'...", jid, roomJid); XMPPMessage* directInviteMsg = [[XMPPMessage alloc] initWithType:kMessageNormalType to:jid]; [directInviteMsg addChildNode:[[MLXMLNode alloc] initWithElement:@"x" andNamespace:@"jabber:x:conference" withAttributes:@{ @"jid": roomJid } andChildren:@[] andData:nil]]; + [directInviteMsg setStoreHint]; [self->_account send:directInviteMsg]; - } -(void) setAffiliation:(NSString*) affiliation ofUser:(NSString*) jid inMuc:(NSString*) roomJid @@ -1457,14 +1451,19 @@ -(void) publishAvatar:(UIImage* _Nullable) image forMuc:(NSString*) room } //make public channels "mention only" on first join if([@"channel" isEqualToString:mucType]) + { + DDLogDebug(@"Configuring new muc %@ to be mention-only...", iqNode.fromUser); [[DataLayer sharedInstance] setMucAlertOnMentionOnly:iqNode.fromUser onAccount:_account.accountNo]; + } } if(![mucType isEqualToString:[[DataLayer sharedInstance] getMucTypeOfRoom:iqNode.fromUser andAccount:_account.accountNo]]) { - DDLogInfo(@"Configuring muc %@ to type '%@'...", iqNode.fromUser, mucType); + DDLogInfo(@"Configuring muc %@ to be of type '%@'...", iqNode.fromUser, mucType); [[DataLayer sharedInstance] updateMucTypeTo:mucType forRoom:iqNode.fromUser andAccount:_account.accountNo]; } + else + DDLogDebug(@"Muc %@ is already configured to be of type '%@' ('%@')...", iqNode.fromUser, mucType, [[DataLayer sharedInstance] getMucTypeOfRoom:iqNode.fromUser andAccount:_account.accountNo]); if(!mucName || ![mucName length]) mucName = @""; @@ -1491,9 +1490,12 @@ -(void) publishAvatar:(UIImage* _Nullable) image forMuc:(NSString*) room if(join) { - DDLogInfo(@"Clearing muc participants and members tables for %@", iqNode.fromUser); - [[DataLayer sharedInstance] cleanupMembersAndParticipantsListFor:iqNode.fromUser forAccountId:_account.accountNo]; - + for(NSString* type in @[@"member", @"admin", @"owner"]) + { + DDLogInfo(@"Clearing muc participants table for type %@: %@", type, iqNode.fromUser); + [[DataLayer sharedInstance] cleanupParticipantsListFor:iqNode.fromUser andType:type onAccountId:_account.accountNo]; + } + //now try to join this room if requested [self sendJoinPresenceFor:iqNode.fromUser]; } @@ -1525,6 +1527,8 @@ -(void) sendJoinPresenceFor:(NSString*) room $$instance_handler(handleMembersList, account.mucProcessor, $$ID(xmpp*, account), $$ID(XMPPIQ*, iqNode), $$ID(NSString*, type)) DDLogInfo(@"Got %@s list from %@...", type, iqNode.fromUser); + DDLogInfo(@"Clearing muc members table for type %@: %@", type, iqNode.fromUser); + [[DataLayer sharedInstance] cleanupMembersListFor:iqNode.fromUser andType:type onAccountId:_account.accountNo]; [self handleMembersListUpdate:[iqNode find:@"{http://jabber.org/protocol/muc#admin}query/item@@"] forMuc:iqNode.fromUser]; [self logMembersOfMuc:iqNode.fromUser]; $$ diff --git a/Monal/Classes/MLNotificationManager.m b/Monal/Classes/MLNotificationManager.m index 762165ba87..75ebb7a0c2 100644 --- a/Monal/Classes/MLNotificationManager.m +++ b/Monal/Classes/MLNotificationManager.m @@ -24,6 +24,12 @@ @import AVFoundation; @import UniformTypeIdentifiers; +typedef NS_ENUM(NSUInteger, MLNotificationState) { + MLNotificationStateNone, + MLNotificationStatePending, + MLNotificationStateDelivered, +}; + @interface MLNotificationManager () @property (nonatomic, readonly) NotificationPrivacySettingOption notificationPrivacySetting; @end @@ -167,28 +173,70 @@ -(void) handleXMPPError:(NSNotification*) notification #pragma mark message signals +-(AnyPromise*) notificationStateForMessage:(MLMessage*) message +{ + NSString* idval = [self identifierWithMessage:message]; + NSMutableArray* promises = [NSMutableArray new]; + + [promises addObject:[AnyPromise promiseWithResolverBlock:^(PMKResolver resolve) { + DDLogVerbose(@"Checking for 'pending' notification state for '%@'...", idval); + [[UNUserNotificationCenter currentNotificationCenter] getPendingNotificationRequestsWithCompletionHandler:^(NSArray* requests) { + for(UNNotificationRequest* request in requests) + if([request.identifier isEqualToString:idval]) + { + DDLogDebug(@"Notification state 'pending' for: %@", idval); + resolve(@(MLNotificationStatePending)); + return; + } + resolve(@(MLNotificationStateNone)); + }]; + }]]; + + [promises addObject:[AnyPromise promiseWithResolverBlock:^(PMKResolver resolve) { + DDLogVerbose(@"Checking for 'delivered' notification state for '%@'...", idval); + [[UNUserNotificationCenter currentNotificationCenter] getDeliveredNotificationsWithCompletionHandler:^(NSArray* notifications) { + for(UNNotification* notification in notifications) + if([notification.request.identifier isEqualToString:idval]) + { + DDLogDebug(@"Notification state 'delivered' for: %@", idval); + resolve(@(MLNotificationStateDelivered)); + return; + } + resolve(@(MLNotificationStateNone)); + }]; + }]]; + + + return PMKWhen(promises).then(^(NSArray* results) { + DDLogVerbose(@"Notification state check for '%@' completed...", idval); + for(NSNumber* entry in results) + if(entry.integerValue != MLNotificationStateNone) + return entry; + return @(MLNotificationStateNone); + }); +} + -(void) handleFiletransferUpdate:(NSNotification*) notification { xmpp* xmppAccount = notification.object; MLMessage* message = [notification.userInfo objectForKey:@"message"]; NSString* idval = [self identifierWithMessage:message]; - - [[UNUserNotificationCenter currentNotificationCenter] getPendingNotificationRequestsWithCompletionHandler:^(NSArray* requests) { - for(UNNotificationRequest* request in requests) - if([request.identifier isEqualToString:idval]) - { - DDLogDebug(@"Already pending notification '%@', updating it...", idval); - [self internalMessageHandlerWithMessage:message andAccount:xmppAccount showAlert:YES andSound:YES]; - } - }]; - [[UNUserNotificationCenter currentNotificationCenter] getDeliveredNotificationsWithCompletionHandler:^(NSArray* notifications) { - for(UNNotification* notification in notifications) - if([notification.request.identifier isEqualToString:idval]) - { - DDLogDebug(@"Already displayed notification '%@', updating it...", idval); - [self internalMessageHandlerWithMessage:message andAccount:xmppAccount showAlert:YES andSound:NO]; - } - }]; + //do this asynchronous on a background thread + [self notificationStateForMessage:message].thenInBackground(^(NSNumber* _state) { + MLNotificationState state = _state.integerValue; + if(state == MLNotificationStatePending || state == MLNotificationStateNone) + { + DDLogDebug(@"Already pending or unknown notification '%@', updating/posting it...", idval); + [self internalMessageHandlerWithMessage:message andAccount:xmppAccount showAlert:YES andSound:YES andLMCReplaced:NO]; + } + else if(state == MLNotificationStateDelivered) + { + DDLogDebug(@"Already displayed notification '%@', updating it...", idval); + [self internalMessageHandlerWithMessage:message andAccount:xmppAccount showAlert:YES andSound:NO andLMCReplaced:NO]; + } + else + unreachable(@"Unknown MLNotificationState!", @{@"state": @(state)}); + }); } -(void) handleNewMessage:(NSNotification*) notification @@ -196,10 +244,11 @@ -(void) handleNewMessage:(NSNotification*) notification xmpp* xmppAccount = notification.object; MLMessage* message = [notification.userInfo objectForKey:@"message"]; BOOL showAlert = notification.userInfo[@"showAlert"] ? [notification.userInfo[@"showAlert"] boolValue] : NO; - [self internalMessageHandlerWithMessage:message andAccount:xmppAccount showAlert:showAlert andSound:YES]; + BOOL LMCReplaced = notification.userInfo[@"LMCReplaced"] ? [notification.userInfo[@"LMCReplaced"] boolValue] : NO; + [self internalMessageHandlerWithMessage:message andAccount:xmppAccount showAlert:showAlert andSound:YES andLMCReplaced:LMCReplaced]; } --(void) internalMessageHandlerWithMessage:(MLMessage*) message andAccount:(xmpp*) xmppAccount showAlert:(BOOL) showAlert andSound:(BOOL) sound +-(void) internalMessageHandlerWithMessage:(MLMessage*) message andAccount:(xmpp*) xmppAccount showAlert:(BOOL) showAlert andSound:(BOOL) sound andLMCReplaced:(BOOL) LMCReplaced { if([message.messageType isEqualToString:kMessageTypeStatus]) return; @@ -231,6 +280,20 @@ -(void) internalMessageHandlerWithMessage:(MLMessage*) message andAccount:(xmpp* return; } + //check if we need to replace the still displayed notification or ignore this LMC + if(LMCReplaced) + { + NSString* idval = [self identifierWithMessage:message]; + //wait synchronous for completion (needed for appex) + MLNotificationState state = PMKHangEnum([self notificationStateForMessage:message]); + DDLogVerbose(@"Notification state for '%@': %@", idval, @(state)); + if(state == MLNotificationStateNone) + { + DDLogDebug(@"not showing notification for LMC: this notification was already removed earlier"); + return; + } + } + if([HelperTools isNotInFocus]) { DDLogVerbose(@"notification manager should show notification in background: %@", message.messageText); 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/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 index 4913c80825..450d5f133c 100644 --- a/Monal/Classes/MLPlaceholderViewController.m +++ b/Monal/Classes/MLPlaceholderViewController.m @@ -6,41 +6,17 @@ // Copyright © 2020 Monal.im. All rights reserved. // -#import "MLPlaceholderViewController.h" - -@interface MLPlaceholderViewController () - -@property (weak, nonatomic) IBOutlet UIImageView *backgroundImageView; +#import +@interface MLPlaceholderViewController : UIViewController @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/Classes/MLPubSub.m b/Monal/Classes/MLPubSub.m index bb180b2742..ff554549d7 100644 --- a/Monal/Classes/MLPubSub.m +++ b/Monal/Classes/MLPubSub.m @@ -32,6 +32,7 @@ @implementation MLPubSub +(void) initialize { + //TODO: wait for servers to support pubsub#publish_node_full and set it at least for bookmarks2 _defaultOptions = @{ @"pubsub#notify_retract": @"true", @"pubsub#notify_delete": @"true" diff --git a/Monal/Classes/MLSQLite.m b/Monal/Classes/MLSQLite.m index 1a7d320adb..386ed8f30e 100644 --- a/Monal/Classes/MLSQLite.m +++ b/Monal/Classes/MLSQLite.m @@ -75,9 +75,7 @@ -(id) initWithFile:(NSString*) dbFile [HelperTools configureFileProtectionFor:[NSString stringWithFormat:@"%@-shm", _dbFile]]; if(sqlite3_open_v2([_dbFile UTF8String], &(self->_database), SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, nil) == SQLITE_OK) - { DDLogInfo(@"Database opened: %@", _dbFile); - } else { //database error message @@ -113,6 +111,10 @@ -(id) initWithFile:(NSString*) dbFile DDLogError(@"Database locked, while calling 'PRAGMA truncate;', retrying..."); while([self executeNonQuery:@"PRAGMA foreign_keys=on;" andArguments:@[] withException:NO] != YES) DDLogError(@"Database locked, while calling 'PRAGMA foreign_keys=on;', retrying..."); + //this seems to provide *slightly* better security + //see https://sqlite.org/pragma.html#pragma_trusted_schema + while([self executeNonQuery:@"PRAGMA trusted_schema = off;" andArguments:@[] withException:NO] != YES) + DDLogError(@"Database locked, while calling 'PRAGMA trusted_schema = off;', retrying..."); return self; } diff --git a/Monal/Classes/MLSettingsTableViewController.m b/Monal/Classes/MLSettingsTableViewController.m index a73297329e..6fc4cb29ad 100644 --- a/Monal/Classes/MLSettingsTableViewController.m +++ b/Monal/Classes/MLSettingsTableViewController.m @@ -13,6 +13,8 @@ #import "DataLayer.h" #import "MLXMPPManager.h" #import "XMPPEdit.h" +#import "MonalAppDelegate.h" +#import "ActiveChatsViewController.h" #import @import SafariServices; @@ -26,8 +28,10 @@ }; enum SettingsAccountRows { +#ifndef IS_QUICKSY QuickSettingsRow, AdvancedSettingsRow, +#endif SettingsAccountRowsCnt }; @@ -58,6 +62,10 @@ //this will hold all disabled rows of all enums (this is needed because the code below still references these rows) enum DummySettingsRows { +#ifdef IS_QUICKSY + QuickSettingsRow, + AdvancedSettingsRow, +#endif DummySettingsRowsBegin = 100, }; @@ -106,6 +114,12 @@ -(void) viewWillAppear:(BOOL)animated self.selected = nil; } +-(void) viewWillDisappear:(BOOL) animated +{ + [super viewWillDisappear:animated]; + [((MonalAppDelegate*)UIApplication.sharedApplication.delegate).activeChats sheetDismissed]; +} + #pragma mark - key commands -(BOOL) canBecomeFirstResponder @@ -129,7 +143,11 @@ -(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger) { switch(section) { +#ifdef IS_QUICKSY + case kSettingSectionAccounts: return [self getAccountNum] + SettingsAccountRowsCnt; +#else case kSettingSectionAccounts: return [self getAccountNum] + SettingsAccountRowsCnt; +#endif case kSettingSectionApp: return SettingsAppRowsCnt; case kSettingSectionSupport: return SettingsSupportRowCnt; #ifndef DEBUG @@ -206,7 +224,9 @@ -(UITableViewCell*) tableView:(UITableView*) tableView cellForRowAtIndexPath:(NS } else { - MLAssert(indexPath.row - [self getAccountNum] < SettingsAccountRowsCnt, @"Tried to tap onto a row ment to be for a concrete account, not for quick or advanced settings"); +#ifndef IS_QUICKSY + MLAssert(indexPath.row - [self getAccountNum] < SettingsAccountRowsCnt, @"Tried to tap onto a row meant to be for a concrete account, not for quick or advanced settings"); + // User selected one of the 'add account' promts switch(indexPath.row - [self getAccountNum]) { case QuickSettingsRow: @@ -218,6 +238,7 @@ -(UITableViewCell*) tableView:(UITableView*) tableView cellForRowAtIndexPath:(NS default: unreachable(); } +#endif } break; } diff --git a/Monal/Classes/MLXMLNode.m b/Monal/Classes/MLXMLNode.m index 0c31545d97..404f37ab76 100644 --- a/Monal/Classes/MLXMLNode.m +++ b/Monal/Classes/MLXMLNode.m @@ -365,13 +365,16 @@ -(NSArray*) find:(NSString* _Nonnull) queryString arguments:(va_list*) args #endif //return results from cache if possible + NSArray* cacheObj = nil; WeakContainer* cacheEntryContainer = [self.cache objectForKey:cacheKey]; - if(cacheEntryContainer != nil && cacheEntryContainer.obj != nil) + if(cacheEntryContainer != nil) + cacheObj = cacheEntryContainer.obj; + if(cacheObj != nil) { #ifdef DEBUG_XMLQueryLanguage - DDLogVerbose(@"Returning cached result: %@", (NSArray*)cacheEntryContainer.obj); + DDLogVerbose(@"Returning cached result: %@", cacheObj); #endif - return (NSArray*)cacheEntryContainer.obj; + return cacheObj; } #ifdef QueryStatistics diff --git a/Monal/Classes/MLXMPPManager.m b/Monal/Classes/MLXMPPManager.m index f4095737a9..1fb8697992 100644 --- a/Monal/Classes/MLXMPPManager.m +++ b/Monal/Classes/MLXMPPManager.m @@ -39,33 +39,7 @@ @implementation MLXMPPManager -(void) defaultSettings { - BOOL setDefaults = [[HelperTools defaultsDB] boolForKey:@"SetDefaults"]; - if(!setDefaults) - { - [[HelperTools defaultsDB] setBool:YES forKey:@"Sound"]; - [[HelperTools defaultsDB] setBool:NO forKey:@"ChatBackgrounds"]; - - // Privacy Settings - [[HelperTools defaultsDB] setBool:YES forKey:@"ShowGeoLocation"]; - [[HelperTools defaultsDB] setBool:YES forKey:@"SendLastUserInteraction"]; - [[HelperTools defaultsDB] setBool:YES forKey:@"SendLastChatState"]; - [[HelperTools defaultsDB] setBool:YES forKey:@"SendReceivedMarkers"]; - [[HelperTools defaultsDB] setBool:YES forKey:@"SendDisplayedMarkers"]; - [[HelperTools defaultsDB] setBool:YES forKey:@"ShowURLPreview"]; - - // Message Settings / Privacy - [[HelperTools defaultsDB] setInteger:NotificationPrivacySettingOptionDisplayNameAndMessage forKey:@"NotificationPrivacySetting"]; - - // udp logger - [[HelperTools defaultsDB] setBool:NO forKey:@"udpLoggerEnabled"]; - [[HelperTools defaultsDB] setObject:@"" forKey:@"udpLoggerHostname"]; - [[HelperTools defaultsDB] setObject:@"" forKey:@"udpLoggerPort"]; - [[HelperTools defaultsDB] setObject:@"" forKey:@"udpLoggerKey"]; - - [[HelperTools defaultsDB] setBool:YES forKey:@"SetDefaults"]; - [[HelperTools defaultsDB] synchronize]; - } - + [self upgradeBoolUserSettingsIfUnset:@"Sound" toDefault:YES]; [self upgradeObjectUserSettingsIfUnset:@"AlertSoundFile" toDefault:@"alert2"]; // upgrade ShowGeoLocation @@ -84,9 +58,16 @@ -(void) defaultSettings //upgrade url preview [self upgradeBoolUserSettingsIfUnset:@"ShowURLPreview" toDefault:YES]; - //upgrade message autodeletion - [self upgradeBoolUserSettingsIfUnset:@"AutodeleteAllMessagesAfter3Days" toDefault:NO]; - + //upgrade message autodeletion and migrate old "3 days" setting + NSNumber* oldAutodelete = [[HelperTools defaultsDB] objectForKey:@"AutodeleteAllMessagesAfter3Days"]; + if(oldAutodelete != nil && [oldAutodelete boolValue]) + { + [self upgradeIntegerUserSettingsIfUnset:@"AutodeleteInterval" toDefault:259200]; + [self removeObjectUserSettingsIfSet:@"AutodeleteAllMessagesAfter3Days"]; + } + else + [self upgradeIntegerUserSettingsIfUnset:@"AutodeleteInterval" toDefault:0]; + //upgrade default omemo on [self upgradeBoolUserSettingsIfUnset:@"OMEMODefaultOn" toDefault:YES]; @@ -157,13 +138,19 @@ -(void) defaultSettings [self upgradeBoolUserSettingsIfUnset:@"useDnssecForAllConnections" toDefault:NO]; #endif - NSTimeZone* timeZone = [NSTimeZone localTimeZone]; DDLogVerbose(@"Current timezone name: '%@'...", [timeZone name]); if([[timeZone name] containsString:@"Europe"]) [self upgradeBoolUserSettingsIfUnset:@"useInlineSafari" toDefault:NO]; else [self upgradeBoolUserSettingsIfUnset:@"useInlineSafari" toDefault:YES]; + + [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 @@ -354,6 +341,21 @@ -(id) init while(YES) { for(xmpp* account in [MLXMPPManager sharedInstance].connectedXMPP) [account updateIqHandlerTimeouts]; + + //needed to not crash the app with an obscure EXC_BREAKPOINT while deleting something in a currently open chat + //the crash report then contains: message at /usr/lib/system/libdispatch.dylib: API MISUSE: Resurrection of an object + //(triggered by [HelperTools dispatchAsync:reentrantOnQueue:withBlock:] in it's call to dispatch_get_current_queue()) + dispatch_async(dispatch_get_main_queue(), ^{ + NSInteger autodeleteInterval = [[HelperTools defaultsDB] integerForKey:@"AutodeleteInterval"]; + if(autodeleteInterval > 0) + { + NSNumber* deletionCount = [[DataLayer sharedInstance] autoDeleteMessagesAfterInterval:(NSTimeInterval)autodeleteInterval]; + //make sure our ui updates after a deletion + if(deletionCount.integerValue > 0) + [[MLNotificationQueue currentQueue] postNotificationName:kMonalRefresh object:nil userInfo:nil]; + } + }); + [NSThread sleepForTimeInterval:1]; } }); @@ -400,6 +402,8 @@ -(void) noLongerInFocus -(void) nowBackgrounded { + DDLogInfo(@"App now backgrounded..."); + _isBackgrounded = YES; _isNotInFocus = YES; @@ -409,6 +413,8 @@ -(void) nowBackgrounded -(void) nowForegrounded { + DDLogInfo(@"App now foregrounded..."); + _isBackgrounded = NO; _isNotInFocus = NO; diff --git a/Monal/Classes/MemberList.swift b/Monal/Classes/MemberList.swift index a00f412b35..091e98e3df 100644 --- a/Monal/Classes/MemberList.swift +++ b/Monal/Classes/MemberList.swift @@ -21,6 +21,7 @@ struct MemberList: View { @State private var memberList: OrderedSet> @State private var affiliations: Dictionary, String> @State private var online: Dictionary, Bool> + @State private var nicknames: Dictionary, String> @State private var navigationActive: ObservableKVOWrapper? @State private var showAlert = false @State private var alertPrompt = AlertPrompt(dismissLabel: Text("Close")) @@ -35,6 +36,7 @@ struct MemberList: View { _memberList = State(wrappedValue:OrderedSet>()) _affiliations = State(wrappedValue:[:]) _online = State(wrappedValue:[:]) + _nicknames = State(wrappedValue:[:]) } func updateMemberlist() { @@ -42,12 +44,14 @@ struct MemberList: View { ownAffiliation = DataLayer.sharedInstance().getOwnAffiliation(inGroupOrChannel:self.muc.obj) ?? "none" affiliations.removeAll(keepingCapacity:true) online.removeAll(keepingCapacity:true) + nicknames.removeAll(keepingCapacity:true) for memberInfo in Array(DataLayer.sharedInstance().getMembersAndParticipants(ofMuc:self.muc.contactJid, forAccountId:account.accountNo)) { DDLogVerbose("Got member/participant entry: \(String(describing:memberInfo))") guard let jid = memberInfo["participant_jid"] as? String ?? memberInfo["member_jid"] as? String else { continue } let contact = ObservableKVOWrapper(MLContact.createContact(fromJid:jid, andAccountNo:account.accountNo)) + nicknames[contact] = memberInfo["room_nick"] as? String if !memberList.contains(contact) { continue } @@ -58,23 +62,29 @@ struct MemberList: View { online[contact] = false } } + //this is needed to improve sorting speed + var contactNames: [ObservableKVOWrapper:String] = [:] + for contact in memberList { + contactNames[contact] = contact.obj.contactDisplayName(withFallback:nicknames[contact], andSelfnotesPrefix:false) + } + //sort our member list memberList.sort { ( (online[$0]! ? 0 : 1), mucAffiliationToInt(affiliations[$0]), - ($0.contactDisplayNameWithoutSelfnotesPrefix as String), + (contactNames[$0]!), ($0.contactJid as String) ) < ( (online[$1]! ? 0 : 1), mucAffiliationToInt(affiliations[$1]), - ($1.contactDisplayNameWithoutSelfnotesPrefix as String), + (contactNames[$1]!), ($1.contactJid as String) ) } } - func performAction(headlineView: some View, descriptionView: some View, action: @escaping ()->Void) -> Promise { - return performMucAction(account:self.account, mucJid:self.muc.contactJid, overlay:self.overlay, headlineView:headlineView, descriptionView:descriptionView, action:action) + func promisifyAction(action: @escaping ()->Void) -> Promise { + return promisifyMucAction(account:self.account, mucJid:self.muc.contactJid, action:action) } func showAlert(title: Text, description: Text) { @@ -162,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) } } } @@ -220,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) } } } @@ -258,7 +268,7 @@ struct MemberList: View { if !contact.isSelfChat { HStack { HStack { - ContactEntry(contact:contact) { + ContactEntry(contact:contact, fallback:nicknames[contact]) { Text("Affiliation: \(mucAffiliationToString(affiliations[contact]))\(!(online[contact] ?? false) ? Text(" (offline)") : Text(""))") //.foregroundColor(Color(UIColor.secondaryLabel)) .font(.footnote) @@ -291,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) } } }) @@ -326,7 +336,9 @@ struct MemberList: View { .onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("kMonalMucParticipantsAndMembersUpdated")).receive(on: RunLoop.main)) { notification in if let xmppAccount = notification.object as? xmpp, let contact = notification.userInfo?["contact"] as? MLContact { DDLogVerbose("Got muc participants/members update from account \(xmppAccount)...") - if contact == self.muc { + //only trigger update if we are either in a group type muc or have admin/owner priviledges + //all other cases will close this view anyways, it makes no sense to update everything directly before hiding thsi view + if contact == self.muc && (contact.mucType == "group" || ["owner", "admin"].contains(DataLayer.sharedInstance().getOwnAffiliation(inGroupOrChannel:self.muc.obj) ?? "none")) { updateMemberlist() } } diff --git a/Monal/Classes/MonalAppDelegate.m b/Monal/Classes/MonalAppDelegate.m index 3a189b2412..dc15a8d2ac 100644 --- a/Monal/Classes/MonalAppDelegate.m +++ b/Monal/Classes/MonalAppDelegate.m @@ -351,7 +351,7 @@ -(void) updateUnread DDLogInfo(@"Updating unread called"); //make sure unread badge matches application badge NSNumber* unreadMsgCnt = [[DataLayer sharedInstance] countUnreadMessages]; - [HelperTools dispatchAsync:NO reentrantOnQueue:dispatch_get_main_queue() withBlock:^{ + [HelperTools dispatchAsync:YES reentrantOnQueue:dispatch_get_main_queue() withBlock:^{ NSInteger unread = 0; if(unreadMsgCnt != nil) unread = [unreadMsgCnt integerValue]; @@ -1000,7 +1000,13 @@ -(void) userNotificationCenter:(UNUserNotificationCenter*) center didReceiveNoti [[MLXMPPManager sharedInstance] removeContact:fromContact]; } else if([response.actionIdentifier isEqualToString:@"com.apple.UNNotificationDefaultActionIdentifier"]) //open chat of this contact - [self openChatOfContact:fromContact]; + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{ + while(self.activeChats == nil) + usleep(100000); + dispatch_async(dispatch_get_main_queue(), ^{ + [(ActiveChatsViewController*)self.activeChats showAddContact]; + }); + }); } else { @@ -1235,15 +1241,11 @@ -(void) applicationWillTerminate:(UIApplication *)application _shutdownPending = YES; DDLogWarn(@"|~~| T E R M I N A T I N G |~~|"); [HelperTools scheduleBackgroundTask:YES]; //make sure delivery will be attempted, if needed (force as soon as possible) - DDLogInfo(@"|~~| 20%% |~~|"); - [self updateUnread]; - DDLogInfo(@"|~~| 40%% |~~|"); - [[HelperTools defaultsDB] synchronize]; - DDLogInfo(@"|~~| 60%% |~~|"); + DDLogInfo(@"|~~| 33%% |~~|"); [[MLXMPPManager sharedInstance] nowBackgrounded]; - DDLogInfo(@"|~~| 80%% |~~|"); + DDLogInfo(@"|~~| 66%% |~~|"); [HelperTools updateSyncErrorsWithDeleteOnly:NO andWaitForCompletion:YES]; - DDLogInfo(@"|~~| 100%% |~~|"); + DDLogInfo(@"|~~| 99%% |~~|"); [[MLXMPPManager sharedInstance] disconnectAll]; DDLogInfo(@"|~~| T E R M I N A T E D |~~|"); [DDLog flushLog]; @@ -1615,9 +1617,10 @@ -(void) handleBackgroundProcessingTask:(BGTask*) task } if(![[MLXMPPManager sharedInstance] hasConnectivity]) - { DDLogError(@"BGTASK has *no* connectivity? That's strange!"); - } + + //we are a bg processing task potentially having minutes of background time --> vacuum database + [[DataLayer sharedInstance] vacuum]; [self startBackgroundTimer:BGPROCESS_GRACEFUL_TIMEOUT]; @synchronized(self) { diff --git a/Monal/Classes/OmemoKeys.swift b/Monal/Classes/OmemoKeys.swift index 09eea4dd70..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) @@ -211,10 +210,18 @@ 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 }) + } + + 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... @@ -233,7 +240,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")) ) @@ -253,6 +259,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() + } + } + } } } diff --git a/Monal/Classes/Quicksy_RegisterAccount.swift b/Monal/Classes/Quicksy_RegisterAccount.swift new file mode 100644 index 0000000000..430946c63c --- /dev/null +++ b/Monal/Classes/Quicksy_RegisterAccount.swift @@ -0,0 +1,497 @@ +// +// Quicksy_RegisterAccount.swift +// Monal +// +// Created by Thilo Molitor on 13.07.24. +// Copyright © 2024 monal-im.org. All rights reserved. +// + +let QUICKSY_BASE_URL = "https://api.quicksy.im"; + +func sendSMSRequest(to number:String) -> Promise<(data: Data, response: URLResponse)> { + var rq = URLRequest(url: URL(string: "\(QUICKSY_BASE_URL)/authentication/\(number)")!) + rq.httpMethod = "GET" + rq.addValue(Locale.current.languageCode ?? "en", forHTTPHeaderField: "Accept-Language") + rq.addValue(UIDevice.current.identifierForVendor?.uuidString.lowercased() ?? UUID().uuidString.lowercased(), forHTTPHeaderField: "Installation-Id") + rq.addValue("Quicksy/2.10.0", forHTTPHeaderField: "User-Agent") + DDLogDebug("Request: \(String(describing:rq))") + if let headers = rq.allHTTPHeaderFields { + for (key, value) in headers { + DDLogDebug("Header: \(key): \(value)") + } + } + return firstly { + URLSession.shared.dataTask(.promise, with: rq).validate() + } +} + +func sendRegisterRequest(number:String, pin:String, password:String) -> Promise<(data: Data, response: URLResponse)> { + var rq = URLRequest(url: URL(string: "\(QUICKSY_BASE_URL)/password")!) + rq.httpMethod = "POST" + rq.addValue(HelperTools.encodeBase64(with:"\(number)\0\(pin)"), forHTTPHeaderField: "Authorization") + rq.addValue("Quicksy/2.10.0", forHTTPHeaderField: "User-Agent") + rq.httpBody = password.data(using:.utf8) + DDLogDebug("Request: \(String(describing:rq))") + if let headers = rq.allHTTPHeaderFields { + for (key, value) in headers { + DDLogDebug("Header: \(key): \(value)") + } + } + return firstly { + URLSession.shared.dataTask(.promise, with: rq).validate() + } +} + +class Quicksy_State: ObservableObject { + @defaultsDB("Quicksy_phoneNumber") + var phoneNumber: String? + + @defaultsDB("Quicksy_countryCode") + var countryCode: String? +} + +struct Quicksy_RegisterAccount: View { + var delegate: SheetDismisserProtocol + let countries: [Quicksy_Country] = COUNTRY_CODES + @StateObject private var overlay = LoadingOverlayState() + @ObservedObject var state = Quicksy_State() + @State private var currentIndex = 0 + @State var selectedCountry: Quicksy_Country? + @State var phoneNumber: String = "" + //ios>=15 + //@FocusState var phoneNumberFocused: Bool = false + @State var showPhoneNumberCheckAlert: String? + @State var pin: String = "" + //ios>=15 + //@FocusState var pinFocused: Bool = false + @State var showErrorAlert: PMKHTTPError? + @State var showBackAlert: Bool? + + //login state + @State private var alertPrompt = AlertPrompt(dismissLabel: Text("Close")) + @State private var showAlert = false + @State var currentTimeout : DispatchTime? = nil + @State var errorObserverEnabled = false + @State var newAccountNo: NSNumber? = nil + @State var loginComplete = false + @State var isLoadingOmemoBundles = false + + init(delegate: SheetDismisserProtocol) { + self.delegate = delegate + self.state.phoneNumber = nil + } + + private func requestSMS(for number:String) { + showPhoneNumberCheckAlert = nil + showPromisingLoadingOverlay(overlay, headline:NSLocalizedString("Requesting validation SMS...", comment: ""), description: "") { + sendSMSRequest(to:number) + }.done { data, response in + DDLogDebug("Got sendSMSRequest success: \(String(describing:response))\n\(String(describing:data))") + state.phoneNumber = number + }.catch { error in + DDLogError("Catched sendSMSRequest error: \(String(describing:error))") + if let response = error as? PMKHTTPError { + showErrorAlert = response + } + } + } + + private func createAccount() { + state.countryCode = selectedCountry!.code //used to add a country code to phonebook entries not having any + let password = HelperTools.generateRandomPassword() + if let number = state.phoneNumber { + showPromisingLoadingOverlay(overlay, headline:NSLocalizedString("Registering account...", comment: ""), description: "") { + sendRegisterRequest(number:number, pin:pin, password:password) + }.done { result in + DDLogDebug("Got sendRegisterRequest success: \(String(describing:result))") + startLoginTimeout() + showLoadingOverlay(overlay, headline:NSLocalizedString("Logging in", comment: "")) + self.errorObserverEnabled = true + self.newAccountNo = MLXMPPManager.sharedInstance().login("\(number)@quicksy.im", password: password) + if(self.newAccountNo == nil) { + currentTimeout = nil // <- disable timeout on error + errorObserverEnabled = false + showLoginErrorAlert(errorMessage:NSLocalizedString("Account already configured!", comment: "")) + self.newAccountNo = nil + } + }.catch { error in + DDLogError("Catched sendRegisterRequest error: \(String(describing:error))") + if let response = error as? PMKHTTPError { + showErrorAlert = response + } + } + } + } + + private var isValidNumber: Bool { + guard let selectedCountry = selectedCountry else { + return false + } + let phonePredicate = NSPredicate(format: "SELF MATCHES %@", selectedCountry.pattern) + return phoneNumber.allSatisfy { $0.isNumber } && phoneNumber.count > 0 && phonePredicate.evaluate(with: phoneNumber) + } + + private func showTimeoutAlert() { + DDLogVerbose("Showing timeout alert...") + hideLoadingOverlay(overlay) + alertPrompt.title = Text("Timeout Error") + alertPrompt.message = Text("We were not able to connect your account. Please check your username and password and make sure you are connected to the internet.") + showAlert = true + } + + private func showSuccessAlert() { + hideLoadingOverlay(overlay) + alertPrompt.title = Text("Success!") + alertPrompt.message = Text("Quicksy is now set up and connected.") + showAlert = true + } + + private func showLoginErrorAlert(errorMessage: String) { + hideLoadingOverlay(overlay) + alertPrompt.title = Text("Error") + alertPrompt.message = Text(String(format: NSLocalizedString("We were not able to connect your account. Please check your username and password and make sure you are connected to the internet.\n\nTechnical error message: %@", comment: ""), errorMessage)) + showAlert = true + } + + private func startLoginTimeout() { + let newTimeout = DispatchTime.now() + 30.0; + self.currentTimeout = newTimeout + DispatchQueue.main.asyncAfter(deadline: newTimeout) { + if(newTimeout == self.currentTimeout) { + DDLogWarn("First login timeout triggered...") + if(self.newAccountNo != nil) { + DDLogVerbose("Removing account...") + MLXMPPManager.sharedInstance().removeAccount(forAccountNo: self.newAccountNo!) + self.newAccountNo = nil + } + self.currentTimeout = nil + showTimeoutAlert() + } + } + } + + var body: some View { + ZStack { + /// Ensure the ZStack takes the entire area + Color.clear + + if state.phoneNumber == nil { + VStack(alignment: .leading) { + Text("Verify your phone number") + .font(.title) + .fontWeight(.bold) + .foregroundColor(.primary) + .padding(.bottom, 8) + + Text("Quicksy will send an SMS message (carrier charges may apply) to verify your phone number. Enter your country code and phone number:") + + HStack { + Text("Country:") + Picker(selection: $selectedCountry, label: EmptyView()) { + ForEach(countries) { country in + Text("\(country.name) (\(country.code))").tag(country as Quicksy_Country?) + } + } + .pickerStyle(MenuPickerStyle()) + } + + HStack { + if let selectedCountry = selectedCountry { + Text(selectedCountry.code) + } + TextField("Phone Number", text: $phoneNumber) + //ios>=15 + //.focused($phoneNumberFocused) + .keyboardType(.numberPad) + .onChange(of: phoneNumber) { newValue in + let filtered = newValue.filter { "0123456789".contains($0) } + if filtered != newValue { + phoneNumber = filtered + } + } + } + .padding() + .border(phoneNumber.count==0 ? Color.gray : (isValidNumber ? Color.green : Color.red), width: phoneNumber.count==0 ? 1 : 2) + + Spacer() + + if let selectedCountry = selectedCountry { + HStack { + Spacer() + + Button(action: { + showPhoneNumberCheckAlert = selectedCountry.code + phoneNumber + }) { + Text("Next") + .fontWeight(.bold) + .padding(10) + .background(!isValidNumber ? Color(UIColor.lightGray) : Color.blue) + .foregroundColor(.white) + .cornerRadius(10) + } + .disabled(!isValidNumber) + } + } + } + .richAlert(isPresented:$showPhoneNumberCheckAlert, title:Text("Check this number?"), body:{ number in + VStack(alignment: .leading) { + Text("We will check the number **\(number)**. Is this okay or do you want to change the number?") + } + }, buttons: { number in + HStack { + Button(action: { + showPhoneNumberCheckAlert = nil + }) { + Text("Change it") + .fontWeight(.bold) + .padding(10) + .background(Color.blue) + .foregroundColor(.white) + .cornerRadius(10) + } + + Spacer() + + Button(action: { + requestSMS(for:number) + }) { + Text("OK") + .fontWeight(.bold) + .padding(10) + .background(Color.blue) + .foregroundColor(.white) + .cornerRadius(10) + } + } + }) + .onAppear { + selectedCountry = countries[0] + print("######## \(String(describing:Locale.current.regionCode))") + print("######## \(String(describing:Locale(identifier: "en_US").localizedString(forRegionCode:Locale.current.regionCode ?? "en")))") + for country in countries { + if country.name == Locale.current.localizedString(forRegionCode:Locale.current.regionCode ?? "en") || country.name == Locale(identifier: "en_US").localizedString(forRegionCode:Locale.current.regionCode ?? "en") { + selectedCountry = country + } + } + //ios>=15 + //phoneNumberFocused = true + } + } else if let number = state.phoneNumber { + VStack(alignment: .leading) { + Text("Verify your phone number") + .font(.title) + .fontWeight(.bold) + .foregroundColor(.primary) + .padding(.bottom, 8) + + Text("We sent you an SMS to \(number)") + Text("Please enter the six-digit pin below") + HStack { + TextField("Pin", text: $pin) + //ios>=15 + //.focused($phoneNumberFocused) + .keyboardType(.numberPad) + .onChange(of: pin) { newValue in + let filtered = newValue.filter { "0123456789".contains($0) } + if filtered != newValue { + pin = filtered + } + } + } + .padding() + .border(pin.count==0 ? Color.gray : (pin.count==6 ? Color.green : Color.red), width: pin.count==0 ? 1 : 2) + + Spacer().frame(height:16) + + Button(action: { + requestSMS(for:number) + }) { + Text("Send SMS again") + } + .frame(maxWidth: .infinity, alignment: .center).padding() + + Spacer() + + HStack { + Button(action: { + showBackAlert = true + }) { + Text("Previous") + .fontWeight(.bold) + .padding(10) + .background(Color.blue) + .foregroundColor(.white) + .cornerRadius(10) + } + + Spacer() + + Button(action: { + createAccount() + }) { + Text("Next") + .fontWeight(.bold) + .padding(10) + .background(Color.blue) + .foregroundColor(.white) + .cornerRadius(10) + } + } + } + .richAlert(isPresented:$showBackAlert, title:Text("Cancel?")) { error in + VStack(alignment: .leading) { + Text("Are you sure to cancel the registration process?") + } + } buttons: { error in + HStack { + Button(action: { + showBackAlert = nil + }) { + Text("No") + .fontWeight(.bold) + .padding(10) + .background(Color.blue) + .foregroundColor(.white) + .cornerRadius(10) + } + + Spacer() + + Button(action: { + showBackAlert = nil + state.phoneNumber = nil + }) { + Text("Yes") + .fontWeight(.bold) + .padding(10) + .background(Color.blue) + .foregroundColor(.white) + .cornerRadius(10) + } + } + } + .onAppear { + //ios>=15 + //pinFocused = true + } + } + } + .alert(isPresented: $showAlert) { + Alert(title: alertPrompt.title, message: alertPrompt.message, dismissButton: .default(alertPrompt.dismissLabel, action: { + if(self.loginComplete == true) { + self.delegate.dismissWithoutAnimation() + } + })) + } + .richAlert(isPresented:$showErrorAlert, title:Text("Error requesting SMS!"), body:{ error in + VStack(alignment: .leading) { + Text("An error happened when trying to request the SMS:") + .bold() + Spacer().frame(height:16) + switch error { + case .badStatusCode(let code, _, let response): + switch code { + case 400: + Text("Invalid user input.") + case 401: + Text("The pin you have entered is incorrect.") + case 403: + Text("You are using an out of date version of this app.") + case 404: + Text("The pin we have sent you has expired.") + case 409: + Text("This phone number is currently logged in with another device.") + case 429: + Text("Too many attempts, please try again in \(HelperTools.string(fromTimeInterval:UInt(response.value(forHTTPHeaderField:"Retry-After") ?? "0") ?? 0)).") + case 500: + Text("Something went wrong processing your request.") + case 501: + Text("Temporarily unavailable. Try again later.") + case 502: + Text("Temporarily unavailable. Try again later.") + case 503: + Text("Temporarily unavailable. Try again later.") + default: + Text("Unexpected error processing your request.") + } + } + } + }, buttons: { error in + HStack { + Spacer() + + Button(action: { + showErrorAlert = nil + }) { + Text("OK") + .fontWeight(.bold) + .padding(10) + .background(Color.blue) + .foregroundColor(.white) + .cornerRadius(10) + } + } + }) + .padding() + .addLoadingOverlay(overlay) + .onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("kXMPPError")).receive(on: RunLoop.main)) { notification in + if(self.errorObserverEnabled == false) { + return + } + if let xmppAccount = notification.object as? xmpp, let newAccountNo : NSNumber = self.newAccountNo, let errorMessage = notification.userInfo?["message"] as? String { + if(xmppAccount.accountNo.intValue == newAccountNo.intValue) { + DispatchQueue.main.async { + currentTimeout = nil // <- disable timeout on error + errorObserverEnabled = false + showLoginErrorAlert(errorMessage: errorMessage) + MLXMPPManager.sharedInstance().removeAccount(forAccountNo: newAccountNo) + self.newAccountNo = nil + } + } + } + } + .onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("kMLResourceBoundNotice")).receive(on: RunLoop.main)) { notification in + if let xmppAccount = notification.object as? xmpp, let newAccountNo : NSNumber = self.newAccountNo { + if(xmppAccount.accountNo.intValue == newAccountNo.intValue) { + DispatchQueue.main.async { + currentTimeout = nil // <- disable timeout on successful connection + self.errorObserverEnabled = false + showLoadingOverlay(overlay, headline:NSLocalizedString("Loading contact list", comment: "")) + } + } + } + } + .onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("kMonalUpdateBundleFetchStatus")).receive(on: RunLoop.main)) { notification in + 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 + showLoadingOverlay( + overlay, + headline:NSLocalizedString("Loading omemo bundles", comment: ""), + description:String(format: NSLocalizedString("Loading omemo bundles: %@ / %@", comment: ""), completed, all) + ) + } + } + } + .onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("kMonalFinishedOmemoBundleFetch")).receive(on: RunLoop.main)) { notification in + if let notificationAccountNo = notification.userInfo?["accountNo"] as? NSNumber, let newAccountNo : NSNumber = self.newAccountNo { + if(notificationAccountNo.intValue == newAccountNo.intValue && isLoadingOmemoBundles) { + DispatchQueue.main.async { + self.loginComplete = true + showSuccessAlert() + } + } + } + } + .onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("kMonalFinishedCatchup")).receive(on: RunLoop.main)) { notification in + if let xmppAccount = notification.object as? xmpp, let newAccountNo : NSNumber = self.newAccountNo { + if(xmppAccount.accountNo.intValue == newAccountNo.intValue && !isLoadingOmemoBundles) { + DispatchQueue.main.async { + self.loginComplete = true + showSuccessAlert() + } + } + } + } + } +} 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/RichAlert.swift b/Monal/Classes/RichAlert.swift index d03e90d9f4..bd1f7afa29 100644 --- a/Monal/Classes/RichAlert.swift +++ b/Monal/Classes/RichAlert.swift @@ -103,6 +103,11 @@ extension View { func richAlert(isPresented: Binding, title: @autoclosure @escaping () -> some View, @ViewBuilder body: @escaping (_ data: T) -> some View, @ViewBuilder buttons: @escaping (_ data: T) -> some View) -> some View { modifier(RichAlertView(isPresented:isPresented, alertTitle:{ _ in title() }, alertBody:body, alertButtons:buttons)) } + //apparently this is sometimes somehow needed to not confuse the compiler into using some of the other functions instead of this + //(it tries to use the title(), body(), buttons(X) variant in Quicksy_RegisterAccount) + func richAlertX(isPresented: Binding, title: @autoclosure @escaping () -> some View, @ViewBuilder body: @escaping (_ data: T) -> some View, @ViewBuilder buttons: @escaping (_ data: T) -> some View) -> some View { + modifier(RichAlertView(isPresented:isPresented, alertTitle:{ _ in title() }, alertBody:body, alertButtons:buttons)) + } //title(), body(X), buttons(X) func richAlert(isPresented: Binding, @ViewBuilder title: @escaping () -> some View, @ViewBuilder body: @escaping (_ data: T) -> some View, @ViewBuilder buttons: @escaping (_ data: T) -> some View) -> some View { modifier(RichAlertView(isPresented:isPresented, alertTitle:{ _ in title() }, alertBody:body, alertButtons:buttons)) diff --git a/Monal/Classes/SwiftHelpers.swift b/Monal/Classes/SwiftHelpers.swift index 0ad13c020c..bd1e974f73 100644 --- a/Monal/Classes/SwiftHelpers.swift +++ b/Monal/Classes/SwiftHelpers.swift @@ -31,6 +31,7 @@ let BGFETCH_DEFAULT_INTERVAL = HelperTools.getObjcDefinedValue(.BGFETCH_DEFAULT_ public typealias monal_void_block_t = @convention(block) () -> Void; public typealias monal_id_block_t = @convention(block) (AnyObject?) -> Void; +public typealias monal_timer_block_t = @convention(block) (MLDelayableTimer?) -> Void; //see https://stackoverflow.com/a/40629365/3528174 extension String: Error {} @@ -220,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 @@ -242,7 +279,18 @@ public struct defaultsDB { ]) } } - set { container.set(newValue, forKey: key) } + set { + 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() + } } public static subscript( @@ -262,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 { diff --git a/Monal/Classes/SwiftuiHelpers.swift b/Monal/Classes/SwiftuiHelpers.swift index 1afde7b542..8d2f659f07 100644 --- a/Monal/Classes/SwiftuiHelpers.swift +++ b/Monal/Classes/SwiftuiHelpers.swift @@ -17,6 +17,7 @@ import PhotosUI import Combine import FLAnimatedImage import OrderedCollections +import CropViewController extension MLContact : Identifiable {} //make MLContact be usable in swiftui ForEach clauses @@ -87,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 { @@ -103,7 +103,12 @@ func performMucAction(account: xmpp, mucJid: String, overlay: LoadingOverlayStat } } }, forMuc:mucJid) - action() + do { + try action() + } catch { + seal.reject(error) + } + } } } @@ -221,6 +226,68 @@ func buildNotificationStateLabel(_ description: Text, isWorking: Bool) -> some V } } +//see https://github.com/CH3COOH/TOCropViewController/blob/issue/421/Swift/CropViewControllerSwiftUIExample/ImageCropView.swift +public struct ImageCropView: UIViewControllerRepresentable { + private let configureBlock: (CropViewController) -> Void + private let originalImage: UIImage + private let onCanceled: () -> Void + private let onImageCropped: (UIImage,CGRect,Int) -> Void + + @Environment(\.presentationMode) private var presentationMode + + public init(originalImage: UIImage, configureBlock: @escaping (CropViewController) -> Void, onCanceled: @escaping () -> Void, success onImageCropped: @escaping (UIImage,CGRect,Int) -> Void) { + self.originalImage = originalImage + self.configureBlock = configureBlock + self.onCanceled = onCanceled + self.onImageCropped = onImageCropped + } + + public func makeUIViewController(context: Context) -> CropViewController { + let cropController = CropViewController(image: originalImage) + cropController.delegate = context.coordinator + configureBlock(cropController) + return cropController + } + + public func updateUIViewController(_ uiViewController: CropViewController, context: Context) { + } + + public func makeCoordinator() -> Coordinator { + Coordinator( + onDismiss: { self.presentationMode.wrappedValue.dismiss() }, + onCanceled: self.onCanceled, + onImageCropped: self.onImageCropped + ) + } + + final public class Coordinator: NSObject, CropViewControllerDelegate { + private let onDismiss: () -> Void + private let onImageCropped: (UIImage,CGRect,Int) -> Void + private let onCanceled: () -> Void + + init(onDismiss: @escaping () -> Void, onCanceled: @escaping () -> Void, onImageCropped: @escaping (UIImage,CGRect,Int) -> Void) { + self.onDismiss = onDismiss + self.onImageCropped = onImageCropped + self.onCanceled = onCanceled + } + + public func cropViewController(_ cropViewController: CropViewController, didCropToImage image: UIImage, withRect cropRect: CGRect, angle: Int) { + self.onImageCropped(image, cropRect, angle) + self.onDismiss() + } + + public func cropViewController(_ cropViewController: CropViewController, didCropToCircularImage image: UIImage, withRect cropRect: CGRect, angle: Int) { + self.onImageCropped(image, cropRect, angle) + self.onDismiss() + } + + public func cropViewController(_ cropViewController: CropViewController, didFinishCancelled cancelled: Bool) { + self.onCanceled() + self.onDismiss() + } + } +} + //see here for some ideas used herein: https://blog.logrocket.com/adding-gifs-ios-app-flanimatedimage-swiftui/#using-flanimatedimage-with-swift struct GIFViewer: UIViewRepresentable { typealias UIViewType = FLAnimatedImageView @@ -230,16 +297,6 @@ struct GIFViewer: UIViewRepresentable { let imageView = FLAnimatedImageView(frame:.zero) let animatedImage = FLAnimatedImage(animatedGIFData:data) imageView.animatedImage = animatedImage - //imageView.translatesAutoresizingMaskIntoConstraints = false - //imageView.contentMode = .scaleAspectFit - //imageView.frame = CGRect(x: 0, y: 0, width: 100, height: 100) - -// imageView.translatesAutoresizingMaskIntoConstraints = false -// imageView.layer.cornerRadius = 24 -// imageView.layer.masksToBounds = true -// imageView.setContentHuggingPriority(.required, for: .vertical) -// imageView.setContentHuggingPriority(.required, for: .horizontal) - return imageView } @@ -460,10 +517,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() @@ -525,6 +578,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 { @@ -533,7 +621,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 } @@ -566,9 +654,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 { @@ -582,7 +668,11 @@ class SwiftuiInterface : NSObject { let delegate = SheetDismisserProtocol() let host = UIHostingController(rootView:AnyView(EmptyView())) delegate.host = host +#if IS_QUICKSY + host.rootView = AnyView(Quicksy_RegisterAccount(delegate:delegate)) +#else host.rootView = AnyView(AddTopLevelNavigation(withDelegate:delegate, to:RegisterAccount(delegate:delegate, registerData:registerData))) +#endif return host } @@ -595,7 +685,7 @@ class SwiftuiInterface : NSObject { return host } - @objc + @objc(makeAddContactViewWithDismisser:) func makeAddContactView(dismisser: @escaping (MLContact) -> ()) -> UIViewController { let delegate = SheetDismisserProtocol() let host = UIHostingController(rootView:AnyView(EmptyView())) @@ -616,30 +706,32 @@ 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))) - case "ContactRequests": - host.rootView = AnyView(AddTopLevelNavigation(withDelegate: delegate, to: ContactRequestsMenu(delegate: delegate))) + host = UIHostingController(rootView:AnyView(UIKitWorkaround(WelcomeLogIn(delegate:delegate)))) 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())) - case "ActiveChatsPrivacySettings": - host.rootView = AnyView(AddTopLevelNavigation(withDelegate: delegate, to: PrivacySettings())) - case "ActiveChatsNotificatioSettings": - host.rootView = AnyView(AddTopLevelNavigation(withDelegate: delegate, to: NotificationSettings())) + host = UIHostingController(rootView:AnyView(UIKitWorkaround(GeneralSettings()))) + case "ActiveChatsGeneralSettings": + host = UIHostingController(rootView:AnyView(AddTopLevelNavigation(withDelegate: delegate, to: GeneralSettings()))) + case "ActiveChatsNotificationSettings": + host = UIHostingController(rootView:AnyView(AddTopLevelNavigation(withDelegate: delegate, to: NotificationSettings()))) + case "OnboardingView": + host = UIHostingController(rootView:AnyView(createOnboardingView(delegate:delegate))) + default: unreachable() } - return host + delegate.host = host! + return host! } } diff --git a/Monal/Classes/WelcomeLogIn.swift b/Monal/Classes/WelcomeLogIn.swift index d1c277a944..4aaaed40db 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) @@ -274,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/XMPPEdit.m b/Monal/Classes/XMPPEdit.m index 308a4d86aa..2fff9c5c26 100644 --- a/Monal/Classes/XMPPEdit.m +++ b/Monal/Classes/XMPPEdit.m @@ -12,7 +12,6 @@ #import "MLBlockedUsersTableViewController.h" #import "MLButtonCell.h" #import "MLImageManager.h" -#import "MLMAMPrefTableViewController.h" #import "MLPasswordChangeTableViewController.h" #import "MLServerDetails.h" #import "MLSwitchCell.h" @@ -49,7 +48,6 @@ enum kSettingsGeneralRows { SettingsChangePasswordRow, SettingsOmemoKeysRow, - SettingsMAMPreferencesRow, SettingsBlockedUsersRow, SettingsGeneralRowsCnt }; @@ -655,10 +653,6 @@ -(UITableViewCell*) tableView:(UITableView*) tableView cellForRowAtIndexPath:(NS [thecell initTapCell:NSLocalizedString(@"Encryption Keys (OMEMO)", @"")]; break; } - case SettingsMAMPreferencesRow: { - [thecell initTapCell:NSLocalizedString(@"Message Archive Preferences", @"")]; - break; - } case SettingsBlockedUsersRow: { [thecell initTapCell:NSLocalizedString(@"Blocked Users", @"")]; break; @@ -848,9 +842,6 @@ -(void) tableView:(UITableView*) tableView didSelectRowAtIndexPath:(NSIndexPath* [self showDetailViewController:ownOmemoKeysView sender:self]; break; } - case SettingsMAMPreferencesRow: - [self performSegueWithIdentifier:@"showMAMPref" sender:self]; - break; case SettingsBlockedUsersRow: [self performSegueWithIdentifier:@"showBlockedUsers" sender:self]; break; @@ -900,11 +891,6 @@ -(void) prepareForSegue:(UIStoryboardSegue*) segue sender:(id) sender MLServerDetails* server= (MLServerDetails*)segue.destinationViewController; server.xmppAccount = [[MLXMPPManager sharedInstance] getConnectedAccountForID:self.accountNo]; } - else if([segue.identifier isEqualToString:@"showMAMPref"]) - { - MLMAMPrefTableViewController* mam = (MLMAMPrefTableViewController*)segue.destinationViewController; - mam.xmppAccount = [[MLXMPPManager sharedInstance] getConnectedAccountForID:self.accountNo]; - } else if([segue.identifier isEqualToString:@"showBlockedUsers"]) { xmpp* xmppAccount = [[MLXMPPManager sharedInstance] getConnectedAccountForID:self.accountNo]; diff --git a/Monal/Classes/XMPPIQ.h b/Monal/Classes/XMPPIQ.h index 3b7141b0c0..569ff59912 100644 --- a/Monal/Classes/XMPPIQ.h +++ b/Monal/Classes/XMPPIQ.h @@ -136,6 +136,10 @@ removes a contact from the roster -(void) setGetRoomConfig; -(void) setRoomConfig:(XMPPDataForm*) configForm; +#ifdef IS_QUICKSY +-(void) setQuicksyPhoneBook:(NSArray*) numbers; +#endif + @end NS_ASSUME_NONNULL_END diff --git a/Monal/Classes/XMPPIQ.m b/Monal/Classes/XMPPIQ.m index 9643747a7a..09a1845448 100644 --- a/Monal/Classes/XMPPIQ.m +++ b/Monal/Classes/XMPPIQ.m @@ -449,4 +449,18 @@ -(void) setMucAdminQueryWithAffiliation:(NSString*) affiliation forJid:(NSString ] andData:nil]]; } +#ifdef IS_QUICKSY +-(void) setQuicksyPhoneBook:(NSArray*) numbers +{ + MLXMLNode* envelope = [[MLXMLNode alloc] initWithElement:@"phone-book" andNamespace:@"im.quicksy.synchronization:0"]; + for(NSString* number in numbers) + { + [envelope addChildNode:[[MLXMLNode alloc] initWithElement:@"entry" withAttributes:@{ + @"number": number, + } andChildren:@[] andData:nil]]; + } + [self addChildNode:envelope]; +} +#endif + @end diff --git a/Monal/Classes/XMPPMessage.h b/Monal/Classes/XMPPMessage.h index 7691e168f1..dc9b80a6b8 100644 --- a/Monal/Classes/XMPPMessage.h +++ b/Monal/Classes/XMPPMessage.h @@ -39,7 +39,6 @@ FOUNDATION_EXPORT NSString* const kMessageHeadlineType; sets the receipt child element */ -(void) setReceipt:(NSString*) messageId; --(void) setChatmarkerReceipt:(NSString*) messageId; -(void) setDisplayed:(NSString*) messageId; -(void) setMDSDisplayed:(NSString*) stanzaId withStanzaIdBy:(NSString*) by; diff --git a/Monal/Classes/XMPPMessage.m b/Monal/Classes/XMPPMessage.m index eee696fc36..a91f8e85a3 100644 --- a/Monal/Classes/XMPPMessage.m +++ b/Monal/Classes/XMPPMessage.m @@ -108,11 +108,6 @@ -(void) setReceipt:(NSString*) messageId [self addChildNode:[[MLXMLNode alloc] initWithElement:@"received" andNamespace:@"urn:xmpp:receipts" withAttributes:@{@"id":messageId} andChildren:@[] andData:nil]]; } --(void) setChatmarkerReceipt:(NSString*) messageId -{ - [self addChildNode:[[MLXMLNode alloc] initWithElement:@"received" andNamespace:@"urn:xmpp:chat-markers:0" withAttributes:@{@"id":messageId} andChildren:@[] andData:nil]]; -} - -(void) setDisplayed:(NSString*) messageId { [self addChildNode:[[MLXMLNode alloc] initWithElement:@"displayed" andNamespace:@"urn:xmpp:chat-markers:0" withAttributes:@{@"id":messageId} andChildren:@[] andData:nil]]; diff --git a/Monal/Classes/chatViewController.m b/Monal/Classes/chatViewController.m index 6f5ee37c08..e61a94ac3b 100644 --- a/Monal/Classes/chatViewController.m +++ b/Monal/Classes/chatViewController.m @@ -103,7 +103,8 @@ @interface chatViewController() context) { - } completion:^(id context) { - //[self lastMsgButtonPositionConfigWithSize:self.inputContainerView.bounds.size]; - [self lastMsgButtonPositionConfigWithSize:size]; - }]; } #pragma mark gestures @@ -2545,7 +2559,7 @@ -(UISwipeActionsConfiguration*) tableView:(UITableView*) tableView trailingSwipe if(!message.inbound) { [self.xmppAccount retractMessage:message]; - [[DataLayer sharedInstance] deleteMessageHistory:message.messageDBId]; + [[DataLayer sharedInstance] retractMessageHistory:message.messageDBId]; [message updateWithMessage:[[[DataLayer sharedInstance] messagesForHistoryIDs:@[message.messageDBId]] firstObject]]; //update table entry @@ -2911,7 +2925,7 @@ -(void) commandIPressed:(UIKeyCommand*)keyCommand // Open search ViewController -(void) commandFPressed:(UIKeyCommand*)keyCommand { - [self showSeachButtonAction]; + //[self showSeachButtonAction]; } // List of custom hardware key commands 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 2f9a58563b..fb245b58fc 100644 --- a/Monal/Classes/xmpp.m +++ b/Monal/Classes/xmpp.m @@ -94,9 +94,9 @@ @interface xmpp() NSDate* _lastInteractionDate; //internal handlers and flags - monal_void_block_t _cancelLoginTimer; - monal_void_block_t _cancelPingTimer; - monal_void_block_t _cancelReconnectTimer; + MLDelayableTimer* _loginTimer; + MLDelayableTimer* _pingTimer; + MLDelayableTimer* _reconnectTimer; NSMutableArray* _timersToCancelOnDisconnect; NSMutableArray* _smacksAckHandler; NSMutableDictionary* _iqHandlers; @@ -221,10 +221,6 @@ -(id) initWithServer:(nonnull MLXMPPServer*) server andIdentity:(nonnull MLXMPPI //we support mds [self.pubsub registerForNode:@"urn:xmpp:mds:displayed:0" withHandler:$newHandler(MLPubSubProcessor, mdsHandler)]; - //autodelete messages old enough (first invocation) - if([[HelperTools defaultsDB] boolForKey:@"AutodeleteAllMessagesAfter3Days"]) - [[DataLayer sharedInstance] autodeleteAllMessagesAfter3Days]; - return self; } @@ -498,7 +494,7 @@ -(BOOL) idle ) || ( //test if we are connected and idle (e.g. we're done with catchup and neither process any incoming stanzas nor trying to send anything) _catchupDone && - _cancelPingTimer == nil && + _pingTimer == nil && !unackedCount && ![_parseQueue operationCount] && //if something blocks the parse queue it is either an incoming stanza currently processed or waiting to be processed //[_receiveQueue operationCount] <= ([NSOperationQueue currentQueue]==_receiveQueue ? 1 : 0) && @@ -512,7 +508,7 @@ -(BOOL) idle "\t_accountState < kStateReconnecting = %@\n" "\t_reconnectInProgress = %@\n" "\t_catchupDone = %@\n" - "\t_cancelPingTimer = %@\n" + "\t_pingTimer = %@\n" "\t[self.unAckedStanzas count] = %lu\n" "\t[_parseQueue operationCount] = %lu\n" //"\t[_receiveQueue operationCount] = %lu\n" @@ -523,7 +519,7 @@ -(BOOL) idle bool2str(_accountState < kStateReconnecting), bool2str(_reconnectInProgress), bool2str(_catchupDone), - _cancelPingTimer == nil ? @"none" : @"running timer", + _pingTimer == nil ? @"none" : @"running timer", unackedCount, (unsigned long)[_parseQueue operationCount], //(unsigned long)[_receiveQueue operationCount], @@ -721,33 +717,47 @@ -(BOOL) parseQueueFrozen -(void) freezeParseQueue { - //don't do this in a block on the parse queue because the parse queue could potentially have a significant amount of blocks waiting - //to be synchronously dispatched to the receive queue and processed and we don't want to wait for all these stanzas to be processed - //and rather freeze the parse queue as soon as possible - _parseQueue.suspended = YES; - - //apparently setting _parseQueue.suspended = YES does return before the queue is actually suspended - //--> busy wait for _parseQueue.suspended == YES - [HelperTools busyWaitForOperationQueue:_parseQueue]; - MLAssert([self parseQueueFrozen] == YES, @"Parse queue not frozen after setting suspended to YES!"); - - //this has to be synchronous because we want to be sure no further stanzas are leaking from the parse queue - //into the receive queue once we leave this method - //--> wait for all blocks put into the receive queue by the parse queue right before it was frozen - [self dispatchOnReceiveQueue: ^{ - [HelperTools busyWaitForOperationQueue:self->_parseQueue]; - MLAssert([self parseQueueFrozen] == YES, @"Parse queue not frozen after setting suspended to YES (in receive queue)!"); - DDLogInfo(@"Parse queue is frozen now!"); - }]; + @synchronized(_parseQueue) { + //pause all timers before freezing the parse queue to not trigger timers that can not be handeld properly while frozen + [_loginTimer pause]; + [_pingTimer pause]; + [_reconnectTimer pause]; + + //don't do this in a block on the parse queue because the parse queue could potentially have a significant amount of blocks waiting + //to be synchronously dispatched to the receive queue and processed and we don't want to wait for all these stanzas to be processed + //and rather freeze the parse queue as soon as possible + _parseQueue.suspended = YES; + + //apparently setting _parseQueue.suspended = YES does return before the queue is actually suspended + //--> busy wait for _parseQueue.suspended == YES + [HelperTools busyWaitForOperationQueue:_parseQueue]; + MLAssert([self parseQueueFrozen] == YES, @"Parse queue not frozen after setting suspended to YES!"); + + //this has to be synchronous because we want to be sure no further stanzas are leaking from the parse queue + //into the receive queue once we leave this method + //--> wait for all blocks put into the receive queue by the parse queue right before it was frozen + [self dispatchOnReceiveQueue: ^{ + [HelperTools busyWaitForOperationQueue:self->_parseQueue]; + MLAssert([self parseQueueFrozen] == YES, @"Parse queue not frozen after setting suspended to YES (in receive queue)!"); + DDLogInfo(@"Parse queue is frozen now!"); + }]; + } } -(void) unfreezeParseQueue { - //this has to be synchronous because we want to be sure the parse queue is operating again once we leave this method - [self dispatchOnReceiveQueue: ^{ - self->_parseQueue.suspended = NO; - DDLogInfo(@"Parse queue is UNfrozen now!"); - }]; + @synchronized(_parseQueue) { + //this has to be synchronous because we want to be sure the parse queue is operating again once we leave this method + [self dispatchOnReceiveQueue: ^{ + self->_parseQueue.suspended = NO; + DDLogInfo(@"Parse queue is UNfrozen now!"); + }]; + + //resume all timers paused when freezing the parse queue + [_loginTimer resume]; + [_pingTimer resume]; + [_reconnectTimer resume]; + } } -(void) freezeSendQueue @@ -847,12 +857,12 @@ -(void) reinitLoginTimer return; //cancel old timer if existing and... - if(self->_cancelLoginTimer != nil) - self->_cancelLoginTimer(); + if(self->_loginTimer != nil) + [self->_loginTimer cancel]; //...replace it with new timer - self->_cancelLoginTimer = createTimer(CONNECT_TIMEOUT, (^{ + self->_loginTimer = createDelayableTimer(CONNECT_TIMEOUT, (^{ + self->_loginTimer = nil; [self dispatchAsyncOnReceiveQueue: ^{ - self->_cancelLoginTimer = nil; DDLogInfo(@"Login took too long, cancelling and trying to reconnect (potentially using another SRV record)"); [self reconnect]; }]; @@ -861,17 +871,19 @@ -(void) reinitLoginTimer -(void) connect { - //autodelete messages old enough (second invocation) - if([[HelperTools defaultsDB] boolForKey:@"AutodeleteAllMessagesAfter3Days"]) - [[DataLayer sharedInstance] autodeleteAllMessagesAfter3Days]; - - if(_parseQueue.suspended) + if([self parseQueueFrozen]) { DDLogWarn(@"Not trying to connect: parse queue frozen!"); return; } [self dispatchAsyncOnReceiveQueue: ^{ + if([self parseQueueFrozen]) + { + DDLogWarn(@"Not trying to connect: parse queue frozen!"); + return; + } + [self->_parseQueue cancelAllOperations]; //throw away all parsed but not processed stanzas from old connections [self unfreezeParseQueue]; //make sure the parse queue is operational again //we don't want to loose outgoing messages by throwing away their receiveQueue operation adding them to the smacks queue etc. @@ -961,15 +973,15 @@ -(void) disconnectWithStreamError:(MLXMLNode* _Nullable) streamError andExplicit //this has to be synchronous because we want to wait for the disconnect to complete before continuingand unlocking the process in the NSE [self dispatchOnReceiveQueue: ^{ DDLogInfo(@"stopping running timers"); - if(self->_cancelLoginTimer) - self->_cancelLoginTimer(); //cancel running login timer - self->_cancelLoginTimer = nil; - if(self->_cancelPingTimer) - self->_cancelPingTimer(); //cancel running ping timer - self->_cancelPingTimer = nil; - if(self->_cancelReconnectTimer) - self->_cancelReconnectTimer(); - self->_cancelReconnectTimer = nil; + if(self->_loginTimer) + [self->_loginTimer cancel]; //cancel running login timer + self->_loginTimer = nil; + if(self->_pingTimer) + [self->_pingTimer cancel]; //cancel running ping timer + self->_pingTimer = nil; + if(self->_reconnectTimer) + [self->_reconnectTimer cancel]; //cancel running reconnect timer + self->_reconnectTimer = nil; @synchronized(self->_timersToCancelOnDisconnect) { for(monal_void_block_t timer in self->_timersToCancelOnDisconnect) timer(); @@ -1248,8 +1260,8 @@ -(void) reconnectWithStreamError:(MLXMLNode* _Nullable) streamError andWaitingTi [self disconnectWithStreamError:streamError andExplicitLogout:NO]; DDLogInfo(@"Trying to connect again in %G seconds...", wait); - self->_cancelReconnectTimer = createTimer(wait, (^{ - self->_cancelReconnectTimer = nil; + self->_reconnectTimer = createDelayableTimer(wait, (^{ + self->_reconnectTimer = nil; [self dispatchAsyncOnReceiveQueue: ^{ //there may be another connect/login operation in progress triggered from reachability or another timer if(self.accountState_cancelReconnectTimer = nil; + self->_reconnectTimer = nil; [self dispatchAsyncOnReceiveQueue: ^{ self->_reconnectInProgress = NO; }]; @@ -1475,16 +1487,16 @@ -(void) sendPing:(double) timeout DDLogInfo(@"ping attempted before logged in and bound, ignoring ping."); return; } - else if(self->_cancelPingTimer) + else if(self->_pingTimer) { DDLogInfo(@"ping already sent, ignoring second ping request."); return; } else if([self->_parseQueue operationCount] > 4) { - DDLogWarn(@"parseQueue overflow, delaying ping by 10 seconds."); + DDLogWarn(@"parseQueue overflow, delaying ping by 4 seconds."); @synchronized(self->_timersToCancelOnDisconnect) { - [self->_timersToCancelOnDisconnect addObject:createTimer(10.0, (^{ + [self->_timersToCancelOnDisconnect addObject:createTimer(4.0, (^{ DDLogDebug(@"ping delay expired, retrying ping."); [self sendPing:timeout]; }))]; @@ -1493,9 +1505,9 @@ -(void) sendPing:(double) timeout else { //start ping timer - self->_cancelPingTimer = createTimer(timeout, (^{ + self->_pingTimer = createDelayableTimer(timeout, (^{ + self->_pingTimer = nil; [self dispatchAsyncOnReceiveQueue: ^{ - self->_cancelPingTimer = nil; //check if someone already called reconnect or disconnect while we were waiting for the ping //(which was called while we still were >= kStateBound) if(self.accountState_cancelPingTimer) + if(self->_pingTimer) { - self->_cancelPingTimer(); //cancel timer (ping was successful) - self->_cancelPingTimer = nil; + [self->_pingTimer cancel]; //cancel timer (ping was successful) + self->_pingTimer = nil; } }; @@ -1534,8 +1546,7 @@ -(void) sendPing:(double) timeout [self sendIq:ping withResponseHandler:^(XMPPIQ* result __unused) { handler(); } andErrorHandler:^(XMPPIQ* error) { - if(error != nil) - handler(); + handler(); }]; } } @@ -1781,7 +1792,7 @@ -(void) processInput:(MLXMLNode*) parsedStanza withDelayedReplay:(BOOL) delayedR self->_catchupStanzaCounter++; //restart logintimer for every incoming stanza when not logged in (don't do anything without a running timer) - if(!delayedReplay && _cancelLoginTimer != nil && self->_accountState < kStateLoggedIn) + if(!delayedReplay && _loginTimer != nil && self->_accountState < kStateLoggedIn) [self reinitLoginTimer]; //only process most stanzas/nonzas after having a secure context @@ -2450,10 +2461,10 @@ -(void) processInput:(MLXMLNode*) parsedStanza withDelayedReplay:(BOOL) delayedR [[MLNotificationQueue currentQueue] postNotificationName:kMLIsLoggedInNotice object:self]; _usableServersList = [NSMutableArray new]; //reset list to start again with the highest SRV priority on next connect - if(_cancelLoginTimer) + if(_loginTimer) { - _cancelLoginTimer(); //we are now logged in --> cancel running login timer - _cancelLoginTimer = nil; + [self->_loginTimer cancel]; //we are now logged in --> cancel running login timer + _loginTimer = nil; } self->_loggedInOnce = YES; @@ -2661,10 +2672,10 @@ -(void) processInput:(MLXMLNode*) parsedStanza withDelayedReplay:(BOOL) delayedR self->_blockToCallOnTCPOpen = nil; //just to be sure but not strictly necessary self->_accountState = kStateLoggedIn; _usableServersList = [NSMutableArray new]; //reset list to start again with the highest SRV priority on next connect - if(_cancelLoginTimer) + if(_loginTimer) { - _cancelLoginTimer(); //we are now logged in --> cancel running login timer - _cancelLoginTimer = nil; + [self->_loginTimer cancel]; //we are now logged in --> cancel running login timer + _loginTimer = nil; } self->_loggedInOnce = YES; @@ -3208,6 +3219,17 @@ -(NSString* _Nullable) channelBindingToUse #pragma mark stanza handling +// -(AnyPromise*) sendIq:(XMPPIQ*) iq +// { +// return [AnyPromise promiseWithResolverBlock:^(PMKResolver resolve) { +// [self sendIq:iq withResponseHandler:^(XMPPIQ* response) { +// resolve(response); +// } andErrorHandler:^(XMPPIQ* error) { +// resolve(error); +// }]; +// }]; +// } + -(void) sendIq:(XMPPIQ*) iq withResponseHandler:(monal_iq_handler_t) resultHandler andErrorHandler:(monal_iq_handler_t) errorHandler { if(resultHandler || errorHandler) @@ -3219,14 +3241,19 @@ -(void) sendIq:(XMPPIQ*) iq withResponseHandler:(monal_iq_handler_t) resultHandl -(void) sendIq:(XMPPIQ*) iq withHandler:(MLHandler*) handler { - if(handler) - { - DDLogVerbose(@"Adding %@ to iqHandlers...", handler); - @synchronized(_iqHandlers) { - _iqHandlers[iq.id] = [@{@"iq":iq, @"timeout":@(IQ_TIMEOUT), @"handler":handler} mutableCopy]; + //serialize this state update with other receive queue updates + //not doing this will make it race with a readState call in the receive queue before the write of this update can happen, + //which will remove this entry from state and the iq answer received later on be discarded + [self dispatchAsyncOnReceiveQueue:^{ + if(handler) + { + DDLogVerbose(@"Adding %@ to iqHandlers...", handler); + @synchronized(self->_iqHandlers) { + self->_iqHandlers[iq.id] = [@{@"iq":iq, @"timeout":@(IQ_TIMEOUT), @"handler":handler} mutableCopy]; + } } - } - [self send:iq]; //this will also call persistState --> we don't need to do this here explicitly (to make sure our iq delegate is stored to db) + [self send:iq]; //this will also call persistState --> we don't need to do this here explicitly (to make sure our iq delegate is stored to db) + }]; } -(void) send:(MLXMLNode*) stanza @@ -3310,7 +3337,7 @@ -(void) logStanza:(MLXMLNode*) stanza withPrefix:(NSString*) prefix -(void) retractMessage:(MLMessage*) msg { MLAssert([msg.accountId isEqual:self.accountNo], @"Can not retract message from one account on another account!", (@{@"self.accountNo": self.accountNo, @"msg": msg})); - XMPPMessage* messageNode = [[XMPPMessage alloc] initWithType:kMessageChatType to:msg.buddyName]; + XMPPMessage* messageNode = [[XMPPMessage alloc] initWithType:msg.isMuc ? kMessageGroupChatType : kMessageChatType to:msg.buddyName]; DDLogVerbose(@"Retracting message: %@", msg); //retraction @@ -4026,6 +4053,7 @@ -(void) initSession [self queryDisco]; [self queryServerVersion]; [self purgeOfflineStorage]; + [self setMAMPrefs:@"always"]; //make sure we are able to do proper catchups [self sendPresence]; //this will trigger a replay of offline stanzas on prosody (no XEP-0013 support anymore 😡) //the offline messages will come in *after* we initialized the mam query, because the disco result comes in first //(and this is what triggers mam catchup) @@ -4418,28 +4446,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 +-(AnyPromise*) checkJidType:(NSString*) jid { - 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); + 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}]); + }]; }]; } @@ -4683,7 +4713,7 @@ -(void)stream:(NSStream*) stream handleEvent:(NSStreamEvent) eventCode self->_streamHasSpace = NO; //restart logintimer when our output stream becomes readable (don't do anything without a running timer) - if(_cancelLoginTimer != nil && self->_accountState < kStateLoggedIn) + if(_loginTimer != nil && self->_accountState < kStateLoggedIn) [self reinitLoginTimer]; //we want this to be sync instead of async to make sure we are in kStateConnected before sending anything @@ -4873,7 +4903,7 @@ -(void) writeFromQueue } //restart logintimer for new write to our stream while not logged in (don't do anything without a running timer) - if(_cancelLoginTimer != nil && self->_accountState < kStateLoggedIn) + if(_loginTimer != nil && self->_accountState < kStateLoggedIn) [self reinitLoginTimer]; if(requestAck) diff --git a/Monal/Images.xcassets/QuicksyAppIcon.appiconset/Quicksy-ios-1024.png b/Monal/Images.xcassets/QuicksyAppIcon.appiconset/Quicksy-ios-1024.png index 7239ddd968..00cde6f443 100644 Binary files a/Monal/Images.xcassets/QuicksyAppIcon.appiconset/Quicksy-ios-1024.png and b/Monal/Images.xcassets/QuicksyAppIcon.appiconset/Quicksy-ios-1024.png differ diff --git a/Monal/Images.xcassets/QuicksyAppLogo.imageset/Quicksy-ios-1024.png b/Monal/Images.xcassets/QuicksyAppLogo.imageset/Quicksy-ios-1024.png index 7239ddd968..00cde6f443 100644 Binary files a/Monal/Images.xcassets/QuicksyAppLogo.imageset/Quicksy-ios-1024.png and b/Monal/Images.xcassets/QuicksyAppLogo.imageset/Quicksy-ios-1024.png differ diff --git a/Monal/Monal-iOS/Quicksy Launch Screen.storyboard b/Monal/Monal-iOS/Quicksy Launch Screen.storyboard new file mode 100644 index 0000000000..3172981417 --- /dev/null +++ b/Monal/Monal-iOS/Quicksy Launch Screen.storyboard @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Monal/Alpha.Monal-Info.plist b/Monal/Monal.Alpha-Info.plist similarity index 100% rename from Monal/Alpha.Monal-Info.plist rename to Monal/Monal.Alpha-Info.plist diff --git a/Monal/Alpha.Monal.ios.entitlements b/Monal/Monal.Alpha.ios.entitlements similarity index 100% rename from Monal/Alpha.Monal.ios.entitlements rename to Monal/Monal.Alpha.ios.entitlements diff --git a/Monal/Alpha.Monal.macos.entitlements b/Monal/Monal.Alpha.macos.entitlements similarity index 100% rename from Monal/Alpha.Monal.macos.entitlements rename to Monal/Monal.Alpha.macos.entitlements diff --git a/Monal/Monal.xcodeproj/project.pbxproj b/Monal/Monal.xcodeproj/project.pbxproj index 493d25f3f6..d7a76444df 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 */; }; @@ -39,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, ); }; }; @@ -141,11 +141,16 @@ 842790852A32D16D005C18CC /* CallSounds in Resources */ = {isa = PBXBuildFile; fileRef = 842790842A32D16C005C18CC /* CallSounds */; }; 843AD3AB2AA55CE20036844D /* MLOgHtmlParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 843AD3AA2AA55CE20036844D /* MLOgHtmlParser.swift */; }; 8441EFF92921B53500E851E9 /* BackgroundSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8441EFF82921B53500E851E9 /* BackgroundSettings.swift */; }; + 844921EA2C29F9A000B99A9C /* MLDelayableTimer.m in Sources */ = {isa = PBXBuildFile; fileRef = 844921E92C29F9A000B99A9C /* MLDelayableTimer.m */; }; + 844921EC2C29F9BE00B99A9C /* MLDelayableTimer.h in Headers */ = {isa = PBXBuildFile; fileRef = 844921EB2C29F9BE00B99A9C /* MLDelayableTimer.h */; }; 844EEC6D28E718DB00CB5EF9 /* UIColor+Theme.m in Sources */ = {isa = PBXBuildFile; fileRef = 26D59D9220714F32006F1DEE /* UIColor+Theme.m */; }; + 845836BA2C49F36300B11EC5 /* Quicksy Launch Screen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 845836B92C49F36300B11EC5 /* Quicksy Launch Screen.storyboard */; }; 845D636B2AD4AEDA0066EFFB /* ImageViewer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845D636A2AD4AEDA0066EFFB /* ImageViewer.swift */; }; 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 */; }; + 848501342C4F2B6D00C1B693 /* CountryCodes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848501332C4F2B6D00C1B693 /* CountryCodes.swift */; }; 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 */; }; @@ -156,6 +161,7 @@ 849ADF3F2BACF0360009BCD7 /* CocoaLumberjack in Frameworks */ = {isa = PBXBuildFile; productRef = 849ADF3E2BACF0360009BCD7 /* CocoaLumberjack */; }; 849ADF412BACF0360009BCD7 /* CocoaLumberjackSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 849ADF402BACF0360009BCD7 /* CocoaLumberjackSwift */; }; 849ADF432BACF0360009BCD7 /* CocoaLumberjackSwiftLogBackend in Frameworks */ = {isa = PBXBuildFile; productRef = 849ADF422BACF0360009BCD7 /* CocoaLumberjackSwiftLogBackend */; }; + 84BBAECA2C42D272009492E2 /* Quicksy_RegisterAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84BBAEC92C42D272009492E2 /* Quicksy_RegisterAccount.swift */; }; 84C1CD502A8C764D007076ED /* SwiftHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84C1CD4F2A8C764D007076ED /* SwiftHelpers.swift */; }; 84C1CD522A8F617F007076ED /* MLStreamRedirect.m in Sources */ = {isa = PBXBuildFile; fileRef = 84C1CD512A8F617F007076ED /* MLStreamRedirect.m */; }; 84C1CD542A8F6196007076ED /* MLStreamRedirect.h in Headers */ = {isa = PBXBuildFile; fileRef = 84C1CD532A8F6196007076ED /* MLStreamRedirect.h */; }; @@ -163,7 +169,6 @@ 84E231F32C16A9CE00735FB7 /* SVGView in Frameworks */ = {isa = PBXBuildFile; productRef = 84E231F22C16A9CE00735FB7 /* SVGView */; }; 84E55E7D2964424E003E191A /* ActiveChatsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 261A6284176C156500059090 /* ActiveChatsViewController.m */; }; 84E55E8029644279003E191A /* ActiveChatsViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = 84E55E7F2964426D003E191A /* ActiveChatsViewController.h */; }; - 84F194CE2C101A3E00F0A994 /* PromiseKit in Frameworks */ = {isa = PBXBuildFile; productRef = 84F194CD2C101A3E00F0A994 /* PromiseKit */; }; 84F194D12C15197200F0A994 /* FrameUp in Frameworks */ = {isa = PBXBuildFile; productRef = 84F194D02C15197200F0A994 /* FrameUp */; }; 84FC37552897521500634E3E /* snprintf.m in Sources */ = {isa = PBXBuildFile; fileRef = 84FC37542897521400634E3E /* snprintf.m */; }; 84FC37572897523500634E3E /* metamacros.h in Headers */ = {isa = PBXBuildFile; fileRef = 84FC37562897523500634E3E /* metamacros.h */; }; @@ -306,6 +311,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 = ""; }; @@ -396,8 +402,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 = ""; }; @@ -577,8 +581,13 @@ 842790842A32D16C005C18CC /* CallSounds */ = {isa = PBXFileReference; lastKnownFileType = folder; path = CallSounds; sourceTree = ""; }; 843AD3AA2AA55CE20036844D /* MLOgHtmlParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = MLOgHtmlParser.swift; path = Classes/MLOgHtmlParser.swift; sourceTree = ""; }; 8441EFF82921B53500E851E9 /* BackgroundSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundSettings.swift; sourceTree = ""; }; + 844921E92C29F9A000B99A9C /* MLDelayableTimer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MLDelayableTimer.m; sourceTree = ""; }; + 844921EB2C29F9BE00B99A9C /* MLDelayableTimer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MLDelayableTimer.h; sourceTree = ""; }; + 845836B92C49F36300B11EC5 /* Quicksy Launch Screen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = "Quicksy Launch Screen.storyboard"; path = "Monal-iOS/Quicksy Launch Screen.storyboard"; 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 = ""; }; + 848501332C4F2B6D00C1B693 /* CountryCodes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountryCodes.swift; 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 = ""; }; @@ -587,6 +596,7 @@ 849248482AD4CEC400986C1A /* ZoomableContainer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ZoomableContainer.swift; sourceTree = ""; }; 849A53E3287135B2007E941A /* MLVoIPProcessor.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = MLVoIPProcessor.m; path = Classes/MLVoIPProcessor.m; sourceTree = SOURCE_ROOT; }; 849A53E5287135D7007E941A /* MLVoIPProcessor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = MLVoIPProcessor.h; path = Classes/MLVoIPProcessor.h; sourceTree = SOURCE_ROOT; }; + 84BBAEC92C42D272009492E2 /* Quicksy_RegisterAccount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Quicksy_RegisterAccount.swift; sourceTree = ""; }; 84C1CD4F2A8C764D007076ED /* SwiftHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftHelpers.swift; sourceTree = ""; }; 84C1CD512A8F617F007076ED /* MLStreamRedirect.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MLStreamRedirect.m; sourceTree = ""; }; 84C1CD532A8F6196007076ED /* MLStreamRedirect.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MLStreamRedirect.h; sourceTree = ""; }; @@ -742,7 +752,7 @@ C1E856A828DECF5F00B104E9 /* eu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = eu; path = external/eu.lproj/iosShare.strings; sourceTree = ""; }; C1E856A928DECF5F00B104E9 /* eu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = eu; path = external/eu.lproj/Localizable.strings; sourceTree = ""; }; C1E8A7F62B8E47C300760220 /* EditGroupSubject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditGroupSubject.swift; sourceTree = ""; }; - C1F0AD15288BCE6F00BB0182 /* Alpha.Monal.ios.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Alpha.Monal.ios.entitlements; sourceTree = ""; }; + C1F0AD15288BCE6F00BB0182 /* Monal.Alpha.ios.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Monal.Alpha.ios.entitlements; sourceTree = ""; }; C1F5C7A72775DA000001F295 /* MLContactSoftwareVersionInfo.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MLContactSoftwareVersionInfo.h; sourceTree = ""; }; C1F5C7A82775DA000001F295 /* MLContactSoftwareVersionInfo.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MLContactSoftwareVersionInfo.m; sourceTree = ""; }; C1F5C7AB2777621B0001F295 /* ContactResources.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactResources.swift; sourceTree = ""; }; @@ -805,7 +815,6 @@ buildActionMask = 2147483647; files = ( 849ADF3F2BACF0360009BCD7 /* CocoaLumberjack in Frameworks */, - 84F194CE2C101A3E00F0A994 /* PromiseKit in Frameworks */, BE8B63D2491B1E5582965A8F /* Pods_monalxmpp.framework in Frameworks */, 849ADF412BACF0360009BCD7 /* CocoaLumberjackSwift in Frameworks */, 84E231F32C16A9CE00735FB7 /* SVGView in Frameworks */, @@ -1050,6 +1059,7 @@ 26715E5E17650AF900684F3D /* View Controllers */ = { isa = PBXGroup; children = ( + 848227902C4A6194003CCA33 /* MLPlaceholderViewController.m */, 849248482AD4CEC400986C1A /* ZoomableContainer.swift */, 845D636A2AD4AEDA0066EFFB /* ImageViewer.swift */, 841B6F16297AFB340074F9B7 /* Calls */, @@ -1060,8 +1070,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 */, @@ -1126,6 +1134,7 @@ 26B2A4B41B73040000272E63 /* Monal-iOS */ = { isa = PBXGroup; children = ( + 845836B92C49F36300B11EC5 /* Quicksy Launch Screen.storyboard */, 841EE42E2A426F0900D3AF14 /* tools */, 84D31CDA28653AA9006D7926 /* WebRTC */, C1E4654424EE515200CA5AAF /* localization */, @@ -1224,6 +1233,8 @@ 54F0B81828231690003664BD /* WelcomeLogIn.swift */, C12436122434AB5D00B8F074 /* MLAttributedLabel.h */, C12436132434AB5D00B8F074 /* MLAttributedLabel.m */, + 20D3611B2C10E12500E46587 /* BoardingCards.swift */, + 84BBAEC92C42D272009492E2 /* Quicksy_RegisterAccount.swift */, ); name = OnBoard; sourceTree = ""; @@ -1258,6 +1269,9 @@ 84C1CD4F2A8C764D007076ED /* SwiftHelpers.swift */, 84C1CD512A8F617F007076ED /* MLStreamRedirect.m */, 84C1CD532A8F6196007076ED /* MLStreamRedirect.h */, + 844921E92C29F9A000B99A9C /* MLDelayableTimer.m */, + 844921EB2C29F9BE00B99A9C /* MLDelayableTimer.h */, + 848501332C4F2B6D00C1B693 /* CountryCodes.swift */, ); name = tools; sourceTree = ""; @@ -1266,7 +1280,7 @@ isa = PBXGroup; children = ( 8414ADF92A7ABAC900EFFCCC /* Packages */, - C1F0AD15288BCE6F00BB0182 /* Alpha.Monal.ios.entitlements */, + C1F0AD15288BCE6F00BB0182 /* Monal.Alpha.ios.entitlements */, C1567E3628255C64006E9637 /* Monal.ios.entitlements */, C1567E3528255C64006E9637 /* Monal.macos.entitlements */, 26AA70222146E2B900598605 /* shareSheet.entitlements */, @@ -1491,6 +1505,7 @@ 54A22D2D26185E7E00B56EAD /* MLNotificationQueue.h in Headers */, 84E55E8029644279003E191A /* ActiveChatsViewController.h in Headers */, 541E4CC4254D369200FD7B28 /* MLPubSubProcessor.h in Headers */, + 844921EC2C29F9BE00B99A9C /* MLDelayableTimer.h in Headers */, 84C1CD542A8F6196007076ED /* MLStreamRedirect.h in Headers */, 389E298D25E901CA009A5268 /* MLAudioRecoderManager.h in Headers */, 541E4CBE254AA0B600FD7B28 /* MLHandler.h in Headers */, @@ -1592,7 +1607,6 @@ 849ADF3E2BACF0360009BCD7 /* CocoaLumberjack */, 849ADF402BACF0360009BCD7 /* CocoaLumberjackSwift */, 849ADF422BACF0360009BCD7 /* CocoaLumberjackSwiftLogBackend */, - 84F194CD2C101A3E00F0A994 /* PromiseKit */, 84E231F22C16A9CE00735FB7 /* SVGView */, ); productName = monalxmpp; @@ -1745,7 +1759,6 @@ C1E1EC79286A025F0097EC74 /* XCRemoteSwiftPackageReference "SwiftSoup" */, 841898A82957712000FEC77D /* XCRemoteSwiftPackageReference "ViewExtractor" */, 849ADF3D2BACF0360009BCD7 /* XCRemoteSwiftPackageReference "cocoalumberjack" */, - 84F194CC2C101A3E00F0A994 /* XCRemoteSwiftPackageReference "PromiseKit" */, 84F194CF2C15197200F0A994 /* XCRemoteSwiftPackageReference "FrameUp" */, 84E231F12C16A9CE00735FB7 /* XCRemoteSwiftPackageReference "SVGView" */, ); @@ -1774,6 +1787,7 @@ 848C73E02BDF2014007035C9 /* PrivacyInfo.xcprivacy in Resources */, 26B0CA8B21AE410E0080B133 /* AlertSounds in Resources */, 842790852A32D16D005C18CC /* CallSounds in Resources */, + 845836BA2C49F36300B11EC5 /* Quicksy Launch Screen.storyboard in Resources */, 26470F521835C4080069E3E0 /* Media.xcassets in Resources */, 26B2A4BB1B73061400272E63 /* Images.xcassets in Resources */, 26E8462824EABAED00ECE419 /* Main.storyboard in Resources */, @@ -2060,6 +2074,7 @@ 3D65B78D27234B74005A30F4 /* ContactDetails.swift in Sources */, E89DD32525C6626400925F62 /* MLFileTransferDataCell.m in Sources */, E89DD32825C6626400925F62 /* MLFileTransferTextCell.m in Sources */, + 84BBAECA2C42D272009492E2 /* Quicksy_RegisterAccount.swift in Sources */, 261A6281176C055400059090 /* ContactsViewController.m in Sources */, 26B0CA8921AE2E3C0080B133 /* MLSoundsTableViewController.m in Sources */, 84D31CE628653B83006D7926 /* WebRTCClient.swift in Sources */, @@ -2073,6 +2088,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 */, @@ -2082,7 +2098,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 */, @@ -2103,6 +2118,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 */, @@ -2175,6 +2191,7 @@ 542CF3FF2763314F002C3710 /* hsluv.c in Sources */, 8420EA9D2915E5100038FF40 /* OmemoState.m in Sources */, 389E298C25E901CA009A5268 /* MLAudioRecoderManager.m in Sources */, + 848501342C4F2B6D00C1B693 /* CountryCodes.swift in Sources */, 84C1CD522A8F617F007076ED /* MLStreamRedirect.m in Sources */, 26CC57C923A0892800ABB92A /* MLMessageProcessor.m in Sources */, C18E757C245E8AE900AE8FB7 /* MLPipe.m in Sources */, @@ -2192,6 +2209,7 @@ 26CC57C723A0892100ABB92A /* MLContact.m in Sources */, 540F625F24BA951E0008A6D8 /* HelperTools.m in Sources */, 38720923251EDE07001837EB /* MLXEPSlashMeHandler.m in Sources */, + 844921EA2C29F9A000B99A9C /* MLDelayableTimer.m in Sources */, 544656BB2534910D006B2953 /* XMPPDataForm.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -2666,7 +2684,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 14.0; LLVM_LTO = YES; MACOSX_DEPLOYMENT_TARGET = 11.0; - MARKETING_VERSION = 6.4.0; + MARKETING_VERSION = 0.0.1; PRESERVE_DEAD_CODE_INITS_AND_TERMS = NO; SDKROOT = iphoneos; STRIP_INSTALLED_PRODUCT = NO; @@ -3033,7 +3051,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 14.0; LLVM_LTO = YES; MACOSX_DEPLOYMENT_TARGET = 11.0; - MARKETING_VERSION = 6.4.0; + MARKETING_VERSION = 0.0.1; PRESERVE_DEAD_CODE_INITS_AND_TERMS = NO; SDKROOT = iphoneos; STRIP_INSTALLED_PRODUCT = NO; @@ -3195,7 +3213,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 14.0; LLVM_LTO = YES; MACOSX_DEPLOYMENT_TARGET = 11.0; - MARKETING_VERSION = 6.4.0; + MARKETING_VERSION = 0.0.1; ONLY_ACTIVE_ARCH = YES; PRESERVE_DEAD_CODE_INITS_AND_TERMS = NO; RUN_CLANG_STATIC_ANALYZER = YES; @@ -3471,7 +3489,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 14.0; LLVM_LTO = YES; MACOSX_DEPLOYMENT_TARGET = 11.0; - MARKETING_VERSION = 6.4.0; + MARKETING_VERSION = 0.0.1; PRESERVE_DEAD_CODE_INITS_AND_TERMS = NO; RUN_CLANG_STATIC_ANALYZER = YES; SDKROOT = iphoneos; @@ -3824,7 +3842,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 14.0; LLVM_LTO = YES; MACOSX_DEPLOYMENT_TARGET = 11.0; - MARKETING_VERSION = 6.4.0; + MARKETING_VERSION = 0.0.1; PRESERVE_DEAD_CODE_INITS_AND_TERMS = NO; SDKROOT = iphoneos; STRIP_INSTALLED_PRODUCT = NO; @@ -4238,7 +4256,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 14.0; LLVM_LTO = NO; MACOSX_DEPLOYMENT_TARGET = 11.0; - MARKETING_VERSION = 6.4.0; + MARKETING_VERSION = 0.0.1; PRESERVE_DEAD_CODE_INITS_AND_TERMS = NO; RUN_CLANG_STATIC_ANALYZER = YES; SDKROOT = iphoneos; @@ -4274,9 +4292,9 @@ CLANG_WARN_STRICT_PROTOTYPES = YES_ERROR; CLANG_WARN_VEXING_PARSE = YES_ERROR; CLANG_WARN__EXIT_TIME_DESTRUCTORS = YES; - CODE_SIGN_ENTITLEMENTS = Alpha.Monal.ios.entitlements; - "CODE_SIGN_ENTITLEMENTS[sdk=iphoneos*]" = Alpha.Monal.ios.entitlements; - "CODE_SIGN_ENTITLEMENTS[sdk=macosx*]" = Alpha.Monal.macos.entitlements; + CODE_SIGN_ENTITLEMENTS = Monal.Alpha.ios.entitlements; + "CODE_SIGN_ENTITLEMENTS[sdk=iphoneos*]" = Monal.Alpha.ios.entitlements; + "CODE_SIGN_ENTITLEMENTS[sdk=macosx*]" = Monal.Alpha.macos.entitlements; COMPILER_INDEX_STORE_ENABLE = YES; CURRENT_PROJECT_VERSION = 0; DEAD_CODE_STRIPPING = NO; @@ -4300,7 +4318,7 @@ GCC_WARN_NON_VIRTUAL_DESTRUCTOR = YES; GCC_WARN_PEDANTIC = NO; GCC_WARN_STRICT_SELECTOR_MATCH = NO; - INFOPLIST_FILE = "Alpha.Monal-Info.plist"; + INFOPLIST_FILE = "Monal.Alpha-Info.plist"; INFOPLIST_KEY_CFBundleDisplayName = Monal.alpha; LD_RUNPATH_SEARCH_PATHS = ( /usr/lib/swift, @@ -4642,14 +4660,6 @@ minimumVersion = 1.0.6; }; }; - 84F194CC2C101A3E00F0A994 /* XCRemoteSwiftPackageReference "PromiseKit" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/mxcl/PromiseKit"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 8.1.2; - }; - }; 84F194CF2C15197200F0A994 /* XCRemoteSwiftPackageReference "FrameUp" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/ryanlintott/FrameUp"; @@ -4706,11 +4716,6 @@ package = 84E231F12C16A9CE00735FB7 /* XCRemoteSwiftPackageReference "SVGView" */; productName = SVGView; }; - 84F194CD2C101A3E00F0A994 /* PromiseKit */ = { - isa = XCSwiftPackageProductDependency; - package = 84F194CC2C101A3E00F0A994 /* XCRemoteSwiftPackageReference "PromiseKit" */; - productName = PromiseKit; - }; 84F194D02C15197200F0A994 /* FrameUp */ = { isa = XCSwiftPackageProductDependency; package = 84F194CF2C15197200F0A994 /* XCRemoteSwiftPackageReference "FrameUp" */; diff --git a/Monal/Podfile b/Monal/Podfile index 7c601fd65c..3c5aaac2db 100644 --- a/Monal/Podfile +++ b/Monal/Podfile @@ -18,9 +18,10 @@ def monal pod 'MBProgressHUD', '~> 1.2.0' pod 'SDWebImage' pod 'DZNEmptyDataSet' - pod 'TOCropViewController' + pod 'CropViewController' pod 'NotificationBannerSwift', '~> 3.2.0' pod 'FLAnimatedImage', '~> 1.0' + pod "PromiseKit" end def monalxmpp @@ -30,22 +31,30 @@ def monalxmpp pod 'SAMKeychain' pod 'sqlite3/perf-threadsafe', inhibit_warnings: true pod 'ASN1Decoder' + #later versions of the webrtc lib trigger the following app review error: + #The app references non-public symbols in Contents/Frameworks/WebRTC.framework/Versions/A/WebRTC: + #_AVCaptureSessionInterruptionReasonKey, _AVCaptureSessionPresetInputPriority. + #If method names in your source code match the private Apple APIs listed above, altering your method names + #will help prevent this app from being flagged in future submissions. + pod 'WebRTC-lib', '~> 123.0' #pod 'GoogleWebRTC' - pod 'WebRTC-lib' pod 'KSCrash', subspecs:['Recording', 'Reporting/Filters/Sets', 'Reporting/Filters/Tools', 'Reporting/Tools', 'Core'] signalDeps + pod "PromiseKit" end target 'shareSheet' do # Uncomment the next line if you're using Swift or would like to use dynamic frameworks use_frameworks! inhibit_all_warnings! + pod "PromiseKit" end target 'NotificationService' do # Uncomment the next line if you're using Swift or would like to use dynamic frameworks use_frameworks! inhibit_all_warnings! + pod "PromiseKit" end target 'Monal' do diff --git a/Monal/Podfile.lock b/Monal/Podfile.lock index d67b4df377..fed3e2c437 100644 --- a/Monal/Podfile.lock +++ b/Monal/Podfile.lock @@ -1,27 +1,28 @@ PODS: - ASN1Decoder (1.10.0) + - CropViewController (2.7.4) - DZNEmptyDataSet (1.8.1) - FLAnimatedImage (1.0.17) - - KSCrash/Core (1.17.0): + - KSCrash/Core (1.17.4): - KSCrash/Reporting/Filters/Basic - - KSCrash/Recording (1.17.0): - - KSCrash/Recording/Tools (= 1.17.0) - - KSCrash/Recording/Tools (1.17.0) - - KSCrash/Reporting/Filters/AppleFmt (1.17.0): + - KSCrash/Recording (1.17.4): + - KSCrash/Recording/Tools (= 1.17.4) + - KSCrash/Recording/Tools (1.17.4) + - KSCrash/Reporting/Filters/AppleFmt (1.17.4): - KSCrash/Recording - KSCrash/Reporting/Filters/Base - - KSCrash/Reporting/Filters/Base (1.17.0): + - KSCrash/Reporting/Filters/Base (1.17.4): - KSCrash/Recording - - KSCrash/Reporting/Filters/Basic (1.17.0): + - KSCrash/Reporting/Filters/Basic (1.17.4): - KSCrash/Recording - KSCrash/Reporting/Filters/Base - - KSCrash/Reporting/Filters/GZip (1.17.0): + - KSCrash/Reporting/Filters/GZip (1.17.4): - KSCrash/Recording - KSCrash/Reporting/Filters/Base - - KSCrash/Reporting/Filters/JSON (1.17.0): + - KSCrash/Reporting/Filters/JSON (1.17.4): - KSCrash/Recording - KSCrash/Reporting/Filters/Base - - KSCrash/Reporting/Filters/Sets (1.17.0): + - KSCrash/Reporting/Filters/Sets (1.17.4): - KSCrash/Recording - KSCrash/Reporting/Filters/AppleFmt - KSCrash/Reporting/Filters/Base @@ -29,34 +30,43 @@ PODS: - KSCrash/Reporting/Filters/GZip - KSCrash/Reporting/Filters/JSON - KSCrash/Reporting/Filters/Stringify - - KSCrash/Reporting/Filters/Stringify (1.17.0): + - KSCrash/Reporting/Filters/Stringify (1.17.4): - KSCrash/Recording - KSCrash/Reporting/Filters/Base - - KSCrash/Reporting/Filters/Tools (1.17.0): + - KSCrash/Reporting/Filters/Tools (1.17.4): - KSCrash/Recording - - KSCrash/Reporting/Tools (1.17.0): + - KSCrash/Reporting/Tools (1.17.4): - KSCrash/Recording - MarqueeLabel (4.3.2) - MBProgressHUD (1.2.0) - NotificationBannerSwift (3.2.1): - MarqueeLabel (~> 4.3.0) - SnapKit (~> 5.6.0) + - PromiseKit (8.1.1): + - PromiseKit/CorePromise (= 8.1.1) + - PromiseKit/Foundation (= 8.1.1) + - PromiseKit/UIKit (= 8.1.1) + - PromiseKit/CorePromise (8.1.1) + - PromiseKit/Foundation (8.1.1): + - PromiseKit/CorePromise + - PromiseKit/UIKit (8.1.1): + - PromiseKit/CorePromise - SAMKeychain (1.5.3) - - SDWebImage (5.19.1): - - SDWebImage/Core (= 5.19.1) - - SDWebImage/Core (5.19.1) + - SDWebImage (5.19.2): + - SDWebImage/Core (= 5.19.2) + - SDWebImage/Core (5.19.2) - SignalProtocolC (2.3.3) - SignalProtocolObjC (1.1.1): - SignalProtocolC (~> 2.3.3) - SnapKit (5.6.0) - - sqlite3/common (3.45.1) - - sqlite3/perf-threadsafe (3.45.1): + - "sqlite3/common (3.46.0+1)" + - "sqlite3/perf-threadsafe (3.46.0+1)": - sqlite3/common - - TOCropViewController (2.6.1) - WebRTC-lib (123.0.0) DEPENDENCIES: - ASN1Decoder + - CropViewController - DZNEmptyDataSet - FLAnimatedImage (~> 1.0) - KSCrash/Core @@ -66,28 +76,29 @@ DEPENDENCIES: - KSCrash/Reporting/Tools - MBProgressHUD (~> 1.2.0) - NotificationBannerSwift (~> 3.2.0) + - PromiseKit - SAMKeychain - SDWebImage - SignalProtocolC (from `https://github.com/monal-im/libsignal-protocol-c`, branch `master`) - SignalProtocolObjC (from `https://github.com/monal-im/SignalProtocol-ObjC.git`, branch `master`) - sqlite3/perf-threadsafe - - TOCropViewController - - WebRTC-lib + - WebRTC-lib (~> 123.0) SPEC REPOS: trunk: - ASN1Decoder + - CropViewController - DZNEmptyDataSet - FLAnimatedImage - KSCrash - MarqueeLabel - MBProgressHUD - NotificationBannerSwift + - PromiseKit - SAMKeychain - SDWebImage - SnapKit - sqlite3 - - TOCropViewController - WebRTC-lib EXTERNAL SOURCES: @@ -108,21 +119,22 @@ CHECKOUT OPTIONS: SPEC CHECKSUMS: ASN1Decoder: 91cb1d781b5a178ea7375b2f1519e2bdaaa4c427 + CropViewController: 3489bbf95a3e11c654382b0bae08ac645cdf1b93 DZNEmptyDataSet: 9525833b9e68ac21c30253e1d3d7076cc828eaa7 FLAnimatedImage: bbf914596368867157cc71b38a8ec834b3eeb32b - KSCrash: 593ec373759e4c1bce381421a627326a20d2dc66 + KSCrash: 158a0998f08ae7d4e54ef8a2da62d6e08b46d03a MarqueeLabel: 15e524a6762552bb279cb17438b8a94990269fb9 MBProgressHUD: 3ee5efcc380f6a79a7cc9b363dd669c5e1ae7406 NotificationBannerSwift: dce54ded532b26e30cd8e7f4d80e124a0f2ba7d1 + PromiseKit: d1be44b474e5acfa16adf007a1f49f104e10fead SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c - SDWebImage: 40b0b4053e36c660a764958bff99eed16610acbb + SDWebImage: dfe95b2466a9823cf9f0c6d01217c06550d7b29a SignalProtocolC: 8092866e45b663a6bc3e45a8d13bad2571dbf236 SignalProtocolObjC: 1beb46b1d35733e7ab96a919f88bac20ec771c73 SnapKit: e01d52ebb8ddbc333eefe2132acf85c8227d9c25 - sqlite3: 73b7fc691fdc43277614250e04d183740cb15078 - TOCropViewController: edfd4f25713d56905ad1e0b9f5be3fbe0f59c863 + sqlite3: 292c3e1bfe89f64e51ea7fc7dab9182a017c8630 WebRTC-lib: bb973dd47acf5bc48d8a935a92ae836b70599bc1 -PODFILE CHECKSUM: f415f317e34fd11a687374f84ee8dfb14ebb29a6 +PODFILE CHECKSUM: f766ee234cce3182eaa8a645d3fa1e41666094d2 COCOAPODS: 1.15.2 diff --git a/Monal/Quicksy-Info.plist b/Monal/Quicksy-Info.plist index 5704bed50e..531c210809 100644 --- a/Monal/Quicksy-Info.plist +++ b/Monal/Quicksy-Info.plist @@ -88,6 +88,8 @@ Quicksy allows users to save photos received in conversations. NSPhotoLibraryUsageDescription Quicksy allows users to upload photos to recipients in a conversation + NSContactsUsageDescription + Quicksy syncs your contact list in regular intervals to make suggestions about possible contacts, who are already using the app. NSUserActivityTypes INSendMessageIntent @@ -108,7 +110,7 @@ UIFileSharingEnabled UILaunchStoryboardName - Launch Screen + Quicksy Launch Screen UIMainStoryboardFile Main UIPrerenderedIcon diff --git a/Monal/localization/Base.lproj/Main.storyboard b/Monal/localization/Base.lproj/Main.storyboard index dd2ae0e236..c12ba625db 100644 --- a/Monal/localization/Base.lproj/Main.storyboard +++ b/Monal/localization/Base.lproj/Main.storyboard @@ -79,29 +79,7 @@ - - - - - - - - - - - - - - - - - - - - - - - + diff --git a/Monal/monalxmpp/module.modulemap b/Monal/monalxmpp/module.modulemap index b8dca695ce..c190e04395 100644 --- a/Monal/monalxmpp/module.modulemap +++ b/Monal/monalxmpp/module.modulemap @@ -41,4 +41,8 @@ module MLCallPrivate { module HelperToolsPrivate { header "../Classes/HelperTools.h" export * +} +module MLDelayableTimerPrivate { + header "../Classes/MLDelayableTimer.h" + export * } \ No newline at end of file diff --git a/Monal/monalxmpp/monalxmpp.h b/Monal/monalxmpp/monalxmpp.h index 8c5b67e332..e42b4602c1 100644 --- a/Monal/monalxmpp/monalxmpp.h +++ b/Monal/monalxmpp/monalxmpp.h @@ -25,3 +25,4 @@ FOUNDATION_EXPORT const unsigned char monalxmppVersionString[]; #import "MLVoIPProcessor.h" #import "MLCall.h" #import "HelperTools.h" +#import "MLDelayableTimer.h" diff --git a/Monal/shareSheet-iOS/ShareViewController.m b/Monal/shareSheet-iOS/ShareViewController.m index 241b60ecf0..e39c379ac0 100644 --- a/Monal/shareSheet-iOS/ShareViewController.m +++ b/Monal/shareSheet-iOS/ShareViewController.m @@ -55,10 +55,14 @@ -(void) viewDidLoad [self.navigationController.navigationBar setBackgroundColor:[UIColor monaldarkGreen]]; self.navigationController.navigationItem.title = NSLocalizedString(@"Monal", @""); + DDLogInfo(@"Extension context: %@", self.extensionContext); + DDLogDebug(@"Raw extension context intent: %@", self.extensionContext.intent); if(self.extensionContext.intent != nil && [self.extensionContext.intent isKindOfClass:[INSendMessageIntent class]]) { INSendMessageIntent* intent = (INSendMessageIntent*)self.extensionContext.intent; + DDLogDebug(@"Got usable intent: %@", intent); self.intentContact = [HelperTools unserializeData:[intent.conversationIdentifier dataUsingEncoding:NSISOLatin1StringEncoding]]; + DDLogInfo(@"Extracted intent contact: %@", self.intentContact); [self.intentContact refresh]; //make sure we are up to date } } @@ -73,6 +77,7 @@ - (void) presentationAnimationDidFinish if(self.intentContact != nil) { + DDLogInfo(@"Intent contact given: %@", self.intentContact); //check if intentContact is in enabled account list for(NSDictionary* accountToCheck in self.accounts) { @@ -89,6 +94,7 @@ - (void) presentationAnimationDidFinish //no intent given or intent contact not found --> select initial recipient (contact with most recent interaction) if(!self.account || !self.recipient) { + DDLogInfo(@"No recipient given, selecting the one with the most recent interaction..."); BOOL recipientFound = NO; for(MLContact* recipient in self.recipients) { diff --git a/ReadMe.md b/ReadMe.md index b8ed7627d7..7728b3481d 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -29,7 +29,7 @@ Monal is developed by volunteers and community collaboration. The work which has - Donate via [GitHub Sponsors](https://github.com/sponsors/tmolitor-stud-tu) - Donate via [Libera Pay](https://liberapay.com/tmolitor) -- EU citizens can donate via SEPA, too. Just contact Thilo Molitor via mail to thilo@monal-im.org to get his IBAN. +- EU citizens can donate via SEPA, too. IBAN: DE66 5007 0371 0856 0419 01 Here you can read about further [support of the development](https://github.com/monal-im/Monal/issues/363)! diff --git a/monal.doap b/monal.doap index 7ece02a1f2..df9a9f3c61 100644 --- a/monal.doap +++ b/monal.doap @@ -61,163 +61,155 @@ complete + 2.13.1 4.9 - XEP-0004: Data Forms wontfix - XEP-0027: Current Jabber OpenPGP Usage + Superseded by OX complete + 2.5.0 4.6 - XEP-0030: Service Discovery - - - - - - wontfix - XEP-0033: Extended Stanza Addressing partial + 1.34.6 5.0 - XEP-0045: Multi-User Chat wontfix - XEP-0047: In-Band Bytestreams complete + 1.2 5.0 - XEP-0048: Bookmarks wontfix - XEP-0049: Private XML Storage + Not used anymore, use PubSub/Pep complete - XEP-0054: vcard-temp (implemented only for MUC profiles) - - - - - - wontfix - XEP-0055: Jabber Search + 1.2 + 5.0 + Implemented only for MUC profiles complete + 1.0 4.8 - XEP-0059: Result Set Management. Used by other XEPs. partial + 1.26.0 4.9 - XEP-0060: Publish-Subscribe + Used mainly for Pep wontfix - XEP-0065: SOCKS5 Bytestreams + Use HTTP upload for filetransfers instead partial + 1.5 4.9 - XEP-0066: Out of Band Data + Used to mark XEP-0363 filetransfers only wontfix - XEP-0070: Verifying HTTP Requests via XMPP partial - XEP-0077: In-Band Registration + 2.4 + 4.7 complete + 1.1.4 4.9 - XEP-0084: User Avatar - complete + partial + 2.1 4.7 - XEP-0085: Chat State Notifications + Only typing notifications, use XEP-0319 to publish interactions complete - XEP-0092: Software Version + 1.1 + 5.0 wontfix - XEP-0107: User Mood complete + 1.6.0 4.7 - XEP-0115: Entity Capabilities - complete + partial + 1.1.1 + 5.0 XEP-0153: vCard-Based Avatars (implemented only for MUC profiles) @@ -225,92 +217,93 @@ planned - XEP-0158: CAPTCHA Forms - complete - XEP-0162: Best Practices for Roster and Subscription Management + partial + 0.2.1 + 5.4 complete + 1.2.2 4.9 - XEP-0163: Personal Eventing Protocol complete + 1.2.2 6.0 - XEP-0167: Jingle RTP Sessions complete + 1.1.1 6.0 - XEP-0176: Jingle ICE-UDP Transport Method complete + 1.1 4.9 - XEP-0172: User Nickname complete + 1.4.0 4.7 - XEP-0184: Message Receipts complete + 1.3 5.0 - XEP-0191: Blocking Command complete + 1.6.1 4.6 - XEP-0198: Stream Management complete + 2.0.1 4.7 - XEP-0199: XMPP Ping complete + 1.0.0 6.0 - XEP-0215: External Service Discovery complete + 1.1.1 4.9 XEP-0223: Persistent Storage of Private Data via PubSub @@ -319,29 +312,30 @@ wontfix - XEP-0234: Jingle File Transfer + Use HTTP filetransfer (XEP-0363) instead complete + 1.3 4.6 - XEP-0237: Roster Versioning complete + 1.0 4.9 - XEP-0245: The /me Command complete + 1.2 5.0 XEP-0249: Direct MUC Invitations @@ -350,197 +344,196 @@ wontfix - XEP-0260: Jingle SOCKS5 Bytestreams Transport Method + Use HTTP filetransfer (XEP-0363) instead wontfix - XEP-0261: Jingle In-Band Bytestreams Transport Method + Use HTTP filetransfer (XEP-0363) instead complete + 1.0.1 4.5 - XEP-0280: Message Carbons complete + 1.0.0 4.7 - XEP-0286: Mobile Considerations on LTE Networks complete + 1.0.2 6.0 - XEP-0293: Jingle RTP Feedback Negotiation complete + 1.1.2 6.0 - XEP-0294: Jingle RTP Header Extensions Negotiation complete + 0.3 5.1.1 - XEP-0305: XMPP Quickstart complete + 1.2.1 4.8 - XEP-0308: Last Message Correction complete + 1.1.1 4.8 - XEP-0313: Message Archive Management complete + 1.0.2 4.7 - XEP-0319: Last User Interaction in Presence complete + 1.0.0 6.0 - XEP-0320: Use of DTLS-SRTP in Jingle Sessions complete + 1.0.0 4.8 - XEP-0333: Displayed Markers complete + 1.0.0 6.0 - XEP-0338: Jingle Grouping Framework complete + 1.0.1 6.0 - XEP-0339: Source-Specific Media Attributes in Jingle complete + 1.0.0 4.7 - XEP-0352: Client State Indication complete - 6.0 0.6.0 - XEP-0353: Jingle Message Initiation + 6.0 complete + 0.4.1 4.8 - XEP-0357: Push Notifications complete + 0.7.0 4.8 - XEP-0359: Unique and Stable Stanza IDs complete + 1.1.0 4.9 - XEP-0363: HTTP File Upload complete + 1.1.0 4.6 - XEP-0368: SRV records for XMPP over TLS planned - XEP-0374: OpenPGP for XMPP Instant Messaging wontfix - XEP-0377: Spam Reporting (via XEP-0191) + Spam reporting done via XEP-0191 partial + 0.3.3 4.9 - XEP-0379: Pre-Authenticated Roster Subscription + No automatic approval if server does not support subscription pre-approval; No checking of tokens, if server does not do so (XEP-0401) complete + 0.4.0 5.1 - XEP-0380: Explicit Message Encryption complete + 0.3.0 4.8 - XEP-0384: OMEMO Encryption @@ -554,92 +547,76 @@ complete - 6.0 1.0.1 - XEP-0388: Extensible SASL Profile + 6.0 wontfix - XEP-0390: Entity Capabilities 2.0 complete + 1.0.0 5.1 - XEP-0392: Consistent Color Generation wontfix - XEP-0396: Jingle Encrypted Transports - OMEMO - - - - - - wontfix - XEP-0397: Instant Stream Resumption + Use HTTP filetransfer (XEP-0363) instead complete - XEP-0398: User Avatar to vCard-Based Avatars Conversion + 1.0.0 + 6.0 + Used for MUC avatars complete - 6.0 0.5.0 - XEP-0401: Ad-hoc Account Invitation Generation + 6.0 complete - 5.4 1.1.4 - XEP-0402: PEP Native Bookmarks - - - - - - wontfix - XEP-0409: IM Routing-NG + 5.4 complete + 1.1.0 5.0 - XEP-0410: MUC Self-Ping (Schrödinger's Chat) planned - XEP-0420: Stanza Content Encryption partial - XEP-0423 XMPP Compliance Suites 2020 + 1.0.1 + Check this XEP to see what's missing @@ -648,7 +625,6 @@ complete 6.3 0.4.1 - XEP-0424: Message Retraction @@ -657,70 +633,70 @@ complete 6.3 0.3.0 - XEP-0425: Moderated Message Retraction complete + 0.4.1 6.0 - XEP-0440: SASL Channel-Binding Type Capability complete + 0.2.0 4.8 - XEP-0441: Message Archive Management Preferences + Only to automatically turn on archiving if possible (setting: always) complete + 0.2.0 5.2 - XEP-0445: Pre-Authenticated In-Band Registration partial + 0.1.0 5.0 - XEP-0454: OMEMO Media sharing + No support for embedded thumbnails complete + 0.3.0 6.0 - XEP-0474: SASL SCRAM Downgrade Protection complete + 0.1.0 6.0 - XEP-0480: SASL Upgrade Tasks planned - XEP-0484: Fast Authentication Streamlining Tokens - + complete + 0.1.0 5.0 - XEP-0486: MUC Avatars @@ -729,8 +705,7 @@ complete 6.3 0.1.0 - XEP-0490: Message Displayed Synchronization - + \ No newline at end of file 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"] } 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, diff --git a/scripts/build.sh b/scripts/build.sh index 9bcb101369..d0b31bdf3e 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -42,53 +42,55 @@ echo "* Installing macOS & iOS Pods *" echo "***************************************" pod install --repo-update -echo "" -echo "***************************" -echo "* Archiving macOS *" -echo "***************************" -xcrun xcodebuild \ - -workspace "Monal.xcworkspace" \ - -scheme "Monal" \ - -sdk macosx \ - -configuration $BUILD_TYPE \ - -destination 'generic/platform=macOS,variant=Mac Catalyst,name=Any Mac' \ - -archivePath "build/macos_$APP_NAME.xcarchive" \ - -allowProvisioningUpdates \ - archive \ - BUILD_LIBRARIES_FOR_DISTRIBUTION=YES \ - SUPPORTS_MACCATALYST=YES - -echo "" -echo "****************************" -echo "* Exporting macOS *" -echo "****************************" -# see: https://gist.github.com/cocoaNib/502900f24846eb17bb29 -# and: https://forums.developer.apple.com/thread/100065 -# and: for developer-id distribution (distribution *outside* of appstore) an developer-id certificate must be used for building -if [ ! -z ${EXPORT_OPTIONS_CATALYST_APPSTORE} ]; then - echo "***************************************" - echo "* Exporting AppStore macOS *" - echo "***************************************" - exportMacOS "$EXPORT_OPTIONS_CATALYST_APPSTORE" "$BUILD_TYPE" -fi - -if [ ! -z ${EXPORT_OPTIONS_CATALYST_APP_EXPORT} ]; then - echo "***********************************" - echo "* Exporting app macOS *" - echo "***********************************" - exportMacOS "$EXPORT_OPTIONS_CATALYST_APP_EXPORT" "$BUILD_TYPE" +if [ "$BUILD_SCHEME" != "Quicksy" ]; then + echo "" + echo "***************************" + echo "* Archiving macOS *" + echo "***************************" + xcrun xcodebuild \ + -workspace "Monal.xcworkspace" \ + -scheme "$BUILD_SCHEME" \ + -sdk macosx \ + -configuration $BUILD_TYPE \ + -destination 'generic/platform=macOS,variant=Mac Catalyst,name=Any Mac' \ + -archivePath "build/macos_$APP_NAME.xcarchive" \ + -allowProvisioningUpdates \ + archive \ + BUILD_LIBRARIES_FOR_DISTRIBUTION=YES \ + SUPPORTS_MACCATALYST=YES echo "" - echo "**************************" - echo "* Packing macOS zip *" - echo "**************************" - cd build/app - mkdir tar_release - mv "$APP_NAME.app" "tar_release/$APP_DIR" - cd tar_release - /usr/bin/ditto -c -k --sequesterRsrc --keepParent "$APP_DIR" "../$APP_NAME".zip - cd ../../.. - ls -l build/app + echo "****************************" + echo "* Exporting macOS *" + echo "****************************" + # see: https://gist.github.com/cocoaNib/502900f24846eb17bb29 + # and: https://forums.developer.apple.com/thread/100065 + # and: for developer-id distribution (distribution *outside* of appstore) an developer-id certificate must be used for building + if [ ! -z ${EXPORT_OPTIONS_CATALYST_APPSTORE} ]; then + echo "***************************************" + echo "* Exporting AppStore macOS *" + echo "***************************************" + exportMacOS "$EXPORT_OPTIONS_CATALYST_APPSTORE" "$BUILD_TYPE" + fi + + if [ ! -z ${EXPORT_OPTIONS_CATALYST_APP_EXPORT} ]; then + echo "***********************************" + echo "* Exporting app macOS *" + echo "***********************************" + exportMacOS "$EXPORT_OPTIONS_CATALYST_APP_EXPORT" "$BUILD_TYPE" + + echo "" + echo "**************************" + echo "* Packing macOS zip *" + echo "**************************" + cd build/app + mkdir tar_release + mv "$APP_NAME.app" "tar_release/$APP_DIR" + cd tar_release + /usr/bin/ditto -c -k --sequesterRsrc --keepParent "$APP_DIR" "../$APP_NAME".zip + cd ../../.. + ls -l build/app + fi fi echo "" @@ -97,7 +99,7 @@ echo "* Archiving iOS *" echo "*************************" xcrun xcodebuild \ -workspace "Monal.xcworkspace" \ - -scheme "Monal" \ + -scheme "$BUILD_SCHEME" \ -sdk iphoneos \ -configuration $BUILD_TYPE \ -archivePath "build/ios_$APP_NAME.xcarchive" \ diff --git a/scripts/exportOptions/Quicksy_Stable_iOS_ExportOptions.plist b/scripts/exportOptions/Quicksy_Stable_iOS_ExportOptions.plist new file mode 100644 index 0000000000..a2dab0a13a --- /dev/null +++ b/scripts/exportOptions/Quicksy_Stable_iOS_ExportOptions.plist @@ -0,0 +1,16 @@ + + + + + method + app-store + compileBitcode + + uploadBitcode + + signingStyle + automatic + teamID + S8D843U34Y + + \ No newline at end of file diff --git a/scripts/itu_pdf_to_swift.py b/scripts/itu_pdf_to_swift.py new file mode 100755 index 0000000000..b7d9c9f029 --- /dev/null +++ b/scripts/itu_pdf_to_swift.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python3 +import requests +import io +from pypdf import PdfReader +import re +import logging + +logging.basicConfig(level=logging.DEBUG, format="%(asctime)s [%(levelname)-7s] %(name)s {%(threadName)s} %(filename)s:%(lineno)d: %(message)s") +logger = logging.getLogger(__name__) + +class Quicksy_Country: + def __init__(self, name, code, pattern): + self.name = name + self.code = code + self.pattern = pattern + + def __repr__(self): + return f"Quicksy_Country(name: NSLocalizedString(\"{self.name}\", comment:\"quicksy country\"), code: \"{self.code}\", pattern: \"{self.pattern}\") ," + +def parse_pdf(pdf_data): + country_regex = re.compile(r'^(?P[^0-9]+)[ ]{32}(?P[0-9]+)[ ]{32}(?P.+)[ ]{32}(?P.+)[ ]{32}(?P.+ digits)[ ]{32}(?P.*)$') + country_end_regex = re.compile(r'^(?P.*)([ ]{32}(?P.+))?$') + countries = {} + pdf = PdfReader(io.BytesIO(pdf_data)) + pagenum = 0 + last_entry = None + for page in pdf.pages: + pagenum += 1 + countries[pagenum] = [] + logger.info(f"Starting to analyze page {pagenum}...") + text = page.extract_text(extraction_mode="layout", layout_mode_space_vertically=False) + if text and "Country/geographical area" in text and "Country" in text and "International" in text and "National" in text and "National (Significant)" in text and "UTC/DST" in text and "Note" in text: + for line in text.split("\n"): + #this is faster than having a "{128,} in the compiled country_regex + match = country_regex.match(re.sub("[ ]{128,}", " "*32, line)) + if match == None: + # check if this is just a linebreak in the country name and append the value to the previous country + if re.sub("[ ]{128,}", " "*32, line) == line.strip() and last_entry != None and "Annex to ITU" not in line: + logger.debug(f"Adding to last country name: {line=}") + countries[pagenum][last_entry].name += f" {line.strip()}" + else: + last_entry = None # don't append line continuations of non-real countries to a real country + else: + match = match.groupdict() | {"dst": None, "notes": None} + if match["end"] and match["end"].strip() != "": + end_splitting = match["end"].split(" "*32) + if len(end_splitting) >= 1: + match["dst"] = end_splitting[0] + if len(end_splitting) >= 2: + match["notes"] = end_splitting[1] + match = {key: (value.strip() if value != None else None) for key, value in match.items()} + # logger.debug("****************") + # logger.debug(f"{match['country'] = }") + # logger.debug(f"{match['code'] = }") + # logger.debug(f"{match['international_prefix'] = }") + # logger.debug(f"{match['national_prefix'] = }") + # logger.debug(f"{match['format'] = }") + # logger.debug(f"{match['dst'] = }") + # logger.debug(f"{match['notes'] = }") + + if match["dst"] == None: # all real countries have a dst entry + last_entry = None # don't append line continuations of non-real countries to a real country + else: + country_code = f"+{match['code']}" + pattern = subpattern_matchers(match['format'], True) + superpattern = matcher(pattern, r"(\([0-9/]+\))[ ]*\+[ ]*(.+)[ ]+digits", match['format'], lambda result: result) + if pattern == None and superpattern != None: + #logger.debug(f"Trying superpattern: '{match['format']}' --> '{superpattern.group(1)}' ## '{superpattern.group(2)}'") + subpattern = subpattern_matchers(superpattern.group(2), False) + if subpattern != None: + pattern = re.sub("/", "|", superpattern.group(1)) + subpattern + if pattern == None: + logger.warning(f"Unknown format description for {match['country']} ({country_code}): '{match['format']}'") + pattern = "[0-9]*" + country = Quicksy_Country(match['country'], country_code, f"^{pattern}$") + countries[pagenum].append(country) + last_entry = len(countries[pagenum]) - 1 + logger.info(f"Page {pagenum}: Found {len(countries[pagenum])} countries so far...") + + return [c for cs in countries.values() for c in cs] + +def matcher(previous_result, regex, text, closure): + if previous_result != None: + return previous_result + matches = re.match(regex, text) + if matches == None: + return None + else: + return closure(matches) + +def subpattern_matchers(text, should_end_with_unit): + if should_end_with_unit: + if text[-6:] != "digits": + logger.error(f"should_end_with_unit set but not ending in 'digits': {text[-6:] = }") + return None + text = text[:-6] + + def subdef(result): + retval = f"[0-9]{{" + grp1 = result.group(1) if result.group(1) != "up" else "1" + retval += f"{grp1}" + if result.group(3) != None: + retval += f",{result.group(3)}" + retval += f"}}" + return retval + pattern = [] + parts = [x.strip() for x in text.split(",")] + for part in parts: + result = matcher(None, r"(up|[0-9]+)([ ]*to[ ]*([0-9]+)[ ]*)?", part, subdef) + #logger.debug(f"{part=} --> {result=}") + if result != None: + pattern.append(result) + if len(pattern) == 0: + return None + return "(" + "|".join(pattern) + ")" + +logger.info("Downloading PDF...") +response = requests.get("https://www.itu.int/dms_pub/itu-t/opb/sp/T-SP-E.164C-2011-PDF-E.pdf") +logger.info("Parsing PDF...") +countries = parse_pdf(response.content) +print("""// This file was automatically generated by scripts/itu_pdf_to_swift.py +// Please run this python script again to update this file +// Example ../scripts/itu_pdf_to_swift.py > Classes/CountryCodes.swift + +public struct Quicksy_Country: Identifiable, Hashable { + public let id = UUID() + public let name: String + public let code: String + public let pattern: String +} +""") +print(f"public let COUNTRY_CODES: [Quicksy_Country] = [") +for country in countries: + print(f" {country}") +print(f"]") 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://api.github.com/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) diff --git a/scripts/requirements.txt b/scripts/requirements.txt new file mode 100644 index 0000000000..8e809ad01a --- /dev/null +++ b/scripts/requirements.txt @@ -0,0 +1 @@ +PyPDF @ git+https://github.com/py-pdf/pypdf@4.3.1 \ No newline at end of file diff --git a/scripts/set_version_number.sh b/scripts/set_version_number.sh index 03d0a4b3c7..e9868a6483 100755 --- a/scripts/set_version_number.sh +++ b/scripts/set_version_number.sh @@ -6,16 +6,16 @@ set -e cd Monal echo "" -echo "*******************************************" -echo "* Reading buildNumber *" -echo "*******************************************" -buildNumber=$(git tag --sort="v:refname" |grep "Build_iOS" | tail -n1 | sed 's/Build_iOS_//g') +echo "***************************************************" +echo "* Setting buildNumber to $buildNumber and version to $buildVersion *" +echo "***************************************************" -echo "" -echo "*******************************************" -echo "* Setting buildNumber to $buildNumber *" -echo "*******************************************" +set -x /usr/libexec/PlistBuddy -c "Set :CFBundleVersion $buildNumber" "NotificationService/Info.plist" /usr/libexec/PlistBuddy -c "Set :CFBundleVersion $buildNumber" "shareSheet-iOS/Info.plist" -/usr/libexec/PlistBuddy -c "Set :CFBundleVersion $buildNumber" "Monal-Info.plist" \ No newline at end of file +/usr/libexec/PlistBuddy -c "Set :CFBundleVersion $buildNumber" "$APP_NAME-Info.plist" + +/usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $buildVersion" "NotificationService/Info.plist" +/usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $buildVersion" "shareSheet-iOS/Info.plist" +/usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $buildVersion" "$APP_NAME-Info.plist"