diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml new file mode 100644 index 0000000000..dcb9f0a766 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -0,0 +1,74 @@ +name: Bug report for the Element Android app +description: Report any issues that you have found with the Element app. Please [check open issues](https://github.com/vector-im/element-android/issues) first, in case it has already been reported. +labels: [T-Defect] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this bug report! + + Please report security issues by email to security@matrix.org + - type: textarea + id: reproduction-steps + attributes: + label: Steps to reproduce + description: Please attach screenshots, videos or logs if you can. + placeholder: Tell us what you see! + value: | + 1. Where are you starting? What can you see? + 2. What do you click? + 3. More steps… + validations: + required: true + - type: textarea + id: result + attributes: + label: What happened? + placeholder: Tell us what went wrong + value: | + ### What did you expect? + + ### What happened? + validations: + required: true + - type: input + id: device + attributes: + label: Your phone model + placeholder: e.g. Samsung S6 + validations: + required: false + - type: input + id: os + attributes: + label: Operating system version + placeholder: e.g. Android 10.0 + validations: + required: false + - type: input + id: version + attributes: + label: Application version and app store + description: You can find the version information in Settings -> Help & About. + placeholder: e.g. Element version 1.7.34, olm version 3.2.3 from F-Droid + validations: + required: false + - type: input + id: homeserver + attributes: + label: Homeserver + description: Which server is your account registered on? + placeholder: e.g. matrix.org + validations: + required: false + - type: dropdown + id: rageshake + attributes: + label: Have you submitted a rageshake? + description: | + Did you know that you can shake your phone to submit logs for this issue? Trigger the defect, then shake your phone and you will see a popup asking if you would like to open the bug report screen. Click YES, and describe the issue, mentioning that you have also filed a bug. Submit the report to send anonymous logs to the developers. + options: + - 'Yes' + - 'No' + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index d7c3506fa0..0000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve Element -title: '' -labels: '' -assignees: '' - ---- - -#### Describe the bug -A clear and concise description of what the bug is. - -#### To Reproduce -Steps to reproduce the behavior: -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error - -#### Expected behavior -A clear and concise description of what you expected to happen. - -#### Screenshots -If applicable, add screenshots to help explain your problem. - -#### Smartphone (please complete the following information): - - Device: [e.g. Samsung S6] - - OS: [e.g. Android 6.0] - -#### Additional context - - App version and store [e.g. 1.0.0 - F-Droid] - - Homeserver: [e.g. matrix.org] - -Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/enhancement.yml b/.github/ISSUE_TEMPLATE/enhancement.yml new file mode 100644 index 0000000000..5d9cfb3c88 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/enhancement.yml @@ -0,0 +1,36 @@ +name: Enhancement request +description: Do you have a suggestion or feature request? +labels: [T-Enhancement] +body: + - type: markdown + attributes: + value: | + Thank you for taking the time to propose a new feature or make a suggestion. + - type: textarea + id: usecase + attributes: + label: Your use case + description: What would you like to be able to do? Please feel welcome to include screenshots or mock ups. + placeholder: Tell us what you would like to do! + value: | + #### What would you like to do? + + #### Why would you like to do it? + + #### How would you like to achieve it? + validations: + required: true + - type: textarea + id: alternative + attributes: + label: Have you considered any alternatives? + placeholder: A clear and concise description of any alternative solutions or features you've considered. + validations: + required: false + - type: textarea + id: additional-context + attributes: + label: Additional context + placeholder: Is there anything else you'd like to add? + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index da96d461c5..0000000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -name: Feature request -about: Suggest an idea for this project -title: '' -labels: type:suggestion -assignees: '' - ---- - -#### Is your feature request related to a problem? Please describe. -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] - -#### Describe the solution you'd like. -A clear and concise description of what you want to happen. - -#### Describe alternatives you've considered. -A clear and concise description of any alternative solutions or features you've considered. - -#### Additional context -Add any other context or screenshots about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/matrix-sdk.md b/.github/ISSUE_TEMPLATE/matrix-sdk.md deleted file mode 100644 index 30f705a575..0000000000 --- a/.github/ISSUE_TEMPLATE/matrix-sdk.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -name: Matrix SDK -about: Report issue or ask for a feature regarding the Android Matrix SDK -title: "[SDK] " -labels: matrix-sdk -assignees: '' - ---- - - diff --git a/.github/ISSUE_TEMPLATE/matrix-sdk.yml b/.github/ISSUE_TEMPLATE/matrix-sdk.yml new file mode 100644 index 0000000000..4033423dd5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/matrix-sdk.yml @@ -0,0 +1,20 @@ +name: Matrix SDK bug or enhancement +description: Report issue or ask for a feature in the [Android Matrix SDK](https://github.com/matrix-org/matrix-android-sdk2) +title: "[SDK] " +labels: [matrix-sdk] + +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this issue! + + Please report security issues by email to security@matrix.org + - type: textarea + id: description + attributes: + label: Description + description: Report issue or ask for a feature in the [Android Matrix SDK](https://github.com/matrix-org/matrix-android-sdk2) + placeholder: This issue template should be used by third party application maintainers, to report a bug or to request a feature on the SDK module of the Element Android application. + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/release.md b/.github/ISSUE_TEMPLATE/release.md deleted file mode 100644 index 154e93286c..0000000000 --- a/.github/ISSUE_TEMPLATE/release.md +++ /dev/null @@ -1,84 +0,0 @@ ---- -name: Release -about: Checklist for each release. To be used by the core team only. -title: "[Release] Element Android v" -labels: "\U0001F680 Release" -assignees: bmarty - ---- - -For the example, we are releasing the version 1.1.10. Delete this line and replace 1.1.10 with the version in the issue content. - -### Before the release - -- [ ] Weblate sync, fix lint issue if any (in a dedicated PR) -- [ ] Check the update of the store descriptions (using Google Translate if necessary) to ensure that the changes are acceptable to be published to the stores. -- [ ] Run the script `./tools/release/pushPlayStoreMetaData.sh`. You can check in the GooglePlay console the Activity log to check the effect. - -### Do the release - -- [ ] Create release with gitflow, branch name `release/1.1.10` -- [ ] Run `./tools/import_emojis.py` and commit the change if any. -- [ ] Run `./tools/import_sas_strings.py` and commit the change if any. If there is no change since a while, ping Travis -- [ ] Check the crashes from the PlayStore -- [ ] Check the rageshake with the current dev version: https://github.com/matrix-org/element-android-rageshakes/labels/1.1.10-dev -- [ ] Run the integration test, and especially `UiAllScreensSanityTest.allScreensTest()` -- [ ] Create an account on matrix.org -- [ ] Run towncrier: `towncrier --version v1.1.10 --draft` (remove `--draft` do write the file CHANGES.md) -- [ ] Add file for fastlane under ./fastlane/metadata/android/en-US/changelogs -- [ ] Push the branch and start a draft PR (will not be merged), to check that the CI is happy with all the changes. -- [ ] Finish release with gitflow, delete the draft PR -- [ ] Push `main` and the new tag `v1.1.10` to origin -- [ ] Checkout `develop` -- [ ] Increase version in `./vector/build.gradle` -- [ ] Commit and push `develop` -- [ ] Wait for [Buildkite](https://buildkite.com/matrix-dot-org/element-android/builds?branch=main) to build the `main` branch. -- [ ] Run the script `~/scripts/releaseElement.sh`. It will download the APKs from Buildkite check them and sign them. -- [ ] Install the APK on your phone to check that the upgrade went well (no init sync, etc.) -- [ ] Create a new beta release on the GooglePlay console and upload the 4 signed Apks. -- [ ] Check that the version codes are correct -- [ ] Copy the fastlane change to the GooglePlay console in the section en-GB. -- [ ] Push to beta release to 100% of the users -- [ ] Create the release on gitHub [from the tag](https://github.com/vector-im/element-android/tags), copy paste the block from the file CHANGES.md -- [ ] Add the 4 signed APKs to the GitHub release -- [ ] Ping the Android Internal room -- [ ] Add an entry in the internal diary - -### Once Live on PlayStore - -- [ ] Ping the Android public room and update its topic - -### After at least 2 days - -- [ ] Check the [rageshakes](https://github.com/matrix-org/element-android-rageshakes/issues) -- [ ] Check the crash reports on the GooglePlay console -- [ ] Check the Android Element room for any reported issues on the new version -- [ ] If all is OK, push to production and notify Markus (Bubu) to release the F-Droid version -- [ ] Ping the Android public room and update its topic with the new available version - -### Android SDK2 - -- [ ] Checkout the `main` branch on Element Android project - -#### On the SDK2 project - -https://github.com/matrix-org/matrix-android-sdk2 - -- [ ] Create a release with GitFlow -- [ ] Update the files `./build.gradle` and `./gradle/gradle-wrapper.properties` manually, to use the latest version for the dependency. You can get inspired by the same files on Element Android project. -- [ ] Run the script `./tools/import_from_element.sh` -- [ ] Update the version in `./matrix-sdk-android/build.gradle` and let the script finish to build the library -- [ ] Update the file `CHANGES.md` -- [ ] Finish the release using GitFlow -- [ ] Create the release on GitHub from [the tag](https://github.com/matrix-org/matrix-android-sdk2/tags) -- [ ] Upload the AAR on the GitHub release - -### Android SDK2 sample - -https://github.com/matrix-org/matrix-android-sdk2-sample - -- [ ] Update the dependency to the new version of the SDK2. Jitpack will have to build the AAR, it can take a few minutes. You can check status on https://jitpack.io/#matrix-org/matrix-android-sdk2 -- [ ] Build and run the sample, you may have to fix some API break -- [ ] Commit and push directly on `main` - - diff --git a/.github/ISSUE_TEMPLATE/release.yml b/.github/ISSUE_TEMPLATE/release.yml new file mode 100644 index 0000000000..903c05c5d3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/release.yml @@ -0,0 +1,106 @@ +name: Release checklist +description: Checklist for each release. This template is only for the core team. +title: "[Release] Element Android v" +labels: [🚀 Release] +assignees: + - bmarty + +body: + - type: textarea + id: checklist + attributes: + label: Release checklist + description: For the template example, we are releasing the version 1.1.10. Replace 1.1.10 with the version in the issue body. + placeholder: | + If you are reading this, you have deleted the content of the release template: undo the deletion or start again. + value: | + ### Before the release + + - [ ] Weblate sync, fix lint issue if any (in a dedicated PR) + - [ ] Check the update of the store descriptions (using Google Translate if necessary) to ensure that the changes are acceptable to be published to the stores. + - [ ] Run the script `./tools/release/pushPlayStoreMetaData.sh`. You can check in the GooglePlay console the Activity log to check the effect. + + ### Do the release + + - [ ] Create release with gitflow, branch name `release/1.1.10` + - [ ] Run `./tools/import_emojis.py` and commit the change if any. + - [ ] Run `./tools/import_sas_strings.py` and commit the change if any. If there is no change since a while, ping Travis + - [ ] Check the crashes from the PlayStore + - [ ] Check the rageshake with the current dev version: https://github.com/matrix-org/element-android-rageshakes/labels/1.1.10-dev + - [ ] Run the integration test, and especially `UiAllScreensSanityTest.allScreensTest()` + - [ ] Create an account on matrix.org + - [ ] Run towncrier: `towncrier --version v1.1.10 --draft` (remove `--draft` do write the file CHANGES.md) + - [ ] Add file for fastlane under ./fastlane/metadata/android/en-US/changelogs + - [ ] Push the branch and start a draft PR (will not be merged), to check that the CI is happy with all the changes. + - [ ] Finish release with gitflow, delete the draft PR + - [ ] Push `main` and the new tag `v1.1.10` to origin + - [ ] Checkout `develop` + - [ ] Increase version in `./vector/build.gradle` + - [ ] Change the value of SDK_VERSION in the file `./matrix-sdk-android/build.gradle` + - [ ] Commit and push `develop` + - [ ] Wait for [Buildkite](https://buildkite.com/matrix-dot-org/element-android/builds?branch=main) to build the `main` branch. + - [ ] Run the script `~/scripts/releaseElement.sh`. It will download the APKs from Buildkite check them and sign them. + - [ ] Install the APK on your phone to check that the upgrade went well (no init sync, etc.) + - [ ] Create a new beta release on the GooglePlay console and upload the 4 signed Apks. + - [ ] Check that the version codes are correct + - [ ] Copy the fastlane change to the GooglePlay console in the section en-GB. + - [ ] Push to beta release to 100% of the users + - [ ] Create the release on gitHub [from the tag](https://github.com/vector-im/element-android/tags), copy paste the block from the file CHANGES.md + - [ ] Add the 4 signed APKs to the GitHub release + - [ ] Ping the Android Internal room + - [ ] Add an entry in the internal diary + + ### Once Live on PlayStore + + - [ ] Ping the Android public room and update its topic + + ### After at least 2 days + + - [ ] Check the [rageshakes](https://github.com/matrix-org/element-android-rageshakes/issues) + - [ ] Check the crash reports on the GooglePlay console + - [ ] Check the Android Element room for any reported issues on the new version + - [ ] If all is OK, push to production and notify Markus (Bubu) to release the F-Droid version + - [ ] Ping the Android public room and update its topic with the new available version + + ### Android SDK2 + + - [ ] Checkout the `main` branch on Element Android project + + #### On the SDK2 project + + https://github.com/matrix-org/matrix-android-sdk2 + + - [ ] Create a release with GitFlow + - [ ] Update the value of VERSION_NAME in the file gradle.properties + - [ ] Update the files `./build.gradle` and `./gradle/gradle-wrapper.properties` manually, to use the latest version for the dependency. You can get inspired by the same files on Element Android project. + - [ ] Run the script `./tools/import_from_element.sh` + - [ ] Check the diff in the file `./matrix-sdk-android/build.gradle` and restore what may have been erased (in particular the line `apply plugin: "com.vanniktech.maven.publish"` and the line about the version) + - [ ] Let the script finish to build the library + - [ ] Update the file `CHANGES.md` + - [ ] Finish the release using GitFlow + - [ ] Push the branch `main`, the new tag and the branch `develop` to origin + + ##### Release on MavenCentral + + - [ ] Run the command `./gradlew publish --no-daemon --no-parallel`. You'll need some non-public element to do so + - [ ] Connect to https://s01.oss.sonatype.org + - [ ] Click on Staging Repositories and check the the files have been uploaded + - [ ] Click on close + - [ ] Wait (check Activity tab until step "Repository closed" is displayed) + - [ ] Click on release. The staging repository will disappear + - [ ] Check that the release is available in https://repo1.maven.org/maven2/org/matrix/android/matrix-android-sdk2/ (it can take a few minutes) + + ##### Release on GitHub + + - [ ] Create the release on GitHub from [the tag](https://github.com/matrix-org/matrix-android-sdk2/tags) + - [ ] Upload the AAR on the GitHub release + + ### Android SDK2 sample + + https://github.com/matrix-org/matrix-android-sdk2-sample + + - [ ] Update the dependency to the new version of the SDK2. It can take some time for MavenCentral to make the librarie available. You can check status on https://repo1.maven.org/maven2/org/matrix/android/matrix-android-sdk2/ + - [ ] Build and run the sample, you may have to fix some API break + - [ ] Commit and push directly on `main` + validations: + required: true diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 8fbc5602fe..2048b823f0 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,10 +1,16 @@ ### Pull Request Checklist - + - [ ] Changes has been tested on an Android device or Android emulator with API 21 - [ ] UI change has been tested on both light and dark themes +- [ ] Accessibility has been taken into account. See https://github.com/vector-im/element-android/blob/develop/CONTRIBUTING.md#accessibility - [ ] Pull request is based on the develop branch - [ ] Pull request includes a new file under ./changelog.d. See https://github.com/vector-im/element-android/blob/develop/CONTRIBUTING.md#changelog - [ ] Pull request includes screenshots or videos if containing UI changes - [ ] Pull request includes a [sign off](https://github.com/matrix-org/synapse/blob/master/CONTRIBUTING.md#sign-off) +- [ ] You've made a self review of your PR +- [ ] If you have modified the screen flow, or added new screens to the application, you have updated the test [UiAllScreensSanityTest.allScreensTest()](https://github.com/vector-im/element-android/blob/main/vector/src/androidTest/java/im/vector/app/ui/UiAllScreensSanityTest.kt#L73) \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 85148a2632..91dc6d830b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,6 +5,12 @@ on: push: branches: [ main, develop ] +# Enrich gradle.properties for CI/CD +env: + CI_GRADLE_ARG_PROPERTIES: > + -Porg.gradle.jvmargs=-Xmx2g + -Porg.gradle.parallel=false + jobs: debug: name: Build debug APKs (${{ matrix.target }}) @@ -25,7 +31,7 @@ jobs: restore-keys: | ${{ runner.os }}-gradle- - name: Assemble ${{ matrix.target }} debug apk - run: ./gradlew assemble${{ matrix.target }}Debug --stacktrace + run: ./gradlew assemble${{ matrix.target }}Debug $CI_GRADLE_ARG_PROPERTIES --stacktrace - name: Upload ${{ matrix.target }} debug APKs uses: actions/upload-artifact@v2 with: @@ -48,7 +54,7 @@ jobs: restore-keys: | ${{ runner.os }}-gradle- - name: Assemble GPlay unsigned apk - run: ./gradlew clean assembleGplayRelease --stacktrace + run: ./gradlew clean assembleGplayRelease $CI_GRADLE_ARG_PROPERTIES --stacktrace - name: Upload Gplay unsigned APKs uses: actions/upload-artifact@v2 with: diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index cb6f1b0e48..c18ca69fde 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -5,15 +5,45 @@ on: push: branches: [ main, develop ] +# Enrich gradle.properties for CI/CD +env: + CI_GRADLE_ARG_PROPERTIES: > + -Porg.gradle.jvmargs=-Xmx2g + -Porg.gradle.parallel=false + jobs: + # Temporary add build of Android tests, which cannot be run on the CI right now, but they need to at least compile + # So it will be mandatory for this action to be successful on every PRs + compile-android-test: + name: Compile Android tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/cache@v2 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + - name: Compile Android tests + run: ./gradlew clean assembleAndroidTest $CI_GRADLE_ARG_PROPERTIES --stacktrace -PallWarningsAsErrors=false + integration-tests: name: Integration Tests (Synapse) runs-on: ubuntu-latest strategy: + fail-fast: false matrix: - api-level: [21, 30] + api-level: [28] steps: - uses: actions/checkout@v2 + - uses: gradle/wrapper-validation-action@v1 + - uses: actions/setup-java@v2 + with: + distribution: 'adopt' + java-version: 11 - name: Set up Python 3.8 uses: actions/setup-python@v2 with: @@ -45,5 +75,12 @@ jobs: uses: reactivecircus/android-emulator-runner@v2 with: api-level: ${{ matrix.api-level }} + #arch: x86_64 + #disable-animations: true # script: ./gradlew -PallWarningsAsErrors=false vector:connectedAndroidTest matrix-sdk-android:connectedAndroidTest - script: ./gradlew -PallWarningsAsErrors=false connectedCheck + arch: x86 + profile: Nexus 5X + force-avd-creation: false + emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none + emulator-build: 7425822 + script: ./gradlew $CI_GRADLE_ARG_PROPERTIES -PallWarningsAsErrors=false connectedCheck --stacktrace diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index a65e6b5dee..0f11915258 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -24,6 +24,7 @@ jobs: curl -sSLO https://github.com/pinterest/ktlint/releases/download/0.36.0/ktlint && chmod a+x ktlint ./ktlint --android --experimental -v +# Lint for main module and all the other modules android-lint: name: Android Linter runs-on: ubuntu-latest @@ -37,14 +38,16 @@ jobs: key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} restore-keys: | ${{ runner.os }}-gradle- - - name: Lint analysis of the SDK - run: ./gradlew clean :matrix-sdk-android:lintRelease --stacktrace + - name: Lint analysis + run: ./gradlew clean :vector:lint --stacktrace - name: Upload reports uses: actions/upload-artifact@v2 with: - name: linting-report-android-sdk - path: matrix-sdk-android/build/reports/*.* + name: lint-report + path: | + vector/build/reports/*.* +# Lint for Gplay and Fdroid release APK apk-lint: name: Lint APK (${{ matrix.target }}) runs-on: ubuntu-latest @@ -69,6 +72,6 @@ jobs: uses: actions/upload-artifact@v2 if: always() with: - name: release-debug-linting-report-${{ matrix.target }} + name: release-lint-report-${{ matrix.target }} path: | vector/build/reports/*.* diff --git a/.github/workflows/sanity_test.yml b/.github/workflows/sanity_test.yml new file mode 100644 index 0000000000..3ab0017ce2 --- /dev/null +++ b/.github/workflows/sanity_test.yml @@ -0,0 +1,56 @@ +name: Sanity Test + +on: + pull_request: { } + push: + branches: [ main, develop ] + +# Enrich gradle.properties for CI/CD +env: + CI_GRADLE_ARG_PROPERTIES: > + -Porg.gradle.jvmargs=-Xmx2g + -Porg.gradle.parallel=false + +jobs: + integration-tests: + name: Sanity Tests (Synapse) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + api-level: [28] + steps: + - uses: actions/checkout@v2 + - name: Set up Python 3.8 + uses: actions/setup-python@v2 + with: + python-version: 3.8 + - name: Cache pip + uses: actions/cache@v2 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip + restore-keys: | + ${{ runner.os }}-pip- + ${{ runner.os }}- + - uses: actions/cache@v2 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + - name: Start synapse server + run: | + python3 -m venv .synapse + source .synapse/bin/activate + pip install synapse matrix-synapse + curl -sL https://raw.githubusercontent.com/matrix-org/synapse/develop/demo/start.sh --no-rate-limit \ + | sed s/127.0.0.1/0.0.0.0/g | bash + - name: Run sanity tests on API ${{ matrix.api-level }} + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: ${{ matrix.api-level }} + script: ./gradlew $CI_GRADLE_ARG_PROPERTIES -PallWarningsAsErrors=false connectedGplayDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=im.vector.app.ui.UiAllScreensSanityTest + diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6e51368ce5..50195638de 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -5,6 +5,12 @@ on: push: branches: [main, develop] +# Enrich gradle.properties for CI/CD +env: + CI_GRADLE_ARG_PROPERTIES: > + -Porg.gradle.jvmargs=-Xmx2g + -Porg.gradle.parallel=false + jobs: unit-tests: name: Run Unit Tests @@ -20,4 +26,11 @@ jobs: restore-keys: | ${{ runner.os }}-gradle- - name: Run unit tests - run: ./gradlew clean test --stacktrace -PallWarningsAsErrors=false + run: ./gradlew clean test $CI_GRADLE_ARG_PROPERTIES -PallWarningsAsErrors=false --stacktrace + - name: Publish Unit Test Results + uses: EnricoMi/publish-unit-test-result-action@v1 + if: always() && + github.event.sender.login != 'dependabot[bot]' && + ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository ) + with: + files: ./**/build/test-results/**/*.xml diff --git a/.gitignore b/.gitignore index 04d1b6fe06..935a3fc329 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ .idea/*.xml .DS_Store /build +/benchmark-out /captures .externalNativeBuild diff --git a/CHANGES.md b/CHANGES.md index 640d56a9fd..8d4899e6fb 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,113 @@ +Changes in Element v1.3.1 (2021-09-29) +====================================== + +Bugfixes 🐛 +---------- + - Verifying exported E2E keys to provide user feedback when the output is malformed ([#4082](https://github.com/vector-im/element-android/issues/4082)) + - Fix settings crash when accelerometer not available ([#4103](https://github.com/vector-im/element-android/issues/4103)) + - Crash while rendering failed message warning ([#4110](https://github.com/vector-im/element-android/issues/4110)) + + +Changes in Element v1.3.0 (2021-09-27) +====================================== + +Features ✨ +---------- + - Spaces! + - Adds email notification registration to Settings ([#2243](https://github.com/vector-im/element-android/issues/2243)) + - Spaces | M3.23 Invite by email in create private space flow ([#3678](https://github.com/vector-im/element-android/issues/3678)) + - Improve space invite bottom sheet ([#4057](https://github.com/vector-im/element-android/issues/4057)) + - Allow to also leave rooms when leaving a space ([#3692](https://github.com/vector-im/element-android/issues/3692)) + - Better expose adding spaces as Subspaces ([#3752](https://github.com/vector-im/element-android/issues/3752)) + - Push and syncs: add debug info on room list and on room detail screen and improves the log format. ([#4046](https://github.com/vector-im/element-android/issues/4046)) + +Bugfixes 🐛 +---------- + - Remove the "Teammate spaces aren't quite ready" bottom sheet ([#3945](https://github.com/vector-im/element-android/issues/3945)) + - Restricted Room previews aren't working ([#3946](https://github.com/vector-im/element-android/issues/3946)) + - A removed room from a space can't be re-added as it won't be shown in add-room ([#3947](https://github.com/vector-im/element-android/issues/3947)) + - "Non-Admin" user able to invite others to Private Space (by default) ([#3951](https://github.com/vector-im/element-android/issues/3951)) + - Kick user dialog for spaces talks about rooms ([#3956](https://github.com/vector-im/element-android/issues/3956)) + - Messages are displayed as unable to decrypt then decrypted a few seconds later ([#4011](https://github.com/vector-im/element-android/issues/4011)) + - Fix DTMF not working ([#4015](https://github.com/vector-im/element-android/issues/4015)) + - Fix sticky end call notification ([#4019](https://github.com/vector-im/element-android/issues/4019)) + - Fix call screen stuck with some hanging up scenarios ([#4026](https://github.com/vector-im/element-android/issues/4026)) + - Fix other call not always refreshed when ended ([#4028](https://github.com/vector-im/element-android/issues/4028)) + - Private space invite bottomsheet only offering inviting by username not by email ([#4042](https://github.com/vector-im/element-android/issues/4042)) + - Spaces invitation system notifications don't take me to the join space toast ([#4043](https://github.com/vector-im/element-android/issues/4043)) + - Space Invites are not lighting up the drawer menu ([#4059](https://github.com/vector-im/element-android/issues/4059)) + - MessageActionsBottomSheet not being shown on local echos ([#4068](https://github.com/vector-im/element-android/issues/4068)) + +SDK API changes ⚠️ +------------------ + - InitialSyncProgressService has been renamed to SyncStatusService and its function getInitialSyncProgressStatus() has been renamed to getSyncStatusLive() ([#4046](https://github.com/vector-im/element-android/issues/4046)) + +Other changes +------------- + - Better support for Sdk2 version. Also slight change in the default user agent: `MatrixAndroidSDK_X` is replaced by `MatrixAndroidSdk2` ([#3994](https://github.com/vector-im/element-android/issues/3994)) + - Introduces ConferenceEvent to abstract usage of Jitsi BroadcastEvent class. ([#4014](https://github.com/vector-im/element-android/issues/4014)) + - Improve performances on RoomDetail screen ([#4065](https://github.com/vector-im/element-android/issues/4065)) + + +Changes in Element v1.2.2 (2021-09-13) +====================================== + +Bugfixes 🐛 +---------- + +- Fix a security issue with message key sharing. See https://matrix.org/blog/2021/09/13/vulnerability-disclosure-key-sharing for details. + + +Changes in Element v1.2.1 (2021-09-08) +====================================== + +Features ✨ +---------- + - Support Android 11 Conversation features ([#1809](https://github.com/vector-im/element-android/issues/1809)) + - Introduces AutoAcceptInvites which can be enabled at compile time. ([#3531](https://github.com/vector-im/element-android/issues/3531)) + - New call designs ([#3599](https://github.com/vector-im/element-android/issues/3599)) + - Restricted Join Rule | Inform admins of new option ([#3631](https://github.com/vector-im/element-android/issues/3631)) + - Mention and Keyword Notification Settings: Turn on/off keyword notifications and edit keywords. ([#3650](https://github.com/vector-im/element-android/issues/3650)) + - Support accept 3pid invite when email is not bound to account ([#3691](https://github.com/vector-im/element-android/issues/3691)) + - Space summary pagination ([#3693](https://github.com/vector-im/element-android/issues/3693)) + - Update Email invite to be aware of spaces ([#3695](https://github.com/vector-im/element-android/issues/3695)) + - M11.12 Spaces | Default to 'Home' in settings ([#3754](https://github.com/vector-im/element-android/issues/3754)) + - Call: show dialog for some ended reasons. ([#3853](https://github.com/vector-im/element-android/issues/3853)) + - Add expired account error code in the matrix SDK ([#3900](https://github.com/vector-im/element-android/issues/3900)) + - Add password errors in the matrix SDK ([#3927](https://github.com/vector-im/element-android/issues/3927)) + - Upgrade AGP to 7.0.2. + When compiling using command line, make sure to use the JDK 11 by adding for instance `-Dorg.gradle.java.home=/Applications/Android\ Studio\ Preview.app/Contents/jre/Contents/Home` or by setting JAVA_HOME. ([#3954](https://github.com/vector-im/element-android/issues/3954)) + - Check power level before displaying actions in the room details' timeline ([#3959](https://github.com/vector-im/element-android/issues/3959)) + +Bugfixes 🐛 +---------- + - Add mxid to autocomplete suggestion if more than one user in a room has the same displayname ([#1823](https://github.com/vector-im/element-android/issues/1823)) + - Use WebView cache for widgets to avoid excessive data use ([#2648](https://github.com/vector-im/element-android/issues/2648)) + - Jitsi-hosted jitsi conferences not loading ([#2846](https://github.com/vector-im/element-android/issues/2846)) + - Space Explore Rooms no feedback on failed to join ([#3207](https://github.com/vector-im/element-android/issues/3207)) + - Notifications - Fix missing sound on notifications. ([#3243](https://github.com/vector-im/element-android/issues/3243)) + - the element-based domain permalinks (e.g. https://app.element.io/#/user/@chagai95:matrix.org) don't have the mxid in the first param (like matrix.to does - https://matrix.to/#/@chagai95:matrix.org) but rather in the second after /user/ so /user/mxid ([#3735](https://github.com/vector-im/element-android/issues/3735)) + - Update the AccountData with the users' matrix Id instead of their email for those invited by email in a direct chat ([#3743](https://github.com/vector-im/element-android/issues/3743)) + - Send an empty body for POST rooms/{roomId}/receipt/{receiptType}/{eventId} ([#3789](https://github.com/vector-im/element-android/issues/3789)) + - Fix order in which the items of the attachment menu appear ([#3793](https://github.com/vector-im/element-android/issues/3793)) + - Authenticated Jitsi not working in release ([#3841](https://github.com/vector-im/element-android/issues/3841)) + - Home: Dial pad lost entry when config changes ([#3845](https://github.com/vector-im/element-android/issues/3845)) + - Message edition is not rendered in e2e rooms after pagination ([#3887](https://github.com/vector-im/element-android/issues/3887)) + - Crash on opening a room on Android 5.0 and 5.1 - Regression with Voice message ([#3897](https://github.com/vector-im/element-android/issues/3897)) + - Fix a crash at start-up if translated string is empty ([#3910](https://github.com/vector-im/element-android/issues/3910)) + - PushRule enabling request is not following the spec ([#3911](https://github.com/vector-im/element-android/issues/3911)) + - Enable image preview in Android's share sheet (Android 11+) ([#3965](https://github.com/vector-im/element-android/issues/3965)) + - Voice Message - Cannot render voice message if the waveform data is corrupted ([#3983](https://github.com/vector-im/element-android/issues/3983)) + - Fix memory leak on RoomDetailFragment (ValueAnimator) ([#3990](https://github.com/vector-im/element-android/issues/3990)) + +Other changes +------------- + - VoIP: Merge virtual room timeline in corresponding native room (call events only). ([#3520](https://github.com/vector-im/element-android/issues/3520)) + - Issue templates: modernise and sync with element-web ([#3883](https://github.com/vector-im/element-android/issues/3883)) + - Issue templates: modernise SDK and release checklists, and add homeserver question for bugs ([#3889](https://github.com/vector-im/element-android/issues/3889)) + - Issue templates: merge expected and actual results ([#3960](https://github.com/vector-im/element-android/issues/3960)) + + Changes in Element v1.2.0 (2021-08-12) ====================================== diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5151a618f6..e1d213e6b7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -116,13 +116,40 @@ You should consider adding Unit tests with your PR, and also integration tests ( ### Internationalisation -When adding new string resources, please only add new entries in file `value/strings.xml`. Translations will be added later by the community of translators with a specific tool named [Weblate](https://translate.riot.im/projects/riot-android/). +Translations are handled using an external tool: [Weblate](https://translate.element.io/projects/element-android/) + +As a general rule, please never edit or add or remove translations to the project in a Pull Request. It can lead to merge conflict if the translations are also modified in Weblate side. + +#### Adding new string + +When adding new string resources, please only add new entries in file `value/strings.xml`. Translations will be added later by the community of translators using Weblate. + +New strings can be added anywhere in the file `value/strings.xml`, not necessarily at the end of the file. Generally, it's even better to add the new strings in some dedicated section per feature, and not at the end of the file, to avoid merge conflict between 2 PR adding strings at the end of the same file. + Do not hesitate to use plurals when appropriate. +#### Editing existing strings + +Two cases: +- If the meaning stays the same, it's OK to edit the original string (i.e. the English version). +- If the meaning is not the same, please create a new string and do not remove the existing string. See below for instructions to remove existing string. + +#### Removing existing strings + +If a string is not used anymore, it should be removed from the resource, but please do not remove the strings or its translations in the PR. It can lead to merge conflict with Weblate, and to lint error if new translations from deleted strings are added with Weblate. + +Instead, please comment the original string with: +```xml + +``` +The string will be removed during the next sync with Weblate. + ### Accessibility Please consider accessibility as an important point. As a minimum requirement, in layout XML files please use attributes such as `android:contentDescription` and `android:importantForAccessibility`, and test with a screen reader if it's working well. You can add new string resources, dedicated to accessibility, in this case, please prefix theirs id with `a11y_`. +For instance, when updating the image `src` of an ImageView, please also consider updating its `contentDescription`. A good example is a play pause button. + ### Layout When adding or editing layouts, make sure the layout will render correctly if device uses a RTL (Right To Left) language. diff --git a/attachment-viewer/build.gradle b/attachment-viewer/build.gradle index c393c5f483..064f497dc7 100644 --- a/attachment-viewer/build.gradle +++ b/attachment-viewer/build.gradle @@ -18,13 +18,12 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' android { - compileSdkVersion 30 + + compileSdk versions.compileSdk defaultConfig { - minSdkVersion 21 - targetSdkVersion 30 - versionCode 1 - versionName "1.0" + minSdk versions.minSdk + targetSdk versions.targetSdk } buildTypes { @@ -34,11 +33,11 @@ android { } } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility versions.sourceCompat + targetCompatibility versions.targetCompat } kotlinOptions { - jvmTarget = '1.8' + jvmTarget = "11" } buildFeatures { @@ -51,13 +50,13 @@ dependencies { implementation 'com.github.chrisbanes:PhotoView:2.3.0' - implementation 'io.reactivex.rxjava2:rxkotlin:2.4.0' - implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' + implementation libs.rx.rxKotlin + implementation libs.rx.rxAndroid - implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" - implementation 'androidx.core:core-ktx:1.6.0' - implementation 'androidx.appcompat:appcompat:1.3.1' - implementation "androidx.recyclerview:recyclerview:1.2.1" + implementation libs.jetbrains.kotlinStdlib + implementation libs.androidx.core + implementation libs.androidx.appCompat + implementation libs.androidx.recyclerview - implementation 'com.google.android.material:material:1.4.0' + implementation libs.google.material } \ No newline at end of file diff --git a/attachment-viewer/src/main/res/layout/item_animated_image_attachment.xml b/attachment-viewer/src/main/res/layout/item_animated_image_attachment.xml index 1096267124..cfeb5e9cc6 100644 --- a/attachment-viewer/src/main/res/layout/item_animated_image_attachment.xml +++ b/attachment-viewer/src/main/res/layout/item_animated_image_attachment.xml @@ -1,21 +1,22 @@ + android:layout_height="match_parent"> + android:layout_height="match_parent" + android:importantForAccessibility="no" + android:visibility="visible" /> diff --git a/attachment-viewer/src/main/res/layout/item_video_attachment.xml b/attachment-viewer/src/main/res/layout/item_video_attachment.xml index 7dd13ea460..cd55be5cb4 100644 --- a/attachment-viewer/src/main/res/layout/item_video_attachment.xml +++ b/attachment-viewer/src/main/res/layout/item_video_attachment.xml @@ -9,6 +9,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:layout_gravity="center" + android:importantForAccessibility="no" android:scaleType="centerInside" /> diff --git a/attachment-viewer/src/main/res/values/strings.xml b/attachment-viewer/src/main/res/values/strings.xml new file mode 100644 index 0000000000..3b32209868 --- /dev/null +++ b/attachment-viewer/src/main/res/values/strings.xml @@ -0,0 +1,5 @@ + + + + Play or pause the video + \ No newline at end of file diff --git a/build.gradle b/build.gradle index d7ba0168dd..49c3e07ece 100644 --- a/build.gradle +++ b/build.gradle @@ -1,9 +1,9 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - // Ref: https://kotlinlang.org/releases.html - ext.kotlin_version = '1.5.21' - ext.kotlin_coroutines_version = "1.5.0" + + apply from: 'dependencies.gradle' + repositories { google() jcenter() @@ -11,10 +11,13 @@ buildscript { url "https://plugins.gradle.org/m2/" } } + dependencies { - classpath 'com.android.tools.build:gradle:4.2.2' - classpath 'com.google.gms:google-services:4.3.8' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + // Release notes of Android Gradle Plugin (AGP): + // https://developer.android.com/studio/releases/gradle-plugin + classpath libs.gradle.gradlePlugin + classpath libs.gradle.kotlinPlugin + classpath 'com.google.gms:google-services:4.3.10' classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.3' classpath 'com.google.android.gms:oss-licenses-plugin:0.10.4' classpath "com.likethesalad.android:string-reference:1.2.2" @@ -44,6 +47,8 @@ allprojects { includeGroupByRegex 'com\\.github\\.chrisbanes' // PFLockScreen-Android includeGroupByRegex 'com\\.github\\.vector-im' + // DraggableView + includeGroupByRegex 'com\\.github\\.hyuwah' // Chat effects includeGroupByRegex 'com\\.github\\.jetradarmobile' diff --git a/changelog.d/240.feature b/changelog.d/240.feature new file mode 100644 index 0000000000..ee4d07a975 --- /dev/null +++ b/changelog.d/240.feature @@ -0,0 +1 @@ +Android Auto notification support diff --git a/changelog.d/3841.bugfix b/changelog.d/3841.bugfix deleted file mode 100644 index d0e84757dc..0000000000 --- a/changelog.d/3841.bugfix +++ /dev/null @@ -1 +0,0 @@ -Authenticated Jitsi not working in release \ No newline at end of file diff --git a/changelog.d/3845.bugfix b/changelog.d/3845.bugfix deleted file mode 100644 index ce820738fe..0000000000 --- a/changelog.d/3845.bugfix +++ /dev/null @@ -1 +0,0 @@ -Home: Dial pad lost entry when config changes \ No newline at end of file diff --git a/changelog.d/3898.bugfix b/changelog.d/3898.bugfix new file mode 100644 index 0000000000..49cab4adad --- /dev/null +++ b/changelog.d/3898.bugfix @@ -0,0 +1,2 @@ +Fixes the passphrase screen being incorrectly shown when pressing back on the key verification screen. +When the user doesn't have a passphrase set we don't show the passphrase screen. \ No newline at end of file diff --git a/changelog.d/3935.bugfix b/changelog.d/3935.bugfix new file mode 100644 index 0000000000..f4b1d309b4 --- /dev/null +++ b/changelog.d/3935.bugfix @@ -0,0 +1 @@ +Save button for adding rooms to a space is hidden when scrolling through list of rooms \ No newline at end of file diff --git a/changelog.d/4027.feature b/changelog.d/4027.feature new file mode 100644 index 0000000000..fa45d07ef9 --- /dev/null +++ b/changelog.d/4027.feature @@ -0,0 +1 @@ +Add client base url config to customize permalinks \ No newline at end of file diff --git a/changelog.d/4092.bugfix b/changelog.d/4092.bugfix new file mode 100644 index 0000000000..68ce518060 --- /dev/null +++ b/changelog.d/4092.bugfix @@ -0,0 +1,4 @@ +Added changes that will make SearchView in search bar focused by default on opening reaction picker. + +When tapping close icon of SearchView, the SearchView did not collapse therefore added the on close listener +which will collapse the SearchView on close. diff --git a/changelog.d/4113.misc b/changelog.d/4113.misc new file mode 100644 index 0000000000..a5faa57b92 --- /dev/null +++ b/changelog.d/4113.misc @@ -0,0 +1 @@ +Fix release label in the release issue template diff --git a/dependencies.gradle b/dependencies.gradle new file mode 100644 index 0000000000..6a70486530 --- /dev/null +++ b/dependencies.gradle @@ -0,0 +1,135 @@ +ext.versions = [ + + 'minSdk' : 21, + 'compileSdk' : 30, + 'targetSdk' : 30, + 'sourceCompat' : JavaVersion.VERSION_11, + 'targetCompat' : JavaVersion.VERSION_11, +] + +def gradle = "7.0.2" +// Ref: https://kotlinlang.org/releases.html +def kotlin = "1.5.31" +def kotlinCoroutines = "1.5.2" +def dagger = "2.38.1" +def retrofit = "2.9.0" +def arrow = "0.8.2" +def markwon = "4.6.2" +def moshi = "1.12.0" +def lifecycle = "2.2.0" +def rxBinding = "3.1.0" +def epoxy = "4.6.2" +def glide = "4.12.0" +def bigImageViewer = "1.8.1" +def jjwt = "0.11.2" +def vanniktechEmoji = "0.8.0" + +// Testing +def mockk = "1.12.0" +def espresso = "3.4.0" +def androidxTest = "1.4.0" + + +ext.libs = [ + gradle : [ + 'gradlePlugin' : "com.android.tools.build:gradle:$gradle", + 'kotlinPlugin' : "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin" + ], + jetbrains : [ + 'kotlinStdlibJdk7' : "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin", + 'kotlinStdlib' : "org.jetbrains.kotlin:kotlin-stdlib:$kotlin", + 'coroutinesCore' : "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinCoroutines", + 'coroutinesAndroid' : "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlinCoroutines", + 'coroutinesRx2' : "org.jetbrains.kotlinx:kotlinx-coroutines-rx2:$kotlinCoroutines" + ], + androidx : [ + 'appCompat' : "androidx.appcompat:appcompat:1.3.1", + 'core' : "androidx.core:core-ktx:1.6.0", + 'recyclerview' : "androidx.recyclerview:recyclerview:1.2.1", + 'exifinterface' : "androidx.exifinterface:exifinterface:1.3.3", + 'fragmentKtx' : "androidx.fragment:fragment-ktx:1.3.6", + 'constraintLayout' : "androidx.constraintlayout:constraintlayout:2.1.1", + 'work' : "androidx.work:work-runtime-ktx:2.5.0", + 'autoFill' : "androidx.autofill:autofill:1.1.0", + 'preferenceKtx' : "androidx.preference:preference-ktx:1.1.1", + 'junit' : "androidx.test.ext:junit:1.1.3", + 'lifecycleExtensions' : "androidx.lifecycle:lifecycle-extensions:$lifecycle", + 'lifecycleJava8' : "androidx.lifecycle:lifecycle-common-java8:$lifecycle", + 'lifecycleLivedata' : "androidx.lifecycle:lifecycle-livedata-ktx:2.3.1", + 'datastore' : "androidx.datastore:datastore:1.0.0", + 'datastorepreferences' : "androidx.datastore:datastore-preferences:1.0.0", + 'pagingRuntimeKtx' : "androidx.paging:paging-runtime-ktx:2.1.2", + 'coreTesting' : "androidx.arch.core:core-testing:2.1.0", + 'testCore' : "androidx.test:core:$androidxTest", + 'orchestrator' : "androidx.test:orchestrator:$androidxTest", + 'testRunner' : "androidx.test:runner:$androidxTest", + 'testRules' : "androidx.test:rules:$androidxTest", + 'espressoCore' : "androidx.test.espresso:espresso-core:$espresso", + 'espressoContrib' : "androidx.test.espresso:espresso-contrib:$espresso", + 'espressoIntents' : "androidx.test.espresso:espresso-intents:$espresso" + ], + google : [ + 'material' : "com.google.android.material:material:1.4.0" + ], + dagger : [ + 'dagger' : "com.google.dagger:dagger:$dagger", + 'daggerCompiler' : "com.google.dagger:dagger-compiler:$dagger" + ], + squareup : [ + 'moshi' : "com.squareup.moshi:moshi-adapters:$moshi", + 'moshiKotlin' : "com.squareup.moshi:moshi-kotlin-codegen:$moshi", + 'retrofit' : "com.squareup.retrofit2:retrofit:$retrofit", + 'retrofitMoshi' : "com.squareup.retrofit2:converter-moshi:$retrofit" + ], + rx : [ + 'rxKotlin' : "io.reactivex.rxjava2:rxkotlin:2.4.0", + 'rxAndroid' : "io.reactivex.rxjava2:rxandroid:2.1.1" + ], + arrow : [ + 'core' : "io.arrow-kt:arrow-core:$arrow", + 'instances' : "io.arrow-kt:arrow-instances-core:$arrow" + ], + markwon : [ + 'core' : "io.noties.markwon:core:$markwon", + 'html' : "io.noties.markwon:html:$markwon" + ], + airbnb : [ + 'epoxy' : "com.airbnb.android:epoxy:$epoxy", + 'epoxyGlide' : "com.airbnb.android:epoxy-glide-preloading:$epoxy", + 'epoxyProcessor' : "com.airbnb.android:epoxy-processor:$epoxy", + 'epoxyPaging' : "com.airbnb.android:epoxy-paging:$epoxy", + 'mvrx' : "com.airbnb.android:mvrx:1.5.1" + ], + mockk : [ + 'mockk' : "io.mockk:mockk:$mockk", + 'mockkAndroid' : "io.mockk:mockk-android:$mockk" + ], + github : [ + 'glide' : "com.github.bumptech.glide:glide:$glide", + 'glideCompiler' : "com.github.bumptech.glide:compiler:$glide", + 'bigImageViewer' : "com.github.piasy:BigImageViewer:$bigImageViewer", + 'glideImageLoader' : "com.github.piasy:GlideImageLoader:$bigImageViewer", + 'progressPieIndicator' : "com.github.piasy:ProgressPieIndicator:$bigImageViewer", + 'glideImageViewFactory' : "com.github.piasy:GlideImageViewFactory:$bigImageViewer" + ], + jakewharton : [ + 'timber' : "com.jakewharton.timber:timber:5.0.1", + 'rxbinding' : "com.jakewharton.rxbinding3:rxbinding:$rxBinding", + 'rxbindingAppcompat' : "com.jakewharton.rxbinding3:rxbinding-appcompat:$rxBinding", + 'rxbindingMaterial' : "com.jakewharton.rxbinding3:rxbinding-material:$rxBinding" + ], + jsonwebtoken: [ + 'jjwtApi' : "io.jsonwebtoken:jjwt-api:$jjwt", + 'jjwtImpl' : "io.jsonwebtoken:jjwt-impl:$jjwt", + 'jjwtOrgjson' : "io.jsonwebtoken:jjwt-orgjson:$jjwt" + ], + vanniktech: [ + 'emojiMaterial' : "com.vanniktech:emoji-material:$vanniktechEmoji", + 'emojiGoogle' : "com.vanniktech:emoji-google:$vanniktechEmoji" + ], + tests : [ + 'kluent' : "org.amshove.kluent:kluent-android:1.68", + 'timberJunitRule' : "net.lachlanmckee:timber-junit-rule:1.0.1", + 'junit' : "junit:junit:4.13.2" + ] +] \ No newline at end of file diff --git a/diff-match-patch/build.gradle b/diff-match-patch/build.gradle index 82292e24db..f623c57a49 100644 --- a/diff-match-patch/build.gradle +++ b/diff-match-patch/build.gradle @@ -3,6 +3,3 @@ apply plugin: 'java-library' dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) } - -sourceCompatibility = "8" -targetCompatibility = "8" diff --git a/fastlane/README.md b/fastlane/README.md index 54d3a005a6..dc33f422d6 100644 --- a/fastlane/README.md +++ b/fastlane/README.md @@ -44,6 +44,6 @@ Get version code ---- -This README.md is auto-generated and will be re-generated every time [fastlane](https://fastlane.tools) is run. +This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run. More information about fastlane can be found on [fastlane.tools](https://fastlane.tools). The documentation of fastlane can be found on [docs.fastlane.tools](https://docs.fastlane.tools). diff --git a/fastlane/metadata/android/cs-CZ/changelogs/40101160.txt b/fastlane/metadata/android/cs-CZ/changelogs/40101160.txt new file mode 100644 index 0000000000..df927bca03 --- /dev/null +++ b/fastlane/metadata/android/cs-CZ/changelogs/40101160.txt @@ -0,0 +1,2 @@ +Hlavní změny v této verzi: Oprava chyby při odesílání šifrované zprávy, pokud se někdo v místnosti odhlásí. +Úplný seznam změn: https://github.com/vector-im/element-android/releases/tag/v1.1.16 diff --git a/fastlane/metadata/android/cs-CZ/changelogs/40102000.txt b/fastlane/metadata/android/cs-CZ/changelogs/40102000.txt new file mode 100644 index 0000000000..929281c388 --- /dev/null +++ b/fastlane/metadata/android/cs-CZ/changelogs/40102000.txt @@ -0,0 +1,2 @@ +Hlavní změny v této verzi: Hlasové zprávy jsou povoleny ve výchozím nastavení. +Úplný seznam změn: https://github.com/vector-im/element-android/releases/tag/v1.2.0 diff --git a/fastlane/metadata/android/cs-CZ/changelogs/40102010.txt b/fastlane/metadata/android/cs-CZ/changelogs/40102010.txt new file mode 100644 index 0000000000..ca75c6b5d8 --- /dev/null +++ b/fastlane/metadata/android/cs-CZ/changelogs/40102010.txt @@ -0,0 +1,2 @@ +Hlavní změny v této verzi: Mnohá vylepšení VoIP a prostorů (stále v beta verzi). +Úplný seznam změn: https://github.com/vector-im/element-android/releases/tag/v1.2.1 diff --git a/fastlane/metadata/android/cs-CZ/full_description.txt b/fastlane/metadata/android/cs-CZ/full_description.txt index d169c077e1..6732b33fe3 100644 --- a/fastlane/metadata/android/cs-CZ/full_description.txt +++ b/fastlane/metadata/android/cs-CZ/full_description.txt @@ -11,7 +11,7 @@ Element je zabezpečený komunikátor a zároveň aplikace pro týmovou spolupr Element se zcela liší od ostatních aplikací pro zasílání zpráv a spolupráci. Funguje na platformě Matrix, otevřené síti pro bezpečné zasílání zpráv a decentralizovanou komunikaci. Umožňuje vlastní hostování, aby uživatelé měli maximální vlastnictví a kontrolu nad svými daty a zprávami. Soukromí a šifrované zprávy -Element vás chrání před nežádoucími reklamami, vytěžováním dat a tzv. walled gardens. Zabezpečuje také všechna vaše data, video a hlasovou komunikaci jeden na jednoho prostřednictvím šifrování end-to-end a křížového ověřování zařízení. +Element vás chrání před nežádoucími reklamami, vytěžováním dat a tzv. walled gardens. Zabezpečuje také všechna vaše data, video a hlasovou komunikaci jeden na jednoho prostřednictvím koncového šifrování a křížového ověřování zařízení. Element vám dává kontrolu nad vaším soukromím a zároveň vám umožňuje bezpečně komunikovat s kýmkoli v síti Matrix nebo s dalšími nástroji pro firemní spolupráci díky integraci s aplikacemi, jako je Slack. @@ -30,7 +30,7 @@ Element vám dává kontrolu různými způsoby: Můžete chatovat s kýmkoli v síti Matrix, ať už používá aplikaci Element, jinou aplikaci podporující protokol Matrix nebo dokonce i když používá jinou aplikaci pro zasílání zpráv. Superbezpečné -Skutečné end-to-end šifrování (zprávy mohou dešifrovat pouze účastníci konverzace) a křížové ověřování zařízení. +Skutečné koncové šifrování (zprávy mohou dešifrovat pouze účastníci konverzace) a křížové ověřování zařízení. Kompletní komunikace a integrace Zprávy, hlasové a videohovory, sdílení souborů, sdílení obrazovky a celá řada integrací, botů a widgetů. Vytvářejte místnosti, komunity, zůstaňte v kontaktu a spolupracujte. diff --git a/fastlane/metadata/android/de-DE/changelogs/40101120.txt b/fastlane/metadata/android/de-DE/changelogs/40101120.txt new file mode 100644 index 0000000000..ac147c6cbb --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/40101120.txt @@ -0,0 +1,2 @@ +Hauptänderungen in dieser Version: Überarbeitetes Design, Crash nach Videoanruf gefixt +Alle Änderungen: https://github.com/vector-im/element-android/releases/tag/v1.1.12 diff --git a/fastlane/metadata/android/de-DE/changelogs/40101130.txt b/fastlane/metadata/android/de-DE/changelogs/40101130.txt new file mode 100644 index 0000000000..4ef31c67ed --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/40101130.txt @@ -0,0 +1,2 @@ +Stabilitäts- und Bugfixupdate +Alle Änderungen: https://github.com/vector-im/element-android/releases/tag/v1.1.13 diff --git a/fastlane/metadata/android/de-DE/changelogs/40101150.txt b/fastlane/metadata/android/de-DE/changelogs/40101150.txt new file mode 100644 index 0000000000..7fa18ed944 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/40101150.txt @@ -0,0 +1,2 @@ +Hauptänderungen: Sprachnachrichten in experimentellen Einstellungen +Alle Änderungen: https://github.com/vector-im/element-android/releases/tag/v1.1.15 diff --git a/fastlane/metadata/android/de-DE/changelogs/40101160.txt b/fastlane/metadata/android/de-DE/changelogs/40101160.txt new file mode 100644 index 0000000000..babd4af04f --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/40101160.txt @@ -0,0 +1,2 @@ +Hauptänderungen in dieser Version: Fehler beim Senden verschlüsselter Nachrichten behoben. +Alle Änderungen: https://github.com/vector-im/element-android/releases/tag/v1.1.16 diff --git a/fastlane/metadata/android/de-DE/changelogs/40102000.txt b/fastlane/metadata/android/de-DE/changelogs/40102000.txt new file mode 100644 index 0000000000..cfa9f725f2 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/40102000.txt @@ -0,0 +1,2 @@ +Hauptänderungen: Sprachnachrichten standardmäßig aktiviert. +Ganze Änderungsliste: https://github.com/vector-im/element-android/releases/tag/v1.2.0 diff --git a/fastlane/metadata/android/de-DE/changelogs/40102010.txt b/fastlane/metadata/android/de-DE/changelogs/40102010.txt new file mode 100644 index 0000000000..2635704a81 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/40102010.txt @@ -0,0 +1,2 @@ +VoIP und Spaces verbessert +Vollständige Änderungsliste: https://github.com/vector-im/element-android/releases/tag/v1.2.1 diff --git a/fastlane/metadata/android/en-US/changelogs/40102000.txt b/fastlane/metadata/android/en-US/changelogs/40102000.txt index 46d9b07809..4fc02966cd 100644 --- a/fastlane/metadata/android/en-US/changelogs/40102000.txt +++ b/fastlane/metadata/android/en-US/changelogs/40102000.txt @@ -1,2 +1,2 @@ Main changes in this version: Voice Message is enabled by default. -Full changelog: https://github.com/vector-im/element-android/releases/tag/v1.1.16 \ No newline at end of file +Full changelog: https://github.com/vector-im/element-android/releases/tag/v1.2.0 \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/40102010.txt b/fastlane/metadata/android/en-US/changelogs/40102010.txt new file mode 100644 index 0000000000..799b7dc8b6 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/40102010.txt @@ -0,0 +1,2 @@ +Main changes in this version: Many improvements on VoIP and Spaces (still in beta). +Full changelog: https://github.com/vector-im/element-android/releases/tag/v1.2.1 \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/40103000.txt b/fastlane/metadata/android/en-US/changelogs/40103000.txt new file mode 100644 index 0000000000..d4ef2f75a0 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/40103000.txt @@ -0,0 +1,2 @@ +Main changes in this version: Organize your rooms using Spaces! +Full changelog: https://github.com/vector-im/element-android/releases/tag/v1.3.0 \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/40103010.txt b/fastlane/metadata/android/en-US/changelogs/40103010.txt new file mode 100644 index 0000000000..e3760f1882 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/40103010.txt @@ -0,0 +1,2 @@ +Main changes in this version: Organize your rooms using Spaces! v1.3.1 is fixing a crash which can occurs in v1.3.0. +Full changelog: https://github.com/vector-im/element-android/releases/tag/v1.3.1 \ No newline at end of file diff --git a/fastlane/metadata/android/es-ES/changelogs/40101140.txt b/fastlane/metadata/android/es-ES/changelogs/40101140.txt new file mode 100644 index 0000000000..77399d6aef --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/40101140.txt @@ -0,0 +1,2 @@ +Principales cambios en esta versión: arreglado un problema con los mensajes encriptados. +Conjunto de cambios completo: https://github.com/vector-im/element-android/releases/tag/v1.1.14 diff --git a/fastlane/metadata/android/es-ES/changelogs/40101150.txt b/fastlane/metadata/android/es-ES/changelogs/40101150.txt new file mode 100644 index 0000000000..c127db1ef7 --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/40101150.txt @@ -0,0 +1,2 @@ +Principales cambios en esta versión: implementación de mensajes de voz bajo los ajustes lab. +Conjunto de cambios completo: https://github.com/vector-im/element-android/releases/tag/v1.1.15 diff --git a/fastlane/metadata/android/es-ES/changelogs/40101160.txt b/fastlane/metadata/android/es-ES/changelogs/40101160.txt new file mode 100644 index 0000000000..0d2ab3e13d --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/40101160.txt @@ -0,0 +1,2 @@ +Principales cambios en esta versión: Solucionado error al enviar mensajes encriptados si alguien en la sala cierra la sesión. +Conjunto de cambios completo: https://github.com/vector-im/element-android/releases/tag/v1.1.16 diff --git a/fastlane/metadata/android/es-ES/short_description.txt b/fastlane/metadata/android/es-ES/short_description.txt index 473228e0df..0bd47ccf15 100644 --- a/fastlane/metadata/android/es-ES/short_description.txt +++ b/fastlane/metadata/android/es-ES/short_description.txt @@ -1 +1 @@ -Chat y VoIP descentralizados y seguros. Mantén tus datos a salvo de terceros. +Mensajería para grupos - mensajería encriptada, chat para grupos y videollamadas diff --git a/fastlane/metadata/android/et/changelogs/40101160.txt b/fastlane/metadata/android/et/changelogs/40101160.txt new file mode 100644 index 0000000000..449a6edfee --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/40101160.txt @@ -0,0 +1,2 @@ +Põhilised muutused selles versioonis: samaaegse krüptitud sõnumi saatmise ja jututoast väljalogimisega seotud vea parandus. +Kogu ingliskeelne muudatuste logi: https://github.com/vector-im/element-android/releases/tag/v1.1.16 diff --git a/fastlane/metadata/android/et/changelogs/40102000.txt b/fastlane/metadata/android/et/changelogs/40102000.txt new file mode 100644 index 0000000000..57f28039c5 --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/40102000.txt @@ -0,0 +1,2 @@ +Põhilised muutused selles versioonis: häälsõnumid on nüüd vaikimisi kasutusel. +Kogu ingliskeelne muudatuste logi: https://github.com/vector-im/element-android/releases/tag/v1.2.0 diff --git a/fastlane/metadata/android/et/changelogs/40102010.txt b/fastlane/metadata/android/et/changelogs/40102010.txt new file mode 100644 index 0000000000..0dc70c90af --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/40102010.txt @@ -0,0 +1,2 @@ +Põhilised muutused selles versioonis: palju täiendusi kõnede ja veel testjärgus olevas kogukonnakeskuste loogikas. +Kogu ingliskeelne muudatuste logi: https://github.com/vector-im/element-android/releases/tag/v1.2.1 diff --git a/fastlane/metadata/android/fa/changelogs/40101020.txt b/fastlane/metadata/android/fa/changelogs/40101020.txt new file mode 100644 index 0000000000..6c0c205f5c --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/40101020.txt @@ -0,0 +1,2 @@ +تغییرات اصلی در این نگارش: بهبودهای کارایی و رفع اشکال‌ها! +گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases/tag/v1.1.2 diff --git a/fastlane/metadata/android/fa/changelogs/40101030.txt b/fastlane/metadata/android/fa/changelogs/40101030.txt new file mode 100644 index 0000000000..6b252e051c --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/40101030.txt @@ -0,0 +1,2 @@ +تغییرات اصلی در این نگارش: بهبودهای کارایی و رفع اشکال‌ها! +گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases/tag/v1.1.3 diff --git a/fastlane/metadata/android/fa/changelogs/40101040.txt b/fastlane/metadata/android/fa/changelogs/40101040.txt new file mode 100644 index 0000000000..043647a793 --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/40101040.txt @@ -0,0 +1,2 @@ +تغییرات اصلی در این نگارش: بهبودهای کارایی و رفع اشکال‌ها! +گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases/tag/v1.1.4 diff --git a/fastlane/metadata/android/fa/changelogs/40101050.txt b/fastlane/metadata/android/fa/changelogs/40101050.txt new file mode 100644 index 0000000000..1c29a55d53 --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/40101050.txt @@ -0,0 +1,2 @@ +تغییرات اصلی در این نگارش: تعمیرات فوری برای 1.1.4 +گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases/tag/v1.1.5 diff --git a/fastlane/metadata/android/fa/changelogs/40101060.txt b/fastlane/metadata/android/fa/changelogs/40101060.txt new file mode 100644 index 0000000000..b9542a3bd7 --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/40101060.txt @@ -0,0 +1,2 @@ +تغییرات اصلی در این نگارش: تعمیرات فوری برای 1.1.5 +گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases/tag/v1.1.6 diff --git a/fastlane/metadata/android/fa/changelogs/40101070.txt b/fastlane/metadata/android/fa/changelogs/40101070.txt new file mode 100644 index 0000000000..8330a5060e --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/40101070.txt @@ -0,0 +1,2 @@ +تغییرات اصلی در این نگارش: پشتیبانی آزمایشی برای فضاها. فشرده‌سازی ویدیو پیش از فرستادن. +گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases/tag/v1.1.7 diff --git a/fastlane/metadata/android/fa/changelogs/40101080.txt b/fastlane/metadata/android/fa/changelogs/40101080.txt new file mode 100644 index 0000000000..02cb3390a3 --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/40101080.txt @@ -0,0 +1,2 @@ +تغییرات اصلی در این نگارش: بهبود برای فضاها. +گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases/tag/v1.1.8 diff --git a/fastlane/metadata/android/fa/changelogs/40101090.txt b/fastlane/metadata/android/fa/changelogs/40101090.txt new file mode 100644 index 0000000000..d458caf46c --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/40101090.txt @@ -0,0 +1,2 @@ +تغییرات اصلی در این نگارش: افزودن پشتیبانی برای شبکهٔ gitter.im. +گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases/tag/v1.1.9 diff --git a/fastlane/metadata/android/fa/changelogs/40101100.txt b/fastlane/metadata/android/fa/changelogs/40101100.txt new file mode 100644 index 0000000000..d51bd2427b --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/40101100.txt @@ -0,0 +1,2 @@ +تغییرات اصلی در این نگارش: به‌روز رسانی زمینه و سبک و ویژگی‌های جدید برای فضاها. +گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases/tag/v1.1.10 diff --git a/fastlane/metadata/android/fa/changelogs/40101110.txt b/fastlane/metadata/android/fa/changelogs/40101110.txt new file mode 100644 index 0000000000..efd2ec9f4d --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/40101110.txt @@ -0,0 +1,2 @@ +تغییرات اصلی در این نگارش: به‌روز رسانی زمینه و سبک و ویژگی‌های جدید برای فضاها (رفع اشکال برای 1.1.10) +گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases/tag/v1.1.11 diff --git a/fastlane/metadata/android/fa/changelogs/40101120.txt b/fastlane/metadata/android/fa/changelogs/40101120.txt new file mode 100644 index 0000000000..1c8fb47c19 --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/40101120.txt @@ -0,0 +1,2 @@ +تغییرات اصلی در این نگارش: به‌روز رسانی زمینه و سبک و رفع یک فروپاشی پس از تماس ویدیویی +گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases/tag/v1.1.12 diff --git a/fastlane/metadata/android/fa/changelogs/40101130.txt b/fastlane/metadata/android/fa/changelogs/40101130.txt new file mode 100644 index 0000000000..55d0801920 --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/40101130.txt @@ -0,0 +1,2 @@ +تغییرات اصلی در این نگارش:به‌روز رسانی پایداری و رفع مشکل. +گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases/tag/v1.1.13 diff --git a/fastlane/metadata/android/fa/changelogs/40101140.txt b/fastlane/metadata/android/fa/changelogs/40101140.txt new file mode 100644 index 0000000000..2239799429 --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/40101140.txt @@ -0,0 +1,2 @@ +تغییرات اصلی در این نگارش: درست کردن مشکلی دربارهٔ پیام‌های رمزنگاری‌شده. +گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases/tag/v1.1.14 diff --git a/fastlane/metadata/android/fa/changelogs/40101160.txt b/fastlane/metadata/android/fa/changelogs/40101160.txt new file mode 100644 index 0000000000..82e3668777 --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/40101160.txt @@ -0,0 +1,2 @@ +تغییرات اصلی در این نگارش: تعمیر خطا هنگام فرستادن پیام رمزنگاشته در صورت خروج عضوی از سامانه. +گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases/tag/v1.1.16 diff --git a/fastlane/metadata/android/fa/changelogs/40102000.txt b/fastlane/metadata/android/fa/changelogs/40102000.txt new file mode 100644 index 0000000000..9c9a7c51d0 --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/40102000.txt @@ -0,0 +1,2 @@ +تغییرهای اصلی در این نگارش: پیام صوتی به صورت پیش‌گزیده به کار افتاده. +گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases/tag/v1.2.0 diff --git a/fastlane/metadata/android/fa/changelogs/40102010.txt b/fastlane/metadata/android/fa/changelogs/40102010.txt new file mode 100644 index 0000000000..a2cc27d1b5 --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/40102010.txt @@ -0,0 +1,2 @@ +تغییرات اصلی در این نگارش: چندین بهبود در ویپ و فضاها (همچنان در حالت آزمایشی). +گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases/tag/v1.2.1 diff --git a/fastlane/metadata/android/fa/full_description.txt b/fastlane/metadata/android/fa/full_description.txt index 3051f491ba..cd2d4eb4c1 100644 --- a/fastlane/metadata/android/fa/full_description.txt +++ b/fastlane/metadata/android/fa/full_description.txt @@ -1,30 +1,39 @@ -المنت گونه‌ای جدید از کاره‌های پیام‌رسانی و همکاری است که: +المنت پیام‌رسانی امن و کاره‌ای برای همکاری گروهی است که برای گپ‌های گروهی هنگام دورکاری، آرمانیست. این کارهٔ گپ برای فراهم کردن کنفرانس ویدیویی، هم‌رسانی پرونده و تماس‌های صوتی، از رمزنگاری سرتاسری استفاده می‌‌کند. -۱. کنترل محرمانگیتان را در دست خودتان می‌گذارد -۲. می‌گذارد با هرکسی در شبکهٔ ماتریکس و حتا فراتر از آن، ارتباط برقرار کنید -۳. از شما در برابر تبلیغات، داده‌کاوری و دیوارهای پرداختی، محافظت می‌کند -۴. با رمزنگاری سرتاسری با ورود چندگانه، امنتان می‌کند +ویژگی‌های المنت شامل: +- ابزارهای ارتباط برخط پیش‌رفته +- پیام‌های کاملاً مزنگاری شده برای ارتباط سازمانی امن، حتا هنگام دورکاری +- گپ نامتمرکز برپایهٔ چارچوب نرم‌افزاری آزاد ماتریکس +- هم‌رسانی پروندهٔ امن با داده‌های رمزنگاری شده هنگام مدیریت پروژه‌ها +- گپ‌های ویدیویی با صدا روی قرارداد اینترنتی و هم‌رسانی پرونده +- یکپارچگی آسان با دیگر کاره‌های پیام‌رسانی گروهی، خدمات وًیپ، ابزارهای مدیریت پروژه و ابزارهای همکاری برخط محبوبتان -المنت به خاطر نامتمرکز و نرم‌افزار آزاد بودن، کاملاً با دیگر کاره‌های پیام‌رسانی و همکاری، فرق دارد. +المنت کاملاً با دیگر کاره‌های پیام‌رسانی و همکاری، فرق دارد؛ چرا که روی ماتریکس، شبکه‌ای باز برای پیام‌رسانی امن و ارتباط نامتمرکز عمل می‌کند. این امر، خودمیزبانی را برای دادن بیشینهٔ مالکیت و واپایش روی داده‌ها و پیام‌ها ممکن می‌کند. -المنت می‌گذارد خودمیزبانی کرده یا میزبانی برگزینید که امنیت، مالکیت و واپایش داده‌ها و گفت‌وگوهایتان را در اختیار داشته باشید. این کاره شما را به شبکه‌ای باز و شدیداً امن وصل کرده تا مجبور نباشید فقط با دیگر کاربران المنت صحبت کنید. +پیام‌رسانی رمزنگاری شده و امن +المنت شما را از تبلیغات ناخواسته، داده‌کاوی و زمین‌های محصور در امان نگه می‌دارد. همچنین تمامی داده‌هایتان و ارتباطات صوتی و تصویری یک‌به‌یکتان را با رمزنگاری سرتاسری و تأیید افزاره با ورود چندگانه، امن می‌کند. -المنت می‌تواند همهٔ این کارها را بکند، چرا که روی ماتریکس، استانداردی برای گفت‌وگوی باز و نامتمرکز عمل می‌کند. +المنت مهار محرمانگیتان را به دست خودتان می‌دهد؛ در عین این که می‌گذار با هرکسی روی شبکهٔ ماتریکس یا با یکپارچگی با کاره‌هایی چون اسلک، دیگر ابزارهای همکاری تجاری، در ارتباط باشید. -المنت با اجازه برای گزینش کسی که گفت‌وگوهایتان را میزبانی می‌کند، کنترل را به شما می‌دهد. با کارهٔ المنت، می‌توانید برگزینید که به روش‌های مختلفی میزبانی شوید: +المنت می‌تواند خودمیزبانی شود +المنت می‌تواند برای دادن واپایش بیش‌تر روی گفت‌وگوها و داده‌های حسّاستان، خودمیزبانی شده یا می‌توانید هر میزبان مبتنی بر ماتریکسی را که استانداردی برای ارتباط نامتمرکز نرم‌افزار آزاد است، برگزینید. المنت به شما محرمانگی، امنیت و انعطاف می‌دهد. -۱. گرفتن حسابی رایگان روی کارساز عمومی matrix.org که به دست توسعه‌دهندگان ماتریکس میزبانی می‌شود، یا گرینش از میان هزاران کارساز عمومی میزبانی‌شده به دست داوطلبان -۲. خودمیزبانی حسابتان با اجرای کراسازی روی سخت‌افزار خودتان -۳. ثبت‌نام برای حسابی روی یک کارساز سفارشی با اشتراک در بن‌یازهٔ میزبانی خدمات ماتریکس المنت +مالک داده‌هایتان باشید +خودتان تصمیم می‌گیرید که داده‌ها و پیام‌هایتان، کجا ذخیره شوند. بدون خطر داده‌کاوی یا دسترسی سوم‌شخص. -چرا المنت را برگزینیم؟ +المنت به روش‌های مختلفی مهار را در دستان شما می‌گذارد: +۱. گرفتن حسابی رایگان روی کارساز عمومی matrix.org که به دست توسعه‌دهندگان ماتریکس میزبانی می‌شود، یا گزینش از میان هزاران کارساز عمومی میزبانی‌شده به دست داوطلبان +۲. خودمیزبانی حسابتان با اجرای کارسازی روی زیرساخت آی‌تی خودتان +۳. ثبت‌نام برای حسابی روی یک کارساز سفارشی با اشتراک در بن‌سازهٔ میزبانی خدمات ماتریکس المنت -مالک داده‌هایتان باشید: خوتان تصمیم می‌گیرید که داده‌ها و پیام‌هایتان را کجا نگه دارید. شما صاحبشان هستید و واپایششان می‌کنید، نه شرکت‌های بزرگی که داده‌هایتان را کاویده و به شرکت‌های دیگر دسترسی می‌دهند. +پیام‌رسانی و همکاری باز +می‌توانید با هرکسی در شبکهٔ ماتریکس گپ بزنید، چه از المنت استفاده کنند، چه از کارهٔ ماتریکس دیگری و حتا از کاره‌های پیام‌رسانی دیگر. -پیام‌رسانی و همکاری باز: می‌توانید با هرکسی در شبکهٔ ماتریکس گپ بزنید، چه از المنت استفاده کنند و چه از هر کارهٔ ماتریکس دیگری؛ و حتا اگر از سامانهٔ پیام‌رسانی متفاوتی مثل اسلک، آی‌آرسی یا جبر استفاده کنند. +فوق امن +رمزنگاری سرتاسری واقعی (فقط کسانی که در گفت‌وگو هستند،‌می‌توانند پیام‌ها را رمزگشایی کنند) و تأیید هویت افزاره با ورود چندگانه. -b>فوق امن: رمزنگاری سرتاسری واقعی (فقط کسانی که در گفت‌وگو هستند،‌می‌توانند پیام‌ها را رمزگشایی کنند) و ورود چندگانه برای تأیید هویت افزاره‌های شرکت‌کنندگان در گفت‌وگو. +یکپارچگی ارتباط کامل +پیام‌رسانی، تماس‌های صوتی و تصویری، هم‌رسانی پرونده، هم‌رسانی صفحه و یه عالمه یکپارچگی، بات و ابزارک. اتاق و اجتماع ساخته، در دسترس بوده و کارها را انجام دهید. -ارتباط کامل: پیام‌رسانی، تماس‌های صوتی و تصویری،‌هم‌رسانی پرونده، هم‌رسانی صفحه و یه عالمه یکپارچگی، بات و ابزارک. اتاق و اجتماع ساخته، در دسترس بوده و کارها را انجام دهید. - -هرجا که هستید: هر کجا که هستید، با هم‌گام سازی کامل تاریخچهٔ پیام‌ها بین همهٔ افزاره‌هایتان و وب روی https://app.element.io در دسترس باشید. +ادامه از جایی که رها کرده‌اید +هر کجا که هستید، با هم‌گام سازی کامل تاریخچهٔ پیام‌ها بین همهٔ افزاره‌هایتان و وب روی https://app.element.io در دسترس باشید diff --git a/fastlane/metadata/android/fr-FR/changelogs/40101160.txt b/fastlane/metadata/android/fr-FR/changelogs/40101160.txt new file mode 100644 index 0000000000..785be4ab69 --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/40101160.txt @@ -0,0 +1,2 @@ +Principaux changements pour cette version : correction d’une erreur lors de l’envoi d’un message chiffré si quelqu’un du salon se déconnecte. +Intégralité des changements : https://github.com/vector-im/element-android/releases/tag/v1.1.16 diff --git a/fastlane/metadata/android/fr-FR/changelogs/40102000.txt b/fastlane/metadata/android/fr-FR/changelogs/40102000.txt new file mode 100644 index 0000000000..504c3e24be --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/40102000.txt @@ -0,0 +1,2 @@ +Principaux changements pour cette version : messages vocaux activés par défault. +Intégralité des changements : https://github.com/vector-im/element-android/releases/tag/v1.1.16 diff --git a/fastlane/metadata/android/fy/changelogs/40101060.txt b/fastlane/metadata/android/fy/changelogs/40101060.txt index 47ac5692d5..34b367f6a1 100644 --- a/fastlane/metadata/android/fy/changelogs/40101060.txt +++ b/fastlane/metadata/android/fy/changelogs/40101060.txt @@ -1,2 +1,2 @@ -Haadferoaring yn disse ferzje: feroaringen foar 1.1.5 -Folsleine feroaringslist: https://github.com/vector-im/element-android/releases/tag/v1.1.6 +Haadwiziging yn dizze ferzje: feroaringen foar 1.1.5 +Folsleine wizigingslist: https://github.com/vector-im/element-android/releases/tag/v1.1.6 diff --git a/fastlane/metadata/android/hu-HU/changelogs/40101150.txt b/fastlane/metadata/android/hu-HU/changelogs/40101150.txt new file mode 100644 index 0000000000..48765e903b --- /dev/null +++ b/fastlane/metadata/android/hu-HU/changelogs/40101150.txt @@ -0,0 +1,2 @@ +Fő változás ebben a verzióban: hang üzenetek implementálva a labor beállítások alatt +Teljes változásnapló: https://github.com/vector-im/element-android/releases/tag/v1.1.5 diff --git a/fastlane/metadata/android/hu-HU/changelogs/40101160.txt b/fastlane/metadata/android/hu-HU/changelogs/40101160.txt new file mode 100644 index 0000000000..54f435eb96 --- /dev/null +++ b/fastlane/metadata/android/hu-HU/changelogs/40101160.txt @@ -0,0 +1,2 @@ +Fő változás ebben a verzióban: Titkosított üzenetküldés hibájának javítása ha valaki kilépett a szobából. +Teljes változásnapló: https://github.com/vector-im/element-android/releases/tag/v1.1.16 diff --git a/fastlane/metadata/android/hu-HU/changelogs/40102000.txt b/fastlane/metadata/android/hu-HU/changelogs/40102000.txt new file mode 100644 index 0000000000..87824693f7 --- /dev/null +++ b/fastlane/metadata/android/hu-HU/changelogs/40102000.txt @@ -0,0 +1,2 @@ +Fő változás ebben a verzióban: Hangüzenetek alapértelmezetten engedélyezettek. +Teljes változásnapló: https://github.com/vector-im/element-android/releases/tag/v1.2.0 diff --git a/fastlane/metadata/android/hu-HU/changelogs/40102010.txt b/fastlane/metadata/android/hu-HU/changelogs/40102010.txt new file mode 100644 index 0000000000..1ccd51aa8a --- /dev/null +++ b/fastlane/metadata/android/hu-HU/changelogs/40102010.txt @@ -0,0 +1,2 @@ +Fő változás ebben a verzióban: Sok fejlesztés a VoIP és Terek kapcsán (még béta) +Teljes változásnapló: https://github.com/vector-im/element-android/releases/tag/v1.2.1 diff --git a/fastlane/metadata/android/id/changelogs/40101160.txt b/fastlane/metadata/android/id/changelogs/40101160.txt new file mode 100644 index 0000000000..19209bacf2 --- /dev/null +++ b/fastlane/metadata/android/id/changelogs/40101160.txt @@ -0,0 +1,2 @@ +Perubahan utama dalam versi ini: Memperbaiki kesalahan saat mengirim pesan terenkripsi jika seseorang yang ada di ruangan keluar. +Changelog lanjutan: https://github.com/vector-im/element-android/releases/tag/v1.1.16 diff --git a/fastlane/metadata/android/id/changelogs/40102000.txt b/fastlane/metadata/android/id/changelogs/40102000.txt new file mode 100644 index 0000000000..2258b114e8 --- /dev/null +++ b/fastlane/metadata/android/id/changelogs/40102000.txt @@ -0,0 +1,2 @@ +Perubahan utama dalam versi ini: Pesan Suara diaktifkan secara default +Changelog lanjutan: https://github.com/vector-im/element-android/releases/tag/v1.1.16 diff --git a/fastlane/metadata/android/id/full_description.txt b/fastlane/metadata/android/id/full_description.txt index 0a18b8d64a..75249c6a20 100644 --- a/fastlane/metadata/android/id/full_description.txt +++ b/fastlane/metadata/android/id/full_description.txt @@ -8,10 +8,10 @@ Element adalah perpesanan yang aman dan aplikasi kolaborasi tim produktivitas ya - Obrolan video dengan VoIP dan berbagi layar - Integrasi yang mudah dengan alat kolaborasi online favorit Anda, alat manajemen proyek, layanan VoIP dan aplikasi perpesanan tim lainnya -Element benar-benar berbeda dari aplikasi perpesanan dan kolaborasi lainnya. Ini beroperasi pada Matrix, jaringan terbuka untuk pengiriman pesan yang aman dan komunikasi terdesentralisasi. Ini memungkinkan hosting sendiri untuk memberi pengguna kepemilikan maksimum dan kontrol data dan pesan mereka. +Element benar-benar berbeda dari aplikasi perpesanan dan kolaborasi lainnya. Element beroperasi pada Matrix, jaringan terbuka untuk pengiriman pesan yang aman dan komunikasi terdesentralisasi. Matrix memungkinkan hosting sendiri untuk memberi pengguna kepemilikan maksimum dan kontrol data dan pesan mereka. Pesan privasi dan terenkripsi -Element melindungi Anda dari iklan yang tidak diinginkan, data penambangan dan taman berdinding. Ini juga mengamankan semua data Anda, komunikasi video dan suara satu-ke-satu melalui enkripsi ujung-ke-ujung dan verifikasi perangkat yang di-cross-signed. +Element melindungi Anda dari iklan yang tidak diinginkan, data penambangan dan taman berdinding. Element juga mengamankan semua data Anda, komunikasi video dan suara satu-ke-satu melalui enkripsi ujung-ke-ujung dan verifikasi perangkat yang ditanda tangani silang. Element memberi Anda kendali atas privasi Anda sambil memungkinkan Anda untuk berkomunikasi dengan aman dengan siapa pun di jaringan Matrix, atau alat kolaborasi bisnis lainnya dengan mengintegrasikan dengan aplikasi seperti Slack. @@ -30,7 +30,7 @@ Element menempatkan Anda dalam kendali dengan cara yang berbeda: Anda dapat mengobrol dengan siapa saja di jaringan Matrix, apakah mereka menggunakan Element, aplikasi Matrix lain atau bahkan jika mereka menggunakan aplikasi perpesanan yang berbeda. Sangat aman -Enkripsi ujung-ke-ujung beneran (hanya mereka yang dalam percakapan dapat mendekripsi pesan), dan verifikasi perangkat yang di-cross-signed. +Enkripsi ujung-ke-ujung beneran (hanya mereka yang dalam percakapan dapat mendekripsi pesan), dan verifikasi perangkat yang ditanda tangani silang. Komunikasi dan integrasi lengkap Perpesanan, panggilan suara dan video, berbagi file, berbagi layar dan banyak integrasi, bot dan widget. Buat ruangan, komunitas, tetap terhubung dan selesaikan hal-hal. diff --git a/fastlane/metadata/android/it-IT/changelogs/40101160.txt b/fastlane/metadata/android/it-IT/changelogs/40101160.txt new file mode 100644 index 0000000000..1c217f50cc --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/40101160.txt @@ -0,0 +1,2 @@ +Modifiche principali in questa versione: corretto errore nell'invio di messaggi cifrati se qualcuno nella stanza si disconnette. +Cronologia completa: https://github.com/vector-im/element-android/releases/tag/v1.1.16 diff --git a/fastlane/metadata/android/it-IT/changelogs/40102000.txt b/fastlane/metadata/android/it-IT/changelogs/40102000.txt new file mode 100644 index 0000000000..e10007a7b7 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/40102000.txt @@ -0,0 +1,2 @@ +Modifiche principali in questa versione: i messaggi vocali sono attivi in modo predefinito. +Cronologia completa: https://github.com/vector-im/element-android/releases/tag/v1.2.0 diff --git a/fastlane/metadata/android/it-IT/changelogs/40102010.txt b/fastlane/metadata/android/it-IT/changelogs/40102010.txt new file mode 100644 index 0000000000..33c2d998a7 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/40102010.txt @@ -0,0 +1,2 @@ +Modifiche principali in questa versione: molti miglioramenti nel VoIP e negli Spazi (ancora in beta). +Cronologia completa: https://github.com/vector-im/element-android/releases/tag/v1.2.1 diff --git a/fastlane/metadata/android/ja-JP/changelogs/40101160.txt b/fastlane/metadata/android/ja-JP/changelogs/40101160.txt new file mode 100644 index 0000000000..a498487f46 --- /dev/null +++ b/fastlane/metadata/android/ja-JP/changelogs/40101160.txt @@ -0,0 +1,2 @@ +このバージョンの主な変更点:ルームにて誰かがログアウトした際に発生するエラーを修正しました。 +すべての変更履歴はこちら: https://github.com/vector-im/element-android/releases/tag/v1.1.16 diff --git a/fastlane/metadata/android/ja-JP/full_description.txt b/fastlane/metadata/android/ja-JP/full_description.txt index 855eb309c9..4e7b01cce3 100644 --- a/fastlane/metadata/android/ja-JP/full_description.txt +++ b/fastlane/metadata/android/ja-JP/full_description.txt @@ -1,30 +1,39 @@ -Elementはまったく新しいタイプのメッセンジャーアプリです。 +Elementはセキュアなメッセンジャーであると同時に、リモートワークでのグループチャットにも最適です。エンドツーエンドの暗号化を使用して、強力なビデオ会議、ファイル共有、音声通話を提供します。 -1. あなた自身がプライバシーをコントロールすることを可能にします。 -2. Matrixネットワークにいる誰とでも通信できることはもちろん、Slackなどのアプリとの連携によって他のネットワークとも通信ができます。 -3. 広告、データ収集、バックドア、ユーザーの囲い込みから逃れることができます。 -4. エンドツーエンド暗号化とクロス署名によってあなたを保護します。 +Elementの特徴 +- 高度なオンラインコミュニケーションツール +- 完全に暗号化されたメッセージ +- Matrixオープンソースフレームワークをベースにした分散型のチャット +- プロジェクトを管理しながら、暗号化されたデータで安全にファイル共有 +- Voice over IPによるビデオチャットと画面共有 +- お気に入りのオンラインコラボレーションツール、プロジェクト管理ツール、VoIPサービス、その他のチームメッセージングアプリと統合可能 -Elementは非中央集権型でオープンソースであるため、他のメッセンジャーアプリとは完全に異なっています。 +Elementは他のメッセージングアプリやコラボレーションアプリとは異なります。安全なメッセージングと分散型(非中央集権)コミュニケーションのためのオープンネットワークであるMatrixで動作します。また、ユーザーが自分のデータやメッセージを最大限にコントロールできるように、セルフホスティングも可能です。 -Elementはあなた自身でサーバーをホストすることも、サーバーを選ぶこともできます。これによってあなたのデータと会話に関するプライバシーや所有権はあなた自身で管理できるようになります。さらに、あなたは他のElementユーザーと話せるだけでなくオープンネットワークへのアクセスも可能です。とてもセキュアです。 +プライバシーと暗号化されたやりとり +Elementは、望ましくない広告、データマイニング、ウォールドガーデンからユーザーを保護します。また、エンド・ツー・エンドの暗号化と相互署名されたデバイスの検証により、すべてのデータ、1対1のビデオおよび音声通信を保護します。 -Elementは、オープンな分散型通信の標準規格であるMatrixで動作するため、これらすべてを実現することができています。 +Elementは、Slackなどのアプリと統合することで、Matrixネットワーク上の誰とでも安全にコミュニケーションをとることができると同時に、プライバシーをコントロールすることができます。 -Elementではあなたの会話をどのサーバーでホストするか決めることができます。アプリでは、さまざまな方法で選択できます。 +Elementはセルフホスティングが可能 +機密データや会話の管理を強化するために、Elementはセルフホスティングが可能で、オープンソースの分散型コミュニケーションの標準であるマトリックスベースのホストを選択することもできます。Elementは、プライバシー、セキュリティコンプライアンス、および統合の柔軟性を提供します。 -1. matrix.orgの公開サーバーで無料のアカウントを取得します。 -2. あなた自身のハードウェアでサーバーを動かし、アカウントを管理します。 -3. Element Matrix Servicesのホスティングプラットフォームに登録することで、カスタムサーバー上のアカウントを取得できます。 +データを所有する +データやメッセージをどこに保管するかは、お客様が決めることができます。データマイニングやサードパーティからのアクセスされません。 -なぜElementを選ぶべきなのか? +Elementではどのサーバーを使うか決めることができます。さまざまな方法で選択できます。 +1. 開発者がホストする matrix.org のパブリックサーバーで無料アカウントを取得するか、ボランティアがホストしているパブリックサーバーから選択する。 +2. 自分でサーバを実行することにより、アカウントをセルフホストする。 +3. Element Matrix Servicesのホスティングプラットフォームに加入しカスタムサーバー上でアカウントを作る。 -データの所有権: 自分でデータやメッセージを保管する場所を決めることができます。あなたが所有権を持ってコントロールすることで、第三者にあなたのデータを渡したり、ビッグデータを収集する巨大テック企業に依存する必要がなくなります。 +オープンなメッセージングとコラボレーション +Matrixネットワーク上の誰とでも、相手がElementを使っているか、他のMatrixアプリを使っていてもコミュニケーションすることができます。 -開かれたネットワークと共同作業: Matrixネットワーク内の他の誰とでも、あるいはElementや他のMatrixアプリを使っているかどうかに関わらず、またSlack、IRC、XMPPのような他のメッセージングシステムを使っているかどうかに関わらず、チャットすることができます。 +すごく安全 +本物のエンド・ツー・エンドの暗号化(会話に参加している人だけがメッセージを復号化できる)と、相互署名されたデバイスの検証を行います。 -はるかに安全: 本物のエンドツーエンド暗号化(会話に参加している者のみがメッセージを読める)と会話参加者の真正性を確認するためクロス署名によって。 +包括的なコミュニケーションと統合 +メッセージング、音声およびビデオ通話、ファイル共有、画面共有、その他多くの統合、ボット、ウィジェットを提供します。部屋やコミュニティを作り、連絡を取り合い、物事を成し遂げることができます。 -完全なるコミュニケーションの訪れ: テキスト、音声通話、ビデオ通話、ファイル共有、画面共有、連携機能、ボット、ウィジェットなどのコミュニケーションに必要な機能の全てが実装されています。ルームやコミュニティを立ち上げて連絡を取り合い、物事をスムーズに成し遂げることができます。 - -いつでもどこでも!: すべてのデバイスとウェブ(https://app.element.io)でメッセージの履歴が完全に同期されるため、どこにいても連絡を取ることができます。 +中断からの再開は +すべてのデバイスとウェブで完全に同期されたメッセージにより、どこにいても連絡を取り合うことができます。https://app.element.io diff --git a/fastlane/metadata/android/ja-JP/short_description.txt b/fastlane/metadata/android/ja-JP/short_description.txt index c3991b7a93..0d37b108ac 100644 --- a/fastlane/metadata/android/ja-JP/short_description.txt +++ b/fastlane/metadata/android/ja-JP/short_description.txt @@ -1 +1 @@ -安全な分散型チャットとVoIP。あなたの情報が第三者から守られます。 +メッセンジャー - 暗号化されたメッセージング、グループチャット、ビデオコールなど diff --git a/fastlane/metadata/android/ja-JP/title.txt b/fastlane/metadata/android/ja-JP/title.txt index 376f4a95de..7c2b777407 100644 --- a/fastlane/metadata/android/ja-JP/title.txt +++ b/fastlane/metadata/android/ja-JP/title.txt @@ -1 +1 @@ -Element(エレメントメッセンジャー) +Element - セキュアメッセンジャー diff --git a/fastlane/metadata/android/no-NO/changelogs/40101040.txt b/fastlane/metadata/android/no-NO/changelogs/40101040.txt new file mode 100644 index 0000000000..5d1ecba7fc --- /dev/null +++ b/fastlane/metadata/android/no-NO/changelogs/40101040.txt @@ -0,0 +1,2 @@ +Hovedendringer i denne versjonen: forbedring av ytelsen og feilrettinger! +Full endringslogg: https://github.com/vector-im/element-android/releases/tag/v1.1.4 diff --git a/fastlane/metadata/android/no-NO/changelogs/40101050.txt b/fastlane/metadata/android/no-NO/changelogs/40101050.txt new file mode 100644 index 0000000000..3ddcb9d04b --- /dev/null +++ b/fastlane/metadata/android/no-NO/changelogs/40101050.txt @@ -0,0 +1,2 @@ +Hovedendringer i denne versjonen: hurtigreparasjoner for 1.1.4 +Full endringslogg: https://github.com/vector-im/element-android/releases/tag/v1.1.5 diff --git a/fastlane/metadata/android/no-NO/changelogs/40101060.txt b/fastlane/metadata/android/no-NO/changelogs/40101060.txt new file mode 100644 index 0000000000..c556359851 --- /dev/null +++ b/fastlane/metadata/android/no-NO/changelogs/40101060.txt @@ -0,0 +1,2 @@ +Hovedendringer i denne versjonen: hurtigreparasjoner for 1.1.5 +Full endringslogg: https://github.com/vector-im/element-android/releases/tag/v1.1.6 diff --git a/fastlane/metadata/android/no-NO/changelogs/40101070.txt b/fastlane/metadata/android/no-NO/changelogs/40101070.txt new file mode 100644 index 0000000000..bc786eb454 --- /dev/null +++ b/fastlane/metadata/android/no-NO/changelogs/40101070.txt @@ -0,0 +1,2 @@ +Hovedendringer i denne versjonen: beta støtte for Mellomrom. Komprimer video før du sender. +Full endringslogg: https://github.com/vector-im/element-android/releases/tag/v1.1.7 diff --git a/fastlane/metadata/android/no-NO/changelogs/40101080.txt b/fastlane/metadata/android/no-NO/changelogs/40101080.txt new file mode 100644 index 0000000000..02d6aa868b --- /dev/null +++ b/fastlane/metadata/android/no-NO/changelogs/40101080.txt @@ -0,0 +1,2 @@ +Hovedendringer i denne versjonen: forbedring for Mellomrom. +Full endringslogg: https://github.com/vector-im/element-android/releases/tag/v1.1.8 diff --git a/fastlane/metadata/android/no-NO/changelogs/40101090.txt b/fastlane/metadata/android/no-NO/changelogs/40101090.txt new file mode 100644 index 0000000000..cdcb4909c7 --- /dev/null +++ b/fastlane/metadata/android/no-NO/changelogs/40101090.txt @@ -0,0 +1,2 @@ +Hovedendringer i denne versjonen: legg til støtte for gitter.im -nettverket. +Full endringslogg: https://github.com/vector-im/element-android/releases/tag/v1.1.9 diff --git a/fastlane/metadata/android/no-NO/changelogs/40101100.txt b/fastlane/metadata/android/no-NO/changelogs/40101100.txt new file mode 100644 index 0000000000..8a397f63bb --- /dev/null +++ b/fastlane/metadata/android/no-NO/changelogs/40101100.txt @@ -0,0 +1,2 @@ +Hovedendringer i denne versjonen: oppdatering av tema og stil og nye funksjoner for mellomrom. +Full endringslogg: https://github.com/vector-im/element-android/releases/tag/v1.1.10 diff --git a/fastlane/metadata/android/no-NO/changelogs/40101110.txt b/fastlane/metadata/android/no-NO/changelogs/40101110.txt new file mode 100644 index 0000000000..2db46e97a8 --- /dev/null +++ b/fastlane/metadata/android/no-NO/changelogs/40101110.txt @@ -0,0 +1,2 @@ +Hovedendringer i denne versjonen: oppdatering av tema og stil og nye funksjoner for mellomrom (feilrettelse for 1.1.10) +Full endringslogg: https://github.com/vector-im/element-android/releases/tag/v1.1.11 diff --git a/fastlane/metadata/android/no-NO/changelogs/40101120.txt b/fastlane/metadata/android/no-NO/changelogs/40101120.txt new file mode 100644 index 0000000000..6b7e160a80 --- /dev/null +++ b/fastlane/metadata/android/no-NO/changelogs/40101120.txt @@ -0,0 +1,2 @@ +Hovedendringer i denne versjonen: tema og stiloppdatering og fikse et krasj etter videosamtale +Full endringslogg: https://github.com/vector-im/element-android/releases/tag/v1.1.12 diff --git a/fastlane/metadata/android/no-NO/changelogs/40101130.txt b/fastlane/metadata/android/no-NO/changelogs/40101130.txt new file mode 100644 index 0000000000..db67b87bf9 --- /dev/null +++ b/fastlane/metadata/android/no-NO/changelogs/40101130.txt @@ -0,0 +1,2 @@ +Hovedendringer i denne versjonen: hovedsakelig stabilitet og oppdateringer av feilrettinger. +Full endringslogg: https://github.com/vector-im/element-android/releases/tag/v1.1.13 diff --git a/fastlane/metadata/android/no-NO/changelogs/40101140.txt b/fastlane/metadata/android/no-NO/changelogs/40101140.txt new file mode 100644 index 0000000000..7921bd03e0 --- /dev/null +++ b/fastlane/metadata/android/no-NO/changelogs/40101140.txt @@ -0,0 +1,2 @@ +Hovedendringer i denne versjonen: Løs et problem om krypterte meldinger. +Full endringslogg: https://github.com/vector-im/element-android/releases/tag/v1.1.14 diff --git a/fastlane/metadata/android/no-NO/changelogs/40101150.txt b/fastlane/metadata/android/no-NO/changelogs/40101150.txt new file mode 100644 index 0000000000..2f4fcb8266 --- /dev/null +++ b/fastlane/metadata/android/no-NO/changelogs/40101150.txt @@ -0,0 +1,2 @@ +Hovedendringer i denne versjonen: implementering av talemeldinger under laboratorieinnstillinger. +Full endringslogg: https://github.com/vector-im/element-android/releases/tag/v1.1.15 diff --git a/fastlane/metadata/android/no-NO/changelogs/40101160.txt b/fastlane/metadata/android/no-NO/changelogs/40101160.txt new file mode 100644 index 0000000000..d7f7ca2387 --- /dev/null +++ b/fastlane/metadata/android/no-NO/changelogs/40101160.txt @@ -0,0 +1,2 @@ +Hovedendringer i denne versjonen: Løs feil ved sending av kryptert melding hvis noen i rommet logger av. +Full endringslogg: https://github.com/vector-im/element-android/releases/tag/v1.1.16 diff --git a/fastlane/metadata/android/no-NO/full_description.txt b/fastlane/metadata/android/no-NO/full_description.txt index 92a3c4c5c3..08d2383532 100644 --- a/fastlane/metadata/android/no-NO/full_description.txt +++ b/fastlane/metadata/android/no-NO/full_description.txt @@ -1,30 +1,39 @@ -Element er en ny type messenger og samarbeidsapp som: +Element er både en sikker budbringer og en produktivitets team -samarbeidsprogram som er ideell for gruppechatter mens du jobber på fjernkontroll. Denne chat-appen bruker ende-til-ende-kryptering for å tilby kraftige videokonferanser, fildeling og taleanrop. -1. Gir deg kontrollen for å bevare personvernet ditt -2. Lar deg kommunisere med hvem som helst i Matrix-nettverket, og til og med ved å integrere med apper som Slack -3. Beskytter deg mot reklame, datamining og inngjerdede hager -4. Sikrer deg gjennom end-to-end-kryptering, med kryssignering for å bekrefte andre + Elementets funksjoner inkluderer: +- Avanserte elektroniske kommunikasjonsverktøy +- Fullt krypterte meldinger for å tillate tryggere bedriftskommunikasjon, selv for eksterne arbeidere +- Desentralisert chat basert på Matrix open source -rammeverket +- Fildeling sikkert med krypterte data mens du administrerer prosjekter +- Videochatter med Voice over IP og skjermdeling +- Enkel integrering med dine favoritt online samarbeidsverktøy, prosjektstyringsverktøy, VoIP -tjenester og andre teammeldingsapper -Element er helt forskjellig fra andre meldings- og samarbeidsapper fordi det er desentralisert og åpen kildekode. +Element er helt annerledes enn andre meldings- og samarbeidsapper. Den opererer på Matrix, et åpent nettverk for sikre meldinger og desentralisert kommunikasjon. Det gjør det mulig for egen hosting å gi brukerne maksimal eierskap og kontroll over sine data og meldinger. -Element lar deg selv være vert - eller velge en vert - slik at du har personvern, eierskap og kontroll over dataene og samtalene dine. Det gir deg tilgang til et åpent nettverk; slik at du ikke bare holder på å snakke med bare andre Element-brukere. Og det er veldig sikkert. + Personvern og krypterte meldinger +Element beskytter deg mot uønskede annonser, datautvinning og inngjerdede hager. Den sikrer også alle dataene dine, en-til-en video- og talekommunikasjon gjennom ende-til-ende-kryptering og kryssignert enhetsverifisering. -Element er i stand til å gjøre alt dette fordi det opererer på Matrix - standarden for åpen, desentralisert kommunikasjon. +Element gir deg kontroll over personvernet ditt, samtidig som du kan kommunisere trygt med alle på Matrix -nettverket eller andre forretningssamarbeidsverktøy ved å integrere med apper som Slack. -Element setter deg i kontroll ved å la deg velge hvem som er vert for samtalene dine. Fra Element-appen kan du velge å være vert på forskjellige måter: + Elementet kan hostes selv +For å tillate mer kontroll over dine sensitive data og samtaler, kan Element være egenvert, eller du kan velge hvilken som helst Matrix-basert vert-standarden for åpen kildekode, desentralisert kommunikasjon. Element gir deg personvern, overholdelse av sikkerhet og fleksibilitet for integrering. -1. Få en gratis konto på matrix.org-serveren som er vert for Matrix-utviklerne, eller velg blant tusenvis av offentlige servere som er vert for frivillige -2. Vær vert for kontoen din ved å kjøre en server på din egen maskinvare -3. Registrer deg for en konto på en tilpasset server ved å bare abonnere på Hosting Matrix Services-vertsplattformen + Ei dataene dine +Du bestemmer hvor du vil beholde dataene og meldingene dine. Uten risiko for datautvinning eller tilgang fra tredjeparter. - Hvorfor velge Element? +Element gir deg kontroll på forskjellige måter: +1. Få en gratis konto på den matrix.org offentlige serveren som Matrix -utviklerne er vert for, eller velg blant tusenvis av offentlige servere som er arrangert av frivillige +2. Vær vert for kontoen din ved å kjøre en server på din egen IT-infrastruktur +3. Registrer deg for en konto på en tilpasset server ved ganske enkelt å abonnere på Element Matrix Services hosting -plattform - EGNE DATA DINE : Du bestemmer hvor du vil oppbevare dataene og meldingene dine. Du eier den og kontrollerer den, ikke noe MEGACORP som utvinner dataene dine eller gir tilgang til tredjeparter. + Åpen melding og samarbeid +Du kan chatte med alle på Matrix -nettverket, enten de bruker Element, en annen Matrix -app eller til og med om de bruker en annen meldingsapp. - ÅPEN MELDING OG SAMARBEID : Du kan chatte med alle andre i Matrix-nettverket, enten de bruker Element eller en annen Matrix-app, og selv om de bruker et annet meldingssystem som Slack, IRC eller XMPP. + Super sikker +Ekte ende-til-ende-kryptering (bare de i samtalen kan dekryptere meldinger) og kryssignert enhetsbekreftelse. - SUPER-SECURE : Ekte end-to-end-kryptering (bare de i samtalen kan dekryptere meldinger), og kryssignering for å verifisere enhetene til samtaledeltakerne. + Fullstendig kommunikasjon og integrering +Meldinger, tale- og videosamtaler, fildeling, skjermdeling og en hel haug med integrasjoner, bots og widgets. Bygg rom, lokalsamfunn, hold kontakten og få ting gjort. - KOMPLETT KOMMUNIKASJON : Meldinger, tale- og videosamtaler, fildeling, skjermdeling og en hel haug med integrasjoner, bots og widgets. Bygg rom, lokalsamfunn, hold kontakten og få ting gjort. - - ALT DER DU ER : Hold kontakten uansett hvor du er med fullt synkronisert meldingslogg på alle enhetene dine og på nettet på https://app.element.io. + Hent der du slapp +Hold kontakten uansett hvor du er med fullt synkronisert meldingshistorikk på alle enhetene dine og på nettet på https://app.element.io diff --git a/fastlane/metadata/android/no-NO/short_description.txt b/fastlane/metadata/android/no-NO/short_description.txt index b7cad4c849..04c4a8c2e5 100644 --- a/fastlane/metadata/android/no-NO/short_description.txt +++ b/fastlane/metadata/android/no-NO/short_description.txt @@ -1 +1 @@ -Sikker desentralisert chat & VoIP. Beskytt dataene dine fra tredjeparter. +Gruppe meldinger - kryptert meldinger, gruppechat og videosamtaler diff --git a/fastlane/metadata/android/pt-BR/changelogs/40101160.txt b/fastlane/metadata/android/pt-BR/changelogs/40101160.txt new file mode 100644 index 0000000000..9ed83f641b --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/40101160.txt @@ -0,0 +1,2 @@ +Principais mudanças nesta versão: Consertar erro quando enviando mensagem encriptada se alguém na sala faz logout. +Changelog completo: https://github.com/vector-im/element-android/releases/tag/v1.1.16 diff --git a/fastlane/metadata/android/pt-BR/changelogs/40102000.txt b/fastlane/metadata/android/pt-BR/changelogs/40102000.txt new file mode 100644 index 0000000000..c6d01391da --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/40102000.txt @@ -0,0 +1,2 @@ +Principais mudanças nesta versão: Mensagem de Voz está habilitada por default. +Changelog completo: https://github.com/vector-im/element-android/releases/tag/v1.2.0 diff --git a/fastlane/metadata/android/pt-BR/changelogs/40102010.txt b/fastlane/metadata/android/pt-BR/changelogs/40102010.txt new file mode 100644 index 0000000000..0894dd2022 --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/40102010.txt @@ -0,0 +1,2 @@ +Principais mudanças nesta versão: Muitas melhorias em VoIP e Espaços (ainda em beta). +Changelog completo: https://github.com/vector-im/element-android/releases/tag/v1.2.1 diff --git a/fastlane/metadata/android/ru-RU/changelogs/40101150.txt b/fastlane/metadata/android/ru-RU/changelogs/40101150.txt new file mode 100644 index 0000000000..cbf64e470b --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/40101150.txt @@ -0,0 +1,2 @@ +Основные изменения в этой версии: реализация голосовых сообщений в настройках лабораторий. +Полный список изменений: https://github.com/vector-im/element-android/releases/tag/v1.1.15 diff --git a/fastlane/metadata/android/ru-RU/changelogs/40101160.txt b/fastlane/metadata/android/ru-RU/changelogs/40101160.txt new file mode 100644 index 0000000000..5f0e555d94 --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/40101160.txt @@ -0,0 +1,2 @@ +Основные изменения в этой версии: Исправление ошибки при отправке зашифрованного сообщения, если кто-то в комнате выходит. +Полный список изменений: https://github.com/vector-im/element-android/releases/tag/v1.1.16 diff --git a/fastlane/metadata/android/ru-RU/changelogs/40102000.txt b/fastlane/metadata/android/ru-RU/changelogs/40102000.txt new file mode 100644 index 0000000000..af0a444afa --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/40102000.txt @@ -0,0 +1,2 @@ +Основные изменения в этой версии: Голосовое сообщение включено по умолчанию. +Полный список изменений: https://github.com/vector-im/element-android/releases/tag/v1.2.0 diff --git a/fastlane/metadata/android/ru-RU/changelogs/40102010.txt b/fastlane/metadata/android/ru-RU/changelogs/40102010.txt new file mode 100644 index 0000000000..167af260d5 --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/40102010.txt @@ -0,0 +1,2 @@ +Основные изменения в этой версии: Множество улучшений в VoIP и пространствах (все еще в бета-версии). +Полный список изменений: https://github.com/vector-im/element-android/releases/tag/v1.2.1 diff --git a/fastlane/metadata/android/sq/changelogs/40100100.txt b/fastlane/metadata/android/sq/changelogs/40100100.txt new file mode 100644 index 0000000000..aba7bebd5a --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40100100.txt @@ -0,0 +1,2 @@ +Ky version i ri përmban kryesisht ndreqje të metash dhe përmirësime. Dërgimi i një mesazhi tani është shumë i shpejtë. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases/tag/v1.0.10 diff --git a/fastlane/metadata/android/sq/changelogs/40100110.txt b/fastlane/metadata/android/sq/changelogs/40100110.txt new file mode 100644 index 0000000000..d1b8e9f9d3 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40100110.txt @@ -0,0 +1,2 @@ +Ky version i ri përmban kryesisht përmirësime të ndërfaqes dhe punimit të përdoruesit. Tani mund të ftoni shokë, dhe të krijoni MD shumë shpejt, përmes skanimit të kodesh QR. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases/tag/v1.0.11 diff --git a/fastlane/metadata/android/sq/changelogs/40100120.txt b/fastlane/metadata/android/sq/changelogs/40100120.txt new file mode 100644 index 0000000000..d7d9998e0b --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40100120.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: Paraparje URL-sh, tastierë e re për emoji, aftësi të reja për rregullime dhome, dhe dëborë për Krishtlindje! +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases/tag/v1.0.12 diff --git a/fastlane/metadata/android/sq/changelogs/40100130.txt b/fastlane/metadata/android/sq/changelogs/40100130.txt new file mode 100644 index 0000000000..5d50ff531d --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40100130.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: Paraparje URL-sh, tastierë e re për emoji, aftësi të reja për rregullime dhome, dhe dëborë për Krishtlindje! +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases/tag/v1.0.13 diff --git a/fastlane/metadata/android/sq/changelogs/40100140.txt b/fastlane/metadata/android/sq/changelogs/40100140.txt new file mode 100644 index 0000000000..bdab3841b0 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40100140.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: Përpunim lejesh dhome, temë e çelët/e errët e automatizuar, dhe një dorë ndreqjesh të metash. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases/tag/v1.0.14 diff --git a/fastlane/metadata/android/sq/changelogs/40100150.txt b/fastlane/metadata/android/sq/changelogs/40100150.txt new file mode 100644 index 0000000000..045f2369b6 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40100150.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: Mbulim Hyrjesh nga rrjete shoqërorë. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases/tag/v1.0.15 diff --git a/fastlane/metadata/android/sq/changelogs/40100160.txt b/fastlane/metadata/android/sq/changelogs/40100160.txt new file mode 100644 index 0000000000..ece7bbd2a6 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40100160.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: Mbulim Hyrjesh nga rrjete shoqërorë. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases/tag/v1.0.15 dhe https://github.com/vector-im/element-android/releases/tag/v1.0.16 diff --git a/fastlane/metadata/android/sq/changelogs/40100170.txt b/fastlane/metadata/android/sq/changelogs/40100170.txt new file mode 100644 index 0000000000..76cf7a9ffa --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40100170.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: Ndreqje të metash! +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases/tag/v1.0.17 diff --git a/fastlane/metadata/android/sq/changelogs/40101000.txt b/fastlane/metadata/android/sq/changelogs/40101000.txt new file mode 100644 index 0000000000..b4424f55bc --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40101000.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: Përmirësime për VoIP (thirrje audio dhe video në DM) dhe ndreqje të metash! +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases/tag/v1.1.0 diff --git a/fastlane/metadata/android/sq/changelogs/40101010.txt b/fastlane/metadata/android/sq/changelogs/40101010.txt new file mode 100644 index 0000000000..20b35d3439 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40101010.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: përmirësime funksionimi dhe ndreqje të metash! +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases/tag/v1.1.1 diff --git a/fastlane/metadata/android/sq/changelogs/40101020.txt b/fastlane/metadata/android/sq/changelogs/40101020.txt new file mode 100644 index 0000000000..6f53ec219e --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40101020.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: përmirësime funksionimi dhe ndreqje të metash! +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases/tag/v1.1.2 diff --git a/fastlane/metadata/android/sq/changelogs/40101030.txt b/fastlane/metadata/android/sq/changelogs/40101030.txt new file mode 100644 index 0000000000..9dbc4f142a --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40101030.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: përmirësime funksionimi dhe ndreqje të metash! +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases/tag/v1.1.3 diff --git a/fastlane/metadata/android/sq/changelogs/40101040.txt b/fastlane/metadata/android/sq/changelogs/40101040.txt new file mode 100644 index 0000000000..949fa629b9 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40101040.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: përmirësime funksionimi dhe ndreqje të metash! +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases/tag/v1.1.4 diff --git a/fastlane/metadata/android/sq/changelogs/40101050.txt b/fastlane/metadata/android/sq/changelogs/40101050.txt new file mode 100644 index 0000000000..28e2852356 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40101050.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: ndreqje të metash për 1.1.4 +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases/tag/v1.1.5 diff --git a/fastlane/metadata/android/sq/changelogs/40101060.txt b/fastlane/metadata/android/sq/changelogs/40101060.txt new file mode 100644 index 0000000000..ab01eede45 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40101060.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: ndreqje të metash për 1.1.5 +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases/tag/v1.1.6 diff --git a/fastlane/metadata/android/sq/changelogs/40101070.txt b/fastlane/metadata/android/sq/changelogs/40101070.txt new file mode 100644 index 0000000000..8d23bb5b94 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40101070.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: mbulim për Hapësira, në fazë beta. Ngjeshje videosh, përpara dërgimi. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases/tag/v1.1.7 diff --git a/fastlane/metadata/android/sq/changelogs/40101080.txt b/fastlane/metadata/android/sq/changelogs/40101080.txt new file mode 100644 index 0000000000..f9282142cb --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40101080.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: përmirësime për Hapësira. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases/tag/v1.1.8 diff --git a/fastlane/metadata/android/sq/changelogs/40101090.txt b/fastlane/metadata/android/sq/changelogs/40101090.txt new file mode 100644 index 0000000000..069ab4954d --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40101090.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: shtim mbulimi për rrjetin gitter.im. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases/tag/v1.1.9 diff --git a/fastlane/metadata/android/sq/changelogs/40101100.txt b/fastlane/metadata/android/sq/changelogs/40101100.txt new file mode 100644 index 0000000000..bf5079bc9a --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40101100.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: përditësime teme dhe stili dhe veçori të reja për hapësira. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases/tag/v1.1.10 diff --git a/fastlane/metadata/android/sq/changelogs/40101110.txt b/fastlane/metadata/android/sq/changelogs/40101110.txt new file mode 100644 index 0000000000..44d03bb8cb --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40101110.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: përditësime teme dhe stili dhe veçori të reja për hapësira (ndreqje të mete për 1.1.10) +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases/tag/v1.1.11 diff --git a/fastlane/metadata/android/sq/changelogs/40101120.txt b/fastlane/metadata/android/sq/changelogs/40101120.txt new file mode 100644 index 0000000000..aecede8d91 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40101120.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: përditësime teme dhe stili dhe ndreqje e një vithisjeje pas një thirrjeje video +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases/tag/v1.1.12 diff --git a/fastlane/metadata/android/sq/changelogs/40101130.txt b/fastlane/metadata/android/sq/changelogs/40101130.txt new file mode 100644 index 0000000000..535ccd7518 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40101130.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: përditësim kryesisht për qëndrueshmërinë dhe ndreqje të metash. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases/tag/v1.1.13 diff --git a/fastlane/metadata/android/sq/changelogs/40101140.txt b/fastlane/metadata/android/sq/changelogs/40101140.txt new file mode 100644 index 0000000000..2dc279e1f7 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40101140.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: ndreqje e një problemi rreth mesazhesh të fshehtëzuar. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases/tag/v1.1.14 diff --git a/fastlane/metadata/android/sq/changelogs/40101150.txt b/fastlane/metadata/android/sq/changelogs/40101150.txt new file mode 100644 index 0000000000..1fbf2bae7a --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40101150.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: sendërtim mesazhesh zanore, nën mjedis laboratori. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases/tag/v1.1.15 diff --git a/fastlane/metadata/android/sq/changelogs/40101160.txt b/fastlane/metadata/android/sq/changelogs/40101160.txt new file mode 100644 index 0000000000..ecb9a83918 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40101160.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: Ndreqje gabimi, kur dërgohet mesazh i fshehtëzuar, nëse dikush nga dhoma bën dalje prej saj. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases/tag/v1.1.16 diff --git a/fastlane/metadata/android/sq/changelogs/40102000.txt b/fastlane/metadata/android/sq/changelogs/40102000.txt new file mode 100644 index 0000000000..c1f2333f1c --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40102000.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: Mesazh Zanor është i aktivizuar, si parazgjedhje. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases/tag/v1.2.0 diff --git a/fastlane/metadata/android/sq/changelogs/40102010.txt b/fastlane/metadata/android/sq/changelogs/40102010.txt new file mode 100644 index 0000000000..6ffe456bd4 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40102010.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: Mjaft përmirësime në VoIP dhe Hapësira (ende në beta). +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases/tag/v1.2.1 diff --git a/fastlane/metadata/android/sq/full_description.txt b/fastlane/metadata/android/sq/full_description.txt new file mode 100644 index 0000000000..96cf102272 --- /dev/null +++ b/fastlane/metadata/android/sq/full_description.txt @@ -0,0 +1,39 @@ +Element-i është si aplikacion shkëmbyes i sigurt mesazhesh, ashtu edhe bashkëpunimi prodhimtar ekipi, i cili është ideal për fjalosje në grup, teksa punohet së largët. Ky aplikacion fjalosjeje përdor fshehtëzim skaj-më-skaj për të furnizuar konferencë video, shkëmbim kartelash dhe thirrje me zë të fuqishme. + +Në veçoritë e Element-it përfshihen: +- Mjete të thelluara komunikimi internetor +- Mesazhe plotësisht të fshehtëzuar, për të lejuar komunikim në nivel korporate, madje edhe për punonjës së largëti +- Fjalosje e decentralizuar, bazuar në platformën me burim të hapët Matrix +- Shkëmbim i sigurt kartelash, me të dhëna të fshehtëzuara, teksa administrohen projekte +- Fjalosje video të llojit VoIP dhe tregim ekrani +- Integrim i kollajtë me mjetet tuaja të parapëlqyera të bashkëpunimit internetor, mjete administrimi projektesh, shërbime VoIP dhe aplikacione të tjera shkëmbimi mesazhesh në ekip + +Element-i është plotësisht i ndryshëm nga aplikacione të tjera shkëmbimi mesazhesh dhe bashkëpunimi. Funksionimi i tij bazohet në Matrix, një rrjet i hapët për mesazhe të siguruar dhe komunikim të decentralizuar. Lejon vetëstrehim, për t’u lejuar përdoruesve pronësi dhe kontroll maksimal të të dhënave dhe mesazheve të tyre. + +Privatësi dhe shkëmbim mesazhesh të fshehtëzuar +Element-i ju mbron nga reklama të padëshiruara, shfrytëzim të dhënash dhe vatha dixhitale. Ai siguron gjithashtu krejt të dhënat tuaja, komunikime tek-për-tek me video dhe me zë, përmes fshehtëzimi skaj-më-skaj dhe verifikim “cross-signed” pajisjesh. + +Element-i ju jep kontrollin e privatësisë tuaj, teksa ju lejon të komunikoni në mënyrë të siguruar me këdo në rrjetin Matrix, ose me mjete të tjera bashkëpunimi në shkallë biznesi, duke u integruar me aplikacione të tillë si Slack. + +Element-i mund të vetëstrehohet +Për të lejuar më tepër kontroll mbi të dhënat dhe bisedat tuaja rezervat, Element-i mund të vetëstrehohet, ose mund të zgjidhni cilëndo strehë të bazuar në Matrix - standardi për komunikim me burim të hapët, të decentralizuar. Element-i ju jep privatësi, pajtueshmëri sigurie dhe zhdërvjelltësi integrimesh. + +Jini zot i të dhënave tuaja +Ju vendosni ku të mbahen të dhënat dhe mesazhet tuaja. Pa rrezikun e shfrytëzimit të të dhënave apo hyrjes në to nga palë të treta. + +Element-i ju vë ju në kontroll përmes rrugësh të ndryshme: +1. Merrni një llogari falas te shërbyesi publik matrix.org strehuar nga zhvillues të Matrix-it, ose zgjidhni prej mijëra shërbyesish publikë të strehuar nga vullnetarë +2. Vetëstrehoni llogarinë tuaj duke xhiruar një shërbyes në infrastrukturën tuaj TI +3. Regjistrohuni për një llogari në një shërbyes vetjak, thjesht duke u pajtuar te platforma Element Matrix Services e strehimeve + +Shkëmbim mesazhesh dhe bashkëpunim me burim të hapët +Mund të fjaloseni me këdo në rrjetin Matrix, qoftë kur përdorin Element, një tjetër aplikacion Matrix, apo edhe kur përdorin një tjetër aplikacion shkëmbimi mesazhesh. + +Super i sigurt +Fshehtëzim i njëmendtë skaj-më-skaj (vetëm ata te biseda mund të shfshehtëzojnë mesazhe), dhe verifikim “cross-signed” pajisjesh. + +Komunikim dhe integrim i plotë +Shkëmbim mesazhesh, thirrje me zë dhe me video, shkëmbim kartelash, tregim ekrani dhe një grup i tërë integrimesh, robotësh dhe widget-esh. Krijoni dhoma, bashkësi, mbani lidhjet dhe mbaroni punë. + +Rifillojani atje ku e latë +Jini në dijeni, kudo ku gjendeni, me historik plotësisht të njëkohësuar mesazhesh nëpër krejt pajisjet tuaja dhe në internet te https://app.element.io diff --git a/fastlane/metadata/android/sq/short_description.txt b/fastlane/metadata/android/sq/short_description.txt new file mode 100644 index 0000000000..21937ccce5 --- /dev/null +++ b/fastlane/metadata/android/sq/short_description.txt @@ -0,0 +1 @@ +Mesazhe grupi - mesazhe, fjalosje në grup dhe thirrje me video, të fshehtëzuara diff --git a/fastlane/metadata/android/sq/title.txt b/fastlane/metadata/android/sq/title.txt new file mode 100644 index 0000000000..097f9c48ea --- /dev/null +++ b/fastlane/metadata/android/sq/title.txt @@ -0,0 +1 @@ +Element - Shkëmbyes i Sigurt Mesazhesh diff --git a/fastlane/metadata/android/sv-SE/changelogs/40101160.txt b/fastlane/metadata/android/sv-SE/changelogs/40101160.txt new file mode 100644 index 0000000000..cfdf3b54d4 --- /dev/null +++ b/fastlane/metadata/android/sv-SE/changelogs/40101160.txt @@ -0,0 +1,2 @@ +Huvudsakliga ändringar i den här versionen: Fixa fel vid sändning av krypterade meddelanden om någon i rummet loggar ut. +Full ändringslogg: https://github.com/vector-im/element-android/releases/tag/v1.1.16 diff --git a/fastlane/metadata/android/sv-SE/changelogs/40102000.txt b/fastlane/metadata/android/sv-SE/changelogs/40102000.txt new file mode 100644 index 0000000000..c31355dc09 --- /dev/null +++ b/fastlane/metadata/android/sv-SE/changelogs/40102000.txt @@ -0,0 +1,2 @@ +Huvudsakliga ändringar i den här versionen: Röstmeddelanden är aktiverade som förval. +Full ändringslogg: https://github.com/vector-im/element-android/releases/tag/v1.2.0 diff --git a/fastlane/metadata/android/uk/changelogs/40101150.txt b/fastlane/metadata/android/uk/changelogs/40101150.txt new file mode 100644 index 0000000000..c3e724a8a0 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/40101150.txt @@ -0,0 +1,2 @@ +Основні зміни в цій версії: впровадження голосових повідомлень у налаштуваннях лабораторії. +Повний журнал змін: https://github.com/vector-im/element-android/releases/tag/v1.1.15 diff --git a/fastlane/metadata/android/uk/changelogs/40101160.txt b/fastlane/metadata/android/uk/changelogs/40101160.txt new file mode 100644 index 0000000000..fe7bb6c7cc --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/40101160.txt @@ -0,0 +1,2 @@ +Основні зміни у цій версії: виправлення помилок надсилання зашифрованого повідомлення, якщо хтось виходить з кімнати. +Усі зміни: https://github.com/vector-im/element-android/releases/tag/v1.1.16 diff --git a/fastlane/metadata/android/uk/changelogs/40102000.txt b/fastlane/metadata/android/uk/changelogs/40102000.txt new file mode 100644 index 0000000000..9abc8c0298 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/40102000.txt @@ -0,0 +1,2 @@ +Основні зміни в цій версії: голосові повідомлення типово увімкнено. +Повний журнал змін: https://github.com/vector-im/element-android/releases/tag/v1.2.0 diff --git a/fastlane/metadata/android/uk/changelogs/40102010.txt b/fastlane/metadata/android/uk/changelogs/40102010.txt new file mode 100644 index 0000000000..39a8d839b6 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/40102010.txt @@ -0,0 +1,2 @@ +Основні зміни в цій версії: багато вдосконалень VoIP і просторів (досі бета) +Повний журнал змін: https://github.com/vector-im/element-android/releases/tag/v1.2.1 diff --git a/fastlane/metadata/android/uk/full_description.txt b/fastlane/metadata/android/uk/full_description.txt index df06315754..285f577452 100644 --- a/fastlane/metadata/android/uk/full_description.txt +++ b/fastlane/metadata/android/uk/full_description.txt @@ -33,7 +33,7 @@ Element надає такі можливості на вибір: Справжнє наскрізне шифрування (лише учасники бесіди можуть розшифровувати повідомлення) та взаємне підписування пристроїв. Повноцінні спілкування та інтеграція -Обмін повідомленнями, голосові та відеовиклики, обмін файлами, спільний доступ до екрана та ціла купа інтеграцій, ботів та віджетів. Створюйте кімнати, спільноти, залишайтеся на зв’язку та виконуйте завдання. +Обмін повідомленнями, голосові та відеовиклики, обмін файлами, спільний доступ до екрана та ціла купа інтеграцій, ботів та розширень. Створюйте кімнати, спільноти, залишайтеся на зв’язку та виконуйте завдання. Продовжуйте, де зупинилися Залишайтеся на зв'язку, де б ви не знаходились, з повністю синхронізованою історією повідомлень на всіх своїх пристроях та в Інтернеті за адресою https://app.element.io diff --git a/fastlane/metadata/android/zh-CN/changelogs/40101120.txt b/fastlane/metadata/android/zh-CN/changelogs/40101120.txt new file mode 100644 index 0000000000..06d0540f58 --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/40101120.txt @@ -0,0 +1,2 @@ +此版本的主要变化:主题和样式更新,以及修复视频通话后崩溃的问题 +完整更新日志:https://github.com/vector-im/element-android/releases/tag/v1.1.12 diff --git a/fastlane/metadata/android/zh-CN/changelogs/40101130.txt b/fastlane/metadata/android/zh-CN/changelogs/40101130.txt new file mode 100644 index 0000000000..623cfca705 --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/40101130.txt @@ -0,0 +1,2 @@ +此版本的主要变化:主要是稳定性和错误修正更新。 +完整更新日志:https://github.com/vector-im/element-android/releases/tag/v1.1.13 diff --git a/fastlane/metadata/android/zh-CN/changelogs/40101140.txt b/fastlane/metadata/android/zh-CN/changelogs/40101140.txt new file mode 100644 index 0000000000..81dea97d96 --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/40101140.txt @@ -0,0 +1,2 @@ +此版本的主要变化:修复有关加密消息的问题。 +完整更新日志:https://github.com/vector-im/element-android/releases/tag/v1.1.14 diff --git a/fastlane/metadata/android/zh-CN/changelogs/40101150.txt b/fastlane/metadata/android/zh-CN/changelogs/40101150.txt new file mode 100644 index 0000000000..a6caddbf30 --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/40101150.txt @@ -0,0 +1,2 @@ +此版本的主要变化:实验室设置下的语音消息实现。 +完整更新日志:https://github.com/vector-im/element-android/releases/tag/v1.1.15 diff --git a/fastlane/metadata/android/zh-CN/changelogs/40101160.txt b/fastlane/metadata/android/zh-CN/changelogs/40101160.txt new file mode 100644 index 0000000000..98357b0dc5 --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/40101160.txt @@ -0,0 +1,2 @@ +此版本的主要变化:修复聊天室中有人登出时发送加密消息所遇到的错误。 +完整更新日志:https://github.com/vector-im/element-android/releases/tag/v1.1.16 diff --git a/fastlane/metadata/android/zh-CN/changelogs/40102000.txt b/fastlane/metadata/android/zh-CN/changelogs/40102000.txt new file mode 100644 index 0000000000..fa1db16805 --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/40102000.txt @@ -0,0 +1,2 @@ +此版本中的主要更改:默认启用语音消息。 +完整更新日志:https://github.com/vector-im/element-android/releases/tag/v1.2.0 diff --git a/fastlane/metadata/android/zh-CN/changelogs/40102010.txt b/fastlane/metadata/android/zh-CN/changelogs/40102010.txt new file mode 100644 index 0000000000..2ec2ae22b3 --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/40102010.txt @@ -0,0 +1,2 @@ +这个版本的主要变化:VoIP和空间的许多改进(仍在测试中)。 +完整更新日志:https://github.com/vector-im/element-android/releases/tag/v1.2.1 diff --git a/fastlane/metadata/android/zh-TW/changelogs/40101160.txt b/fastlane/metadata/android/zh-TW/changelogs/40101160.txt new file mode 100644 index 0000000000..364bec14b9 --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/40101160.txt @@ -0,0 +1,2 @@ +此版本中的主要變動:修復在聊天室中有人登出時傳送加密訊息所發生的問題。 +完整的變更紀錄:https://github.com/vector-im/element-android/releases/tag/v1.1.16 diff --git a/fastlane/metadata/android/zh-TW/changelogs/40102000.txt b/fastlane/metadata/android/zh-TW/changelogs/40102000.txt new file mode 100644 index 0000000000..993a59c825 --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/40102000.txt @@ -0,0 +1,2 @@ +此版本中的主要變動:語音訊息預設啟用。 +完整的變更紀錄:https://github.com/vector-im/element-android/releases/tag/v1.2.0 diff --git a/fastlane/metadata/android/zh-TW/changelogs/40102010.txt b/fastlane/metadata/android/zh-TW/changelogs/40102010.txt new file mode 100644 index 0000000000..b520266a78 --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/40102010.txt @@ -0,0 +1,2 @@ +此版本中的主要變動:對 VoIP 與空間功能的諸多改善(仍在測試中)。 +完整的變更紀錄:https://github.com/vector-im/element-android/releases/tag/v1.2.1 diff --git a/gradle.properties b/gradle.properties index 200866be25..98d561815b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,20 +6,20 @@ # http://www.gradle.org/docs/current/userguide/build_environment.html # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. -android.enableJetifier=true -android.useAndroidX=true -org.gradle.jvmargs=-Xmx2048m -# When configured, Gradle will run in incubating parallel mode. -# This option should only be used with decoupled projects. More details, visit -# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects -# org.gradle.parallel=true -# Enable file system watch (https://docs.gradle.org/6.7/release-notes.html) +# Build Time Optimizations +org.gradle.jvmargs=-Xmx3g -Xms512M -XX:MaxPermSize=2048m -XX:MaxMetaspaceSize=1g -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 -XX:+UseParallelGC +org.gradle.configureondemand=true +org.gradle.parallel=true org.gradle.vfs.watch=true +# Android Settings +android.enableJetifier=true +android.useAndroidX=true + +#Project Settings +# Change debugPrivateData to true for debugging vector.debugPrivateData=false +# httpLogLevel values: NONE, BASIC, HEADERS, BODY vector.httpLogLevel=BASIC -# Note: to debug, you can put and uncomment the following lines in the file ~/.gradle/gradle.properties to override the value above -#vector.debugPrivateData=true -#vector.httpLogLevel=BODY diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 17ba19021b..a37233c5e2 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=9bb8bc05f562f2d42bdf1ba8db62f6b6fa1c3bf6c392228802cc7cb0578fe7e0 -distributionUrl=https\://services.gradle.org/distributions/gradle-7.1.1-all.zip +distributionSha256Sum=a8da5b02437a60819cad23e10fc7e9cf32bcb57029d9cb277e26eeff76ce014b +distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 744e882ed5..1b6c787337 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ -#!/usr/bin/env sh +#!/bin/sh # -# Copyright 2015 the original author or authors. +# Copyright © 2015-2021 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,67 +17,101 @@ # ############################################################################## -## -## Gradle start up script for UN*X -## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# ############################################################################## # Attempt to set APP_HOME + # Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` +APP_BASE_NAME=${0##*/} # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum warn () { echo "$*" -} +} >&2 die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MSYS* | MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar @@ -87,9 +121,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -98,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" + JAVACMD=java which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the @@ -106,80 +140,95 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi -fi - -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi - -# For Cygwin or MSYS, switch paths to Windows format before running java -if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" - fi - i=`expr $i + 1` - done - case $i in - 0) set -- ;; - 1) set -- "$args0" ;; - 2) set -- "$args0" "$args1" ;; - 3) set -- "$args0" "$args1" "$args2" ;; - 4) set -- "$args0" "$args1" "$args2" "$args3" ;; - 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" esac fi -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=`save "$@"` +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' exec "$JAVACMD" "$@" diff --git a/library/ui-styles/build.gradle b/library/ui-styles/build.gradle index e264ef8319..cee58414c7 100644 --- a/library/ui-styles/build.gradle +++ b/library/ui-styles/build.gradle @@ -20,14 +20,11 @@ plugins { } android { - compileSdkVersion 30 - buildToolsVersion "30.0.3" + compileSdk versions.compileSdk defaultConfig { - minSdkVersion 21 - targetSdkVersion 30 - versionCode 1 - versionName "1.0" + minSdk versions.minSdk + targetSdk versions.targetSdk testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles "consumer-rules.pro" @@ -39,23 +36,26 @@ android { proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } + compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility versions.sourceCompat + targetCompatibility versions.targetCompat } + kotlinOptions { - jvmTarget = '1.8' + jvmTarget = "11" } + buildFeatures { viewBinding true } } dependencies { - implementation 'androidx.appcompat:appcompat:1.3.1' - implementation 'com.google.android.material:material:1.4.0' + implementation libs.androidx.appCompat + implementation libs.google.material // Pref theme - implementation 'androidx.preference:preference-ktx:1.1.1' + implementation libs.androidx.preferenceKtx // PFLockScreen attrs implementation 'com.github.vector-im:PFLockScreen-Android:1.0.0-beta12' // dialpad dimen diff --git a/library/ui-styles/src/main/AndroidManifest.xml b/library/ui-styles/src/main/AndroidManifest.xml index 19aa89e2e7..254827465d 100644 --- a/library/ui-styles/src/main/AndroidManifest.xml +++ b/library/ui-styles/src/main/AndroidManifest.xml @@ -1,2 +1,7 @@ - \ No newline at end of file + + + + + \ No newline at end of file diff --git a/library/ui-styles/src/main/res/color/keyword_background_selector.xml b/library/ui-styles/src/main/res/color/keyword_background_selector.xml new file mode 100644 index 0000000000..3420cfeaba --- /dev/null +++ b/library/ui-styles/src/main/res/color/keyword_background_selector.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/library/ui-styles/src/main/res/color/keyword_foreground_selector.xml b/library/ui-styles/src/main/res/color/keyword_foreground_selector.xml new file mode 100644 index 0000000000..339f240246 --- /dev/null +++ b/library/ui-styles/src/main/res/color/keyword_foreground_selector.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/library/ui-styles/src/main/res/layout/dialog_progress_material.xml b/library/ui-styles/src/main/res/layout/dialog_progress_material.xml index 09c88cc50b..32c4f666c7 100644 --- a/library/ui-styles/src/main/res/layout/dialog_progress_material.xml +++ b/library/ui-styles/src/main/res/layout/dialog_progress_material.xml @@ -2,7 +2,8 @@ + android:layout_height="wrap_content" + tools:ignore="UselessParent"> @android:color/black + #0BAC7E #80000000 diff --git a/library/ui-styles/src/main/res/values/dimens.xml b/library/ui-styles/src/main/res/values/dimens.xml index 88338f799b..e2e50449ce 100644 --- a/library/ui-styles/src/main/res/values/dimens.xml +++ b/library/ui-styles/src/main/res/values/dimens.xml @@ -28,6 +28,10 @@ 20dp 4dp + 128dp + 88dp + 8dp + 76dp diff --git a/library/ui-styles/src/main/res/values/stylable_bottom_sheet_action.xml b/library/ui-styles/src/main/res/values/stylable_bottom_sheet_action.xml index d6fa160d6b..b0c45b1fea 100644 --- a/library/ui-styles/src/main/res/values/stylable_bottom_sheet_action.xml +++ b/library/ui-styles/src/main/res/values/stylable_bottom_sheet_action.xml @@ -8,6 +8,7 @@ + diff --git a/library/ui-styles/src/main/res/values/styles_keyword.xml b/library/ui-styles/src/main/res/values/styles_keyword.xml new file mode 100644 index 0000000000..76e8eb4fc7 --- /dev/null +++ b/library/ui-styles/src/main/res/values/styles_keyword.xml @@ -0,0 +1,15 @@ + + + + + + + \ No newline at end of file diff --git a/library/ui-styles/src/main/res/values/theme_dark.xml b/library/ui-styles/src/main/res/values/theme_dark.xml index 0dbdc5ad4f..f83953a527 100644 --- a/library/ui-styles/src/main/res/values/theme_dark.xml +++ b/library/ui-styles/src/main/res/values/theme_dark.xml @@ -134,6 +134,8 @@ @style/Widget.Vector.Button.Outlined.SocialLogin.Gitlab.Dark @style/Widget.Vector.JumpToUnread.Dark + + @style/Widget.Vector.Keyword @color/vctr_voice_message_toast_background_dark diff --git a/library/ui-styles/src/main/res/values/theme_light.xml b/library/ui-styles/src/main/res/values/theme_light.xml index 17e0ff2938..cd5e17d607 100644 --- a/library/ui-styles/src/main/res/values/theme_light.xml +++ b/library/ui-styles/src/main/res/values/theme_light.xml @@ -137,6 +137,9 @@ @style/Widget.Vector.JumpToUnread.Light + + @style/Widget.Vector.Keyword + @color/vctr_voice_message_toast_background_light diff --git a/matrix-sdk-android-rx/build.gradle b/matrix-sdk-android-rx/build.gradle index 899432b498..dbd761cee3 100644 --- a/matrix-sdk-android-rx/build.gradle +++ b/matrix-sdk-android-rx/build.gradle @@ -3,13 +3,11 @@ apply plugin: 'kotlin-android' apply plugin: 'kotlin-kapt' android { - compileSdkVersion 30 + compileSdk versions.compileSdk defaultConfig { - minSdkVersion 21 - targetSdkVersion 30 - versionCode 1 - versionName "1.0" + minSdk versions.minSdk + targetSdk versions.targetSdk // Multidex is useful for tests multiDexEnabled true @@ -24,25 +22,26 @@ android { } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility versions.sourceCompat + targetCompatibility versions.targetCompat } kotlinOptions { - jvmTarget = "1.8" + jvmTarget = "11" } } dependencies { + implementation project(":matrix-sdk-android") - implementation 'androidx.appcompat:appcompat:1.3.1' - implementation 'io.reactivex.rxjava2:rxkotlin:2.4.0' - implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-rx2:$kotlin_coroutines_version" + implementation libs.androidx.appCompat + implementation libs.rx.rxKotlin + implementation libs.rx.rxAndroid + implementation libs.jetbrains.coroutinesRx2 // Paging - implementation "androidx.paging:paging-runtime-ktx:2.1.2" + implementation libs.androidx.pagingRuntimeKtx // Logging - implementation 'com.jakewharton.timber:timber:4.7.1' + implementation libs.jakewharton.timber } diff --git a/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxSession.kt b/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxSession.kt index 58fb760ff5..47203816b4 100644 --- a/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxSession.kt +++ b/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxSession.kt @@ -32,6 +32,7 @@ import org.matrix.android.sdk.api.session.crypto.crosssigning.SELF_SIGNING_KEY_S import org.matrix.android.sdk.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME import org.matrix.android.sdk.api.session.group.GroupSummaryQueryParams import org.matrix.android.sdk.api.session.group.model.GroupSummary +import org.matrix.android.sdk.api.session.identity.FoundThreePid import org.matrix.android.sdk.api.session.identity.ThreePid import org.matrix.android.sdk.api.session.pushers.Pusher import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams @@ -239,6 +240,10 @@ class RxSession(private val session: Session) { ) .distinctUntilChanged() } + + fun lookupThreePid(threePid: ThreePid): Single> = rxSingle { + session.identityService().lookUp(listOf(threePid)).firstOrNull().toOptional() + } } fun Session.rx(): RxSession { diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index 96c9fdb1e6..868ae56bf1 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -9,19 +9,19 @@ buildscript { mavenCentral() } dependencies { - classpath "io.realm:realm-gradle-plugin:10.6.1" + classpath "io.realm:realm-gradle-plugin:10.8.0" } } android { - compileSdkVersion 30 testOptions.unitTests.includeAndroidResources = true + compileSdk versions.compileSdk + defaultConfig { - minSdkVersion 21 - targetSdkVersion 30 - versionCode 1 - versionName "0.0.1" + minSdk versions.minSdk + targetSdk versions.targetSdk + // Multidex is useful for tests multiDexEnabled true testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -31,9 +31,7 @@ android { // that the app's state is completely cleared between tests. testInstrumentationRunnerArguments clearPackageData: 'true' - // Seems that the build tools 4.1.0 does not generate BuildConfig.VERSION_NAME anymore. - // Add it manually here. We may remove this trick in the future - buildConfigField "String", "VERSION_NAME", "\"0.0.1\"" + buildConfigField "String", "SDK_VERSION", "\"1.3.2\"" buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\"" resValue "string", "git_sdk_revision", "\"${gitRevision()}\"" @@ -67,17 +65,13 @@ android { installOptions "-g" } - lintOptions { - lintConfig file("lint.xml") - } - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility versions.sourceCompat + targetCompatibility versions.targetCompat } kotlinOptions { - jvmTarget = "1.8" + jvmTarget = "11" } sourceSets { @@ -107,92 +101,83 @@ static def gitRevisionDate() { dependencies { - def arrow_version = "0.8.2" - def moshi_version = '1.12.0' - def lifecycle_version = '2.2.0' - def arch_version = '2.1.0' - def markwon_version = '3.1.0' - def daggerVersion = '2.38' - def work_version = '2.5.0' - def retrofit_version = '2.9.0' + implementation libs.jetbrains.kotlinStdlibJdk7 + implementation libs.jetbrains.coroutinesCore + implementation libs.jetbrains.coroutinesAndroid - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version" + implementation libs.androidx.appCompat + implementation libs.androidx.core - implementation "androidx.appcompat:appcompat:1.3.1" - implementation "androidx.core:core-ktx:1.6.0" - - implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version" - implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version" + implementation libs.androidx.lifecycleExtensions + implementation libs.androidx.lifecycleJava8 // Network - implementation "com.squareup.retrofit2:retrofit:$retrofit_version" - implementation "com.squareup.retrofit2:converter-moshi:$retrofit_version" + implementation libs.squareup.retrofit + implementation libs.squareup.retrofitMoshi implementation(platform("com.squareup.okhttp3:okhttp-bom:4.9.1")) implementation 'com.squareup.okhttp3:okhttp' implementation 'com.squareup.okhttp3:logging-interceptor' implementation 'com.squareup.okhttp3:okhttp-urlconnection' - implementation "com.squareup.moshi:moshi-adapters:$moshi_version" - kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi_version" + implementation libs.squareup.moshi + kapt libs.squareup.moshiKotlin - implementation "ru.noties.markwon:core:$markwon_version" + implementation libs.markwon.core // Image - implementation 'androidx.exifinterface:exifinterface:1.3.2' + implementation libs.androidx.exifinterface // Database implementation 'com.github.Zhuinden:realm-monarchy:0.7.1' kapt 'dk.ilios:realmfieldnameshelper:2.0.0' // Work - implementation "androidx.work:work-runtime-ktx:$work_version" + implementation libs.androidx.work // FP - implementation "io.arrow-kt:arrow-core:$arrow_version" - implementation "io.arrow-kt:arrow-instances-core:$arrow_version" + implementation libs.arrow.core + implementation libs.arrow.instances // olm lib is now hosted by jitpack: https://jitpack.io/#org.matrix.gitlab.matrix-org/olm implementation 'org.matrix.gitlab.matrix-org:olm:3.2.4' // DI - implementation "com.google.dagger:dagger:$daggerVersion" - kapt "com.google.dagger:dagger-compiler:$daggerVersion" + implementation libs.dagger.dagger + kapt libs.dagger.daggerCompiler // Logging - implementation 'com.jakewharton.timber:timber:4.7.1' + implementation libs.jakewharton.timber implementation 'com.facebook.stetho:stetho-okhttp3:1.6.0' // Video compression - implementation 'com.otaliastudios:transcoder:0.10.3' + implementation 'com.otaliastudios:transcoder:0.10.4' // Phone number https://github.com/google/libphonenumber - implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.28' + implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.33' - testImplementation 'junit:junit:4.13.2' - testImplementation 'org.robolectric:robolectric:4.5.1' + testImplementation libs.tests.junit + testImplementation 'org.robolectric:robolectric:4.6.1' //testImplementation 'org.robolectric:shadows-support-v4:3.0' // Note: version sticks to 1.9.2 due to https://github.com/mockk/mockk/issues/281 - testImplementation 'io.mockk:mockk:1.12.0' - testImplementation 'org.amshove.kluent:kluent-android:1.68' - testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version" + testImplementation libs.mockk.mockk + testImplementation libs.tests.kluent + implementation libs.jetbrains.coroutinesAndroid // Plant Timber tree for test testImplementation 'net.lachlanmckee:timber-junit-rule:1.0.1' - kaptAndroidTest "com.google.dagger:dagger-compiler:$daggerVersion" - androidTestImplementation 'androidx.test:core:1.4.0' - androidTestImplementation 'androidx.test:runner:1.4.0' - androidTestImplementation 'androidx.test:rules:1.4.0' - androidTestImplementation 'androidx.test.ext:junit:1.1.3' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' - androidTestImplementation 'org.amshove.kluent:kluent-android:1.68' - androidTestImplementation 'io.mockk:mockk-android:1.12.0' - androidTestImplementation "androidx.arch.core:core-testing:$arch_version" - androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version" + kaptAndroidTest libs.dagger.daggerCompiler + androidTestImplementation libs.androidx.testCore + androidTestImplementation libs.androidx.testRunner + androidTestImplementation libs.androidx.testRules + androidTestImplementation libs.androidx.junit + androidTestImplementation libs.androidx.espressoCore + androidTestImplementation libs.tests.kluent + androidTestImplementation libs.mockk.mockkAndroid + androidTestImplementation libs.androidx.coreTesting + androidTestImplementation libs.jetbrains.coroutinesAndroid // Plant Timber tree for test - androidTestImplementation 'net.lachlanmckee:timber-junit-rule:1.0.1' + androidTestImplementation libs.tests.timberJunitRule - androidTestUtil 'androidx.test:orchestrator:1.4.0' + androidTestUtil libs.androidx.orchestrator } diff --git a/matrix-sdk-android/lint.xml b/matrix-sdk-android/lint.xml deleted file mode 100644 index 134aba822b..0000000000 --- a/matrix-sdk-android/lint.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/matrix-sdk-android/src/androidTest/AndroidManifest.xml b/matrix-sdk-android/src/androidTest/AndroidManifest.xml new file mode 100644 index 0000000000..274bd8c87b --- /dev/null +++ b/matrix-sdk-android/src/androidTest/AndroidManifest.xml @@ -0,0 +1,18 @@ + + + + + + + + + + diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/PermalinkParserTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/PermalinkParserTest.kt new file mode 100644 index 0000000000..b11a538949 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/PermalinkParserTest.kt @@ -0,0 +1,60 @@ +/* + * Copyright 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk + +import org.junit.Assert +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runners.MethodSorters +import org.matrix.android.sdk.api.session.permalinks.PermalinkData +import org.matrix.android.sdk.api.session.permalinks.PermalinkParser + +@FixMethodOrder(MethodSorters.JVM) +class PermalinkParserTest { + + @Test + fun testParseEmailInvite() { + val rawInvite = """ + https://app.element.io/#/room/%21MRBNLPtFnMAazZVPMO%3Amatrix.org?email=bob%2Bspace%40example.com&signurl=https%3A%2F%2Fvector.im%2F_matrix%2Fidentity%2Fapi%2Fv1%2Fsign-ed25519%3Ftoken%3DXmOwRZnSFabCRhTywFbJWKXWVNPysOpXIbroMGaUymqkJSvHeVKRsjHajwjCYdBsvGSvHauxbKfJmOxtXldtyLnyBMLKpBQCMzyYggrdapbVIceWZBtmslOQrXLABRoe%26private_key%3DT2gq0c3kJB_8OroXVxl1pBnzHsN7V6Xn4bEBSeW1ep4&room_name=Team2&room_avatar_url=&inviter_name=hiphop5&guest_access_token=&guest_user_id= + """.trimIndent() + .replace("https://app.element.io/#/room/", "https://matrix.to/#/") + + val parsedLink = PermalinkParser.parse(rawInvite) + Assert.assertTrue("Should be parsed as email invite but was ${parsedLink::class.java}", parsedLink is PermalinkData.RoomEmailInviteLink) + parsedLink as PermalinkData.RoomEmailInviteLink + Assert.assertEquals("!MRBNLPtFnMAazZVPMO:matrix.org", parsedLink.roomId) + Assert.assertEquals("XmOwRZnSFabCRhTywFbJWKXWVNPysOpXIbroMGaUymqkJSvHeVKRsjHajwjCYdBsvGSvHauxbKfJmOxtXldtyLnyBMLKpBQCMzyYggrdapbVIceWZBtmslOQrXLABRoe", parsedLink.token) + Assert.assertEquals("vector.im", parsedLink.identityServer) + Assert.assertEquals("Team2", parsedLink.roomName) + Assert.assertEquals("hiphop5", parsedLink.inviterName) + } + + @Test + fun testParseLinkWIthEvent() { + val rawInvite = "https://matrix.to/#/!OGEhHVWSdvArJzumhm:matrix.org/\$xuvJUVDJnwEeVjPx029rAOZ50difpmU_5gZk_T0jGfc?via=matrix.org&via=libera.chat&via=matrix.example.io" + + val parsedLink = PermalinkParser.parse(rawInvite) + Assert.assertTrue("Should be parsed as room link", parsedLink is PermalinkData.RoomLink) + parsedLink as PermalinkData.RoomLink + Assert.assertEquals("!OGEhHVWSdvArJzumhm:matrix.org", parsedLink.roomIdOrAlias) + Assert.assertEquals("\$xuvJUVDJnwEeVjPx029rAOZ50difpmU_5gZk_T0jGfc", parsedLink.eventId) + Assert.assertEquals(3, parsedLink.viaParameters.size) + Assert.assertTrue(parsedLink.viaParameters.contains("matrix.example.io")) + Assert.assertTrue(parsedLink.viaParameters.contains("matrix.org")) + Assert.assertTrue(parsedLink.viaParameters.contains("matrix.example.io")) + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/api/Matrix.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/api/Matrix.kt index c439da8407..8b9b6efa11 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/api/Matrix.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/api/Matrix.kt @@ -117,7 +117,7 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo } fun getSdkVersion(): String { - return BuildConfig.VERSION_NAME + " (" + BuildConfig.GIT_SDK_REVISION + ")" + return BuildConfig.SDK_VERSION + " (" + BuildConfig.GIT_SDK_REVISION + ")" } } } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt index 6e07223ac7..cf9b8f87c1 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt @@ -19,6 +19,8 @@ package org.matrix.android.sdk.common import android.content.Context import android.net.Uri import androidx.lifecycle.Observer +import androidx.test.internal.runner.junit4.statement.UiThreadStatement +import androidx.test.internal.runner.junit4.statement.UiThreadStatement.runOnUiThread import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.delay @@ -59,13 +61,15 @@ class CommonTestHelper(context: Context) { fun getTestInterceptor(session: Session): MockOkHttpInterceptor? = TestNetworkModule.interceptorForSession(session.sessionId) as? MockOkHttpInterceptor init { - Matrix.initialize( - context, - MatrixConfiguration( - applicationFlavor = "TestFlavor", - roomDisplayNameFallbackProvider = TestRoomDisplayNameFallbackProvider() - ) - ) + UiThreadStatement.runOnUiThread { + Matrix.initialize( + context, + MatrixConfiguration( + applicationFlavor = "TestFlavor", + roomDisplayNameFallbackProvider = TestRoomDisplayNameFallbackProvider() + ) + ) + } matrix = Matrix.getInstance(context) } @@ -91,6 +95,7 @@ class CommonTestHelper(context: Context) { * * @param session the session to sync */ + @Suppress("EXPERIMENTAL_API_USAGE") fun syncSession(session: Session, timeout: Long = TestConstants.timeOutMillis) { val lock = CountDownLatch(1) @@ -327,6 +332,7 @@ class CommonTestHelper(context: Context) { assertTrue(latch.await(timeout ?: TestConstants.timeOutMillis, TimeUnit.MILLISECONDS)) } + @Suppress("EXPERIMENTAL_API_USAGE") fun retryPeriodicallyWithLatch(latch: CountDownLatch, condition: (() -> Boolean)) { GlobalScope.launch { while (true) { diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt index da176491c6..a8cbc160dd 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt @@ -84,6 +84,7 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) { /** * @return alice and bob sessions */ + @Suppress("EXPERIMENTAL_API_USAGE") fun doE2ETestWithAliceAndBobInARoom(encryptedRoom: Boolean = true): CryptoTestData { val cryptoTestData = doE2ETestWithAliceInARoom(encryptedRoom) val aliceSession = cryptoTestData.firstSession @@ -255,6 +256,7 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) { ) } + @Suppress("EXPERIMENTAL_API_USAGE") fun createDM(alice: Session, bob: Session): String { val roomId = mTestHelper.runBlockingTest { alice.createDirectRoom(bob.myUserId) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/ssss/QuadSTests.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/ssss/QuadSTests.kt index d14de30c90..74855b8630 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/ssss/QuadSTests.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/ssss/QuadSTests.kt @@ -60,6 +60,7 @@ class QuadSTests : InstrumentedTest { } @Test + @Suppress("EXPERIMENTAL_API_USAGE") fun test_Generate4SKey() { val aliceSession = mTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) @@ -275,6 +276,7 @@ class QuadSTests : InstrumentedTest { mTestHelper.signOutAndClose(aliceSession) } + @Suppress("EXPERIMENTAL_API_USAGE") private fun assertAccountData(session: Session, type: String): UserAccountDataEvent { val accountDataLock = CountDownLatch(1) var accountData: UserAccountDataEvent? = null diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineForwardPaginationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineForwardPaginationTest.kt index f156a5eb64..0fe341cad6 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineForwardPaginationTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineForwardPaginationTest.kt @@ -139,7 +139,7 @@ class TimelineForwardPaginationTest : InstrumentedTest { // Alice can see the first event of the room (so Back pagination has worked) snapshot.lastOrNull()?.root?.getClearType() == EventType.STATE_ROOM_CREATE // 6 for room creation item (backward pagination), 1 for the context, and 50 for the forward pagination - && snapshot.size == 6 + 1 + 50 + && snapshot.size == 57 // 6 + 1 + 50 } aliceTimeline.addListener(aliceEventsListener) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelinePreviousLastForwardTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelinePreviousLastForwardTest.kt index 9ebac8766a..03a4d41988 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelinePreviousLastForwardTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelinePreviousLastForwardTest.kt @@ -189,7 +189,7 @@ class TimelinePreviousLastForwardTest : InstrumentedTest { Timber.w(" event ${it.root}") } - snapshot.size == 8 + 1 + 35 + snapshot.size == 44 // 8 + 1 + 35 } bobTimeline.addListener(eventsListener) @@ -218,7 +218,7 @@ class TimelinePreviousLastForwardTest : InstrumentedTest { // Bob can see the first event of the room (so Back pagination has worked) snapshot.lastOrNull()?.root?.getClearType() == EventType.STATE_ROOM_CREATE // 8 for room creation item 60 message from Alice - && snapshot.size == 8 + 60 + && snapshot.size == 68 // 8 + 60 && snapshot.checkSendOrder(secondMessage, 30, 0) && snapshot.checkSendOrder(firstMessage, 30, 30) } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceCreationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceCreationTest.kt index a1744a0dae..5911414c25 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceCreationTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceCreationTest.kt @@ -28,12 +28,9 @@ import org.junit.runner.RunWith import org.junit.runners.JUnit4 import org.junit.runners.MethodSorters import org.matrix.android.sdk.InstrumentedTest -import org.matrix.android.sdk.api.query.ActiveSpaceFilter import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.toModel -import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams import org.matrix.android.sdk.api.session.room.model.GuestAccess -import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent import org.matrix.android.sdk.api.session.room.model.RoomGuestAccessContent import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility @@ -42,7 +39,6 @@ import org.matrix.android.sdk.api.session.room.model.RoomType import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams import org.matrix.android.sdk.api.session.room.model.create.CreateRoomPreset import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent -import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams import org.matrix.android.sdk.api.session.space.JoinSpaceResult import org.matrix.android.sdk.common.CommonTestHelper import org.matrix.android.sdk.common.SessionTestParams @@ -54,6 +50,7 @@ class SpaceCreationTest : InstrumentedTest { private val commonTestHelper = CommonTestHelper(context()) @Test + @Suppress("EXPERIMENTAL_API_USAGE") fun createSimplePublicSpace() { val session = commonTestHelper.createAccount("Hubble", SessionTestParams(true)) val roomName = "My Space" @@ -137,6 +134,7 @@ class SpaceCreationTest : InstrumentedTest { } @Test + @Suppress("EXPERIMENTAL_API_USAGE") fun testSimplePublicSpaceWithChildren() { val aliceSession = commonTestHelper.createAccount("alice", SessionTestParams(true)) val bobSession = commonTestHelper.createAccount("bob", SessionTestParams(true)) @@ -162,7 +160,7 @@ class SpaceCreationTest : InstrumentedTest { commonTestHelper.waitWithLatch { GlobalScope.launch { - syncedSpace?.addChildren(firstChild!!, listOf(aliceSession.sessionParams.homeServerHost ?: ""), "a", true, suggested = true) + syncedSpace?.addChildren(firstChild!!, listOf(aliceSession.sessionParams.homeServerHost ?: ""), "a", suggested = true) it.countDown() } } @@ -181,7 +179,7 @@ class SpaceCreationTest : InstrumentedTest { commonTestHelper.waitWithLatch { GlobalScope.launch { - syncedSpace?.addChildren(secondChild!!, listOf(aliceSession.sessionParams.homeServerHost ?: ""), "b", false, suggested = true) + syncedSpace?.addChildren(secondChild!!, listOf(aliceSession.sessionParams.homeServerHost ?: ""), "b", suggested = true) it.countDown() } } @@ -202,19 +200,20 @@ class SpaceCreationTest : InstrumentedTest { assertEquals("Room name should be set", roomName, spaceBobPov?.asRoom()?.roomSummary()?.name) assertEquals("Room topic should be set", topic, spaceBobPov?.asRoom()?.roomSummary()?.topic) + // /!\ AUTO_JOIN has been descoped // check if bob has joined automatically the first room - val bobMembershipFirstRoom = bobSession.getRoomSummary(firstChild!!)?.membership - assertEquals("Bob should have joined this room", Membership.JOIN, bobMembershipFirstRoom) - RoomSummaryQueryParams.Builder() - - val childCount = bobSession.getRoomSummaries( - roomSummaryQueryParams { - activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(spaceId) - } - ).size - - assertEquals("Unexpected number of joined children", 1, childCount) +// val bobMembershipFirstRoom = bobSession.getRoomSummary(firstChild!!)?.membership +// assertEquals("Bob should have joined this room", Membership.JOIN, bobMembershipFirstRoom) +// RoomSummaryQueryParams.Builder() +// +// val childCount = bobSession.getRoomSummaries( +// roomSummaryQueryParams { +// activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(spaceId) +// } +// ).size +// +// assertEquals("Unexpected number of joined children", 1, childCount) commonTestHelper.signOutAndClose(aliceSession) commonTestHelper.signOutAndClose(bobSession) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceHierarchyTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceHierarchyTest.kt index 521b5805bd..436daf001b 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceHierarchyTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceHierarchyTest.kt @@ -32,10 +32,19 @@ import org.junit.runners.JUnit4 import org.junit.runners.MethodSorters import org.matrix.android.sdk.InstrumentedTest import org.matrix.android.sdk.api.query.ActiveSpaceFilter +import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent +import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesAllowEntry import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.RoomType import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams +import org.matrix.android.sdk.api.session.room.model.create.RestrictedRoomPreset +import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper +import org.matrix.android.sdk.api.session.room.powerlevels.Role import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams import org.matrix.android.sdk.common.CommonTestHelper import org.matrix.android.sdk.common.SessionTestParams @@ -47,6 +56,7 @@ class SpaceHierarchyTest : InstrumentedTest { private val commonTestHelper = CommonTestHelper(context()) @Test + @Suppress("EXPERIMENTAL_API_USAGE") fun createCanonicalChildRelation() { val session = commonTestHelper.createAccount("John", SessionTestParams(true)) val spaceName = "My Space" @@ -171,6 +181,7 @@ class SpaceHierarchyTest : InstrumentedTest { // } @Test + @Suppress("EXPERIMENTAL_API_USAGE") fun testFilteringBySpace() { val session = commonTestHelper.createAccount("John", SessionTestParams(true)) @@ -179,7 +190,7 @@ class SpaceHierarchyTest : InstrumentedTest { Triple("A2", true, true) )) - val spaceBInfo = createPublicSpace(session, "SpaceB", listOf( + /* val spaceBInfo = */ createPublicSpace(session, "SpaceB", listOf( Triple("B1", true /*auto-join*/, true/*canonical*/), Triple("B2", true, true), Triple("B3", true, true) @@ -254,6 +265,7 @@ class SpaceHierarchyTest : InstrumentedTest { } @Test + @Suppress("EXPERIMENTAL_API_USAGE") fun testBreakCycle() { val session = commonTestHelper.createAccount("John", SessionTestParams(true)) @@ -301,6 +313,7 @@ class SpaceHierarchyTest : InstrumentedTest { } @Test + @Suppress("EXPERIMENTAL_API_USAGE") fun testLiveFlatChildren() { val session = commonTestHelper.createAccount("John", SessionTestParams(true)) @@ -382,6 +395,8 @@ class SpaceHierarchyTest : InstrumentedTest { // The room should have disapear from flat children GlobalScope.launch(Dispatchers.Main) { flatAChildren.observeForever(childObserver) } } + + commonTestHelper.signOutAndClose(session) } data class TestSpaceCreationResult( @@ -389,6 +404,7 @@ class SpaceHierarchyTest : InstrumentedTest { val roomIds: List ) + @Suppress("EXPERIMENTAL_API_USAGE") private fun createPublicSpace(session: Session, spaceName: String, childInfo: List> @@ -429,11 +445,62 @@ class SpaceHierarchyTest : InstrumentedTest { return TestSpaceCreationResult(spaceId, roomIds) } + @Suppress("EXPERIMENTAL_API_USAGE") + private fun createPrivateSpace(session: Session, + spaceName: String, + childInfo: List> + /** Name, auto-join, canonical*/ + ): TestSpaceCreationResult { + var spaceId = "" + commonTestHelper.waitWithLatch { + GlobalScope.launch { + spaceId = session.spaceService().createSpace(spaceName, "My Private Space", null, false) + it.countDown() + } + } + + val syncedSpace = session.spaceService().getSpace(spaceId) + val viaServers = listOf(session.sessionParams.homeServerHost ?: "") + + val roomIds = + childInfo.map { entry -> + var roomId = "" + commonTestHelper.waitWithLatch { + GlobalScope.launch { + val homeServerCapabilities = session + .getHomeServerCapabilities() + roomId = session.createRoom(CreateRoomParams().apply { + name = entry.first + this.featurePreset = RestrictedRoomPreset( + homeServerCapabilities, + listOf( + RoomJoinRulesAllowEntry.restrictedToRoom(spaceId) + ) + ) + }) + it.countDown() + } + } + roomId + } + + roomIds.forEachIndexed { index, roomId -> + runBlocking { + syncedSpace!!.addChildren(roomId, viaServers, null, childInfo[index].second) + val canonical = childInfo[index].third + if (canonical != null) { + session.spaceService().setSpaceParent(roomId, spaceId, canonical, viaServers) + } + } + } + return TestSpaceCreationResult(spaceId, roomIds) + } + @Test fun testRootSpaces() { val session = commonTestHelper.createAccount("John", SessionTestParams(true)) - val spaceAInfo = createPublicSpace(session, "SpaceA", listOf( + /* val spaceAInfo = */ createPublicSpace(session, "SpaceA", listOf( Triple("A1", true /*auto-join*/, true/*canonical*/), Triple("A2", true, true) )) @@ -468,5 +535,111 @@ class SpaceHierarchyTest : InstrumentedTest { val rootSpaces = session.spaceService().getRootSpaceSummaries() assertEquals("Unexpected number of root spaces ${rootSpaces.map { it.name }}", 2, rootSpaces.size) + + commonTestHelper.signOutAndClose(session) + } + + @Test + fun testParentRelation() { + val aliceSession = commonTestHelper.createAccount("Alice", SessionTestParams(true)) + val bobSession = commonTestHelper.createAccount("Bib", SessionTestParams(true)) + + val spaceAInfo = createPrivateSpace(aliceSession, "Private Space A", listOf( + Triple("General", true /*suggested*/, true/*canonical*/), + Triple("Random", true, true) + )) + + commonTestHelper.runBlockingTest { + aliceSession.getRoom(spaceAInfo.spaceId)!!.invite(bobSession.myUserId, null) + } + + commonTestHelper.runBlockingTest { + bobSession.joinRoom(spaceAInfo.spaceId, null, emptyList()) + } + + var bobRoomId = "" + commonTestHelper.waitWithLatch { + GlobalScope.launch { + bobRoomId = bobSession.createRoom(CreateRoomParams().apply { name = "A Bob Room" }) + bobSession.getRoom(bobRoomId)!!.invite(aliceSession.myUserId) + it.countDown() + } + } + + commonTestHelper.runBlockingTest { + aliceSession.joinRoom(bobRoomId) + } + + commonTestHelper.waitWithLatch { latch -> + commonTestHelper.retryPeriodicallyWithLatch(latch) { + aliceSession.getRoomSummary(bobRoomId)?.membership?.isActive() == true + } + } + + commonTestHelper.waitWithLatch { + GlobalScope.launch { + bobSession.spaceService().setSpaceParent(bobRoomId, spaceAInfo.spaceId, false, listOf(bobSession.sessionParams.homeServerHost ?: "")) + it.countDown() + } + } + + commonTestHelper.waitWithLatch { latch -> + commonTestHelper.retryPeriodicallyWithLatch(latch) { + val stateEvent = aliceSession.getRoom(bobRoomId)!!.getStateEvent(EventType.STATE_SPACE_PARENT, QueryStringValue.Equals(spaceAInfo.spaceId)) + stateEvent != null + } + } + + // This should be an invalid space parent relation, because no opposite child and bob is not admin of the space + commonTestHelper.runBlockingTest { + // we can see the state event + // but it is not valid and room is not in hierarchy + assertTrue("Bob Room should not be listed as a child of the space", aliceSession.getRoomSummary(bobRoomId)?.flattenParentIds?.isEmpty() == true) + } + + // Let's now try to make alice admin of the room + + commonTestHelper.waitWithLatch { + GlobalScope.launch { + val room = bobSession.getRoom(bobRoomId)!! + val currentPLContent = room + .getStateEvent(EventType.STATE_ROOM_POWER_LEVELS) + ?.let { it.content.toModel() } + + val newPowerLevelsContent = currentPLContent + ?.setUserPowerLevel(aliceSession.myUserId, Role.Admin.value) + ?.toContent() + + room.sendStateEvent(EventType.STATE_ROOM_POWER_LEVELS, null, newPowerLevelsContent!!) + it.countDown() + } + } + + commonTestHelper.waitWithLatch { latch -> + commonTestHelper.retryPeriodicallyWithLatch(latch) { + val powerLevelsHelper = aliceSession.getRoom(bobRoomId)!! + .getStateEvent(EventType.STATE_ROOM_POWER_LEVELS) + ?.content + ?.toModel() + ?.let { PowerLevelsHelper(it) } + powerLevelsHelper!!.isUserAllowedToSend(aliceSession.myUserId, true, EventType.STATE_SPACE_PARENT) + } + } + + commonTestHelper.waitWithLatch { + GlobalScope.launch { + aliceSession.spaceService().setSpaceParent(bobRoomId, spaceAInfo.spaceId, false, listOf(bobSession.sessionParams.homeServerHost ?: "")) + it.countDown() + } + } + + commonTestHelper.waitWithLatch { latch -> + commonTestHelper.retryPeriodicallyWithLatch(latch) { + bobSession.getRoomSummary(bobRoomId)?.flattenParentIds?.contains(spaceAInfo.spaceId) == true + } + } + + commonTestHelper.signOutAndClose(aliceSession) + commonTestHelper.signOutAndClose(bobSession) } } diff --git a/matrix-sdk-android/src/main/AndroidManifest.xml b/matrix-sdk-android/src/main/AndroidManifest.xml index 220a168f60..de0731422c 100644 --- a/matrix-sdk-android/src/main/AndroidManifest.xml +++ b/matrix-sdk-android/src/main/AndroidManifest.xml @@ -10,15 +10,6 @@ - - - + - @@ -47,6 +47,7 @@ + diff --git a/vector/src/androidTest/java/im/vector/app/VerificationTestBase.kt b/vector/src/androidTest/java/im/vector/app/VerificationTestBase.kt index 285f40aaf3..a562287263 100644 --- a/vector/src/androidTest/java/im/vector/app/VerificationTestBase.kt +++ b/vector/src/androidTest/java/im/vector/app/VerificationTestBase.kt @@ -107,6 +107,7 @@ abstract class VerificationTestBase { return result!! } + @Suppress("EXPERIMENTAL_API_USAGE") private fun syncSession(session: Session) { val lock = CountDownLatch(1) diff --git a/vector/src/androidTest/java/im/vector/app/features/reactions/data/EmojiDataSourceTest.kt b/vector/src/androidTest/java/im/vector/app/features/reactions/data/EmojiDataSourceTest.kt index 79090c42dd..a880b17e0c 100644 --- a/vector/src/androidTest/java/im/vector/app/features/reactions/data/EmojiDataSourceTest.kt +++ b/vector/src/androidTest/java/im/vector/app/features/reactions/data/EmojiDataSourceTest.kt @@ -17,6 +17,10 @@ package im.vector.app.features.reactions.data import im.vector.app.InstrumentedTest +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.FixMethodOrder @@ -30,64 +34,80 @@ import kotlin.system.measureTimeMillis @FixMethodOrder(MethodSorters.JVM) class EmojiDataSourceTest : InstrumentedTest { + private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main) + @Test fun checkParsingTime() { val time = measureTimeMillis { - EmojiDataSource(context().resources) + createEmojiDataSource() } - assertTrue("Too long to parse", time < 100) } @Test fun checkNumberOfResult() { - val emojiDataSource = EmojiDataSource(context().resources) - assertTrue("Wrong number of emojis", emojiDataSource.rawData.emojis.size >= 500) - assertTrue("Wrong number of categories", emojiDataSource.rawData.categories.size >= 8) + val emojiDataSource = createEmojiDataSource() + val rawData = runBlocking { + emojiDataSource.rawData.await() + } + assertTrue("Wrong number of emojis", rawData.emojis.size >= 500) + assertTrue("Wrong number of categories", rawData.categories.size >= 8) } @Test fun searchTestEmptySearch() { - val emojiDataSource = EmojiDataSource(context().resources) - - assertTrue("Empty search should return at least 500 results", emojiDataSource.filterWith("").size >= 500) + val emojiDataSource = createEmojiDataSource() + val result = runBlocking { + emojiDataSource.filterWith("") + } + assertTrue("Empty search should return at least 500 results", result.size >= 500) } @Test fun searchTestNoResult() { - val emojiDataSource = EmojiDataSource(context().resources) - - assertTrue("Should not have result", emojiDataSource.filterWith("noresult").isEmpty()) + val emojiDataSource = createEmojiDataSource() + val result = runBlocking { + emojiDataSource.filterWith("noresult") + } + assertTrue("Should not have result", result.isEmpty()) } @Test fun searchTestOneResult() { - val emojiDataSource = EmojiDataSource(context().resources) - - assertEquals("Should have 1 result", 1, emojiDataSource.filterWith("france").size) + val emojiDataSource = createEmojiDataSource() + val result = runBlocking { + emojiDataSource.filterWith("france") + } + assertEquals("Should have 1 result", 1, result.size) } @Test fun searchTestManyResult() { - val emojiDataSource = EmojiDataSource(context().resources) - - assertTrue("Should have many result", emojiDataSource.filterWith("fra").size > 1) + val emojiDataSource = createEmojiDataSource() + val result = runBlocking { + emojiDataSource.filterWith("fra") + } + assertTrue("Should have many result", result.size > 1) } @Test fun testTada() { - val emojiDataSource = EmojiDataSource(context().resources) - - val result = emojiDataSource.filterWith("tada") - + val emojiDataSource = createEmojiDataSource() + val result = runBlocking { + emojiDataSource.filterWith("tada") + } assertEquals("Should find tada emoji", 1, result.size) assertEquals("Should find tada emoji", "🎉", result[0].emoji) } @Test fun testQuickReactions() { - val emojiDataSource = EmojiDataSource(context().resources) - - assertEquals("Should have 8 quick reactions", 8, emojiDataSource.getQuickReactions().size) + val emojiDataSource = createEmojiDataSource() + val result = runBlocking { + emojiDataSource.getQuickReactions() + } + assertEquals("Should have 8 quick reactions", 8, result.size) } + + private fun createEmojiDataSource() = EmojiDataSource(coroutineScope, context().resources) } diff --git a/vector/src/androidTest/java/im/vector/app/ui/UiAllScreensSanityTest.kt b/vector/src/androidTest/java/im/vector/app/ui/UiAllScreensSanityTest.kt index 26365e0c72..aef5d3fe49 100644 --- a/vector/src/androidTest/java/im/vector/app/ui/UiAllScreensSanityTest.kt +++ b/vector/src/androidTest/java/im/vector/app/ui/UiAllScreensSanityTest.kt @@ -30,18 +30,20 @@ import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.ext.junit.rules.ActivityScenarioRule import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.LargeTest -import com.schibsted.spain.barista.assertion.BaristaListAssertions.assertListItemCount -import com.schibsted.spain.barista.assertion.BaristaVisibilityAssertions.assertDisplayed -import com.schibsted.spain.barista.interaction.BaristaClickInteractions.clickBack -import com.schibsted.spain.barista.interaction.BaristaClickInteractions.clickOn -import com.schibsted.spain.barista.interaction.BaristaClickInteractions.longClickOn -import com.schibsted.spain.barista.interaction.BaristaDialogInteractions.clickDialogNegativeButton -import com.schibsted.spain.barista.interaction.BaristaDialogInteractions.clickDialogPositiveButton -import com.schibsted.spain.barista.interaction.BaristaEditTextInteractions.writeTo -import com.schibsted.spain.barista.interaction.BaristaListInteractions.clickListItem -import com.schibsted.spain.barista.interaction.BaristaListInteractions.clickListItemChild -import com.schibsted.spain.barista.interaction.BaristaMenuClickInteractions.clickMenu -import com.schibsted.spain.barista.interaction.BaristaMenuClickInteractions.openMenu +import com.adevinta.android.barista.assertion.BaristaListAssertions.assertListItemCount +import com.adevinta.android.barista.assertion.BaristaVisibilityAssertions.assertDisplayed +import com.adevinta.android.barista.interaction.BaristaClickInteractions.clickBack +import com.adevinta.android.barista.interaction.BaristaClickInteractions.clickOn +import com.adevinta.android.barista.interaction.BaristaClickInteractions.longClickOn +import com.adevinta.android.barista.interaction.BaristaDialogInteractions.clickDialogNegativeButton +import com.adevinta.android.barista.interaction.BaristaDialogInteractions.clickDialogPositiveButton +import com.adevinta.android.barista.interaction.BaristaDrawerInteractions.openDrawer +import com.adevinta.android.barista.interaction.BaristaEditTextInteractions.writeTo +import com.adevinta.android.barista.interaction.BaristaListInteractions.clickListItem +import com.adevinta.android.barista.interaction.BaristaListInteractions.clickListItemChild +import com.adevinta.android.barista.interaction.BaristaMenuClickInteractions.clickMenu +import com.adevinta.android.barista.interaction.BaristaMenuClickInteractions.openMenu +import im.vector.app.BuildConfig import im.vector.app.EspressoHelper import im.vector.app.R import im.vector.app.SleepViewAction @@ -169,9 +171,13 @@ class UiAllScreensSanityTest { } clickOn(R.string.create_new_room) + // Room access bottom sheet + clickOn(R.string.room_settings_room_access_private_title) + pressBack() + // Create - assertListItemCount(R.id.createRoomForm, 10) - clickListItemChild(R.id.createRoomForm, 9, R.id.form_submit_button) + assertListItemCount(R.id.createRoomForm, 12) + clickListItemChild(R.id.createRoomForm, 11, R.id.form_submit_button) waitUntilActivityVisible { assertDisplayed(R.id.roomDetailContainer) @@ -219,6 +225,8 @@ class UiAllScreensSanityTest { clickOn(R.string.message_add_reaction) // Filter // TODO clickMenu(R.id.search) + // Wait for emoji to load, it's async now + sleep(1_000) clickListItem(R.id.emojiRecyclerView, 4) // Test Edit mode @@ -229,6 +237,8 @@ class UiAllScreensSanityTest { // Wait a bit for the keyboard layout to update sleep(30) clickOn(R.id.sendButton) + // Wait for the UI to update + sleep(1000) // Open edit history longClickOnMessage("Hello universe! (edited)") clickOn(R.string.message_view_edit_history) @@ -275,6 +285,7 @@ class UiAllScreensSanityTest { clickListItem(R.id.matrixProfileRecyclerView, 9) // File tab clickOn(R.string.uploads_files_title) + sleep(1000) pressBack() assertDisplayed(R.id.roomProfileAvatarView) @@ -326,6 +337,7 @@ class UiAllScreensSanityTest { private fun navigateToRoomPeople() { // Open first user clickListItem(R.id.roomSettingsRecyclerView, 1) + sleep(1000) assertDisplayed(R.id.memberProfilePowerLevelView) // Verification @@ -334,8 +346,9 @@ class UiAllScreensSanityTest { // Role clickListItem(R.id.matrixProfileRecyclerView, 3) + sleep(1000) clickDialogNegativeButton() - + sleep(1000) clickBack() } @@ -355,7 +368,8 @@ class UiAllScreensSanityTest { } private fun navigateToSettings() { - clickOn(R.id.groupToolbarAvatarImageView) + // clickOn(R.id.groupToolbarAvatarImageView) + openDrawer() clickOn(R.id.homeDrawerHeaderSettingsView) clickOn(R.string.settings_general_title) @@ -458,8 +472,18 @@ class UiAllScreensSanityTest { } private fun navigateToSettingsNotifications() { - clickOn(R.string.settings_notification_advanced) - pressBack() + if (BuildConfig.USE_NOTIFICATION_SETTINGS_V2) { + clickOn(R.string.settings_notification_default) + pressBack() + clickOn(R.string.settings_notification_mentions_and_keywords) + // TODO Test adding a keyword? + pressBack() + clickOn(R.string.settings_notification_other) + pressBack() + } else { + clickOn(R.string.settings_notification_advanced) + pressBack() + } /* clickOn(R.string.settings_noisy_notifications_preferences) TODO Cannot go back diff --git a/vector/src/androidTest/java/im/vector/app/ui/UiTestBase.kt b/vector/src/androidTest/java/im/vector/app/ui/UiTestBase.kt index ed174b50a2..2cef326501 100644 --- a/vector/src/androidTest/java/im/vector/app/ui/UiTestBase.kt +++ b/vector/src/androidTest/java/im/vector/app/ui/UiTestBase.kt @@ -20,11 +20,11 @@ import androidx.test.espresso.Espresso.closeSoftKeyboard import androidx.test.espresso.Espresso.onView import androidx.test.espresso.matcher.ViewMatchers.isRoot import androidx.test.espresso.matcher.ViewMatchers.withId -import com.schibsted.spain.barista.assertion.BaristaEnabledAssertions.assertDisabled -import com.schibsted.spain.barista.assertion.BaristaEnabledAssertions.assertEnabled -import com.schibsted.spain.barista.assertion.BaristaVisibilityAssertions.assertDisplayed -import com.schibsted.spain.barista.interaction.BaristaClickInteractions.clickOn -import com.schibsted.spain.barista.interaction.BaristaEditTextInteractions.writeTo +import com.adevinta.android.barista.assertion.BaristaEnabledAssertions.assertDisabled +import com.adevinta.android.barista.assertion.BaristaEnabledAssertions.assertEnabled +import com.adevinta.android.barista.assertion.BaristaVisibilityAssertions.assertDisplayed +import com.adevinta.android.barista.interaction.BaristaClickInteractions.clickOn +import com.adevinta.android.barista.interaction.BaristaEditTextInteractions.writeTo import im.vector.app.R import im.vector.app.espresso.tools.waitUntilActivityVisible import im.vector.app.features.home.HomeActivity diff --git a/vector/src/gplay/java/im/vector/app/gplay/push/fcm/VectorFirebaseMessagingService.kt b/vector/src/gplay/java/im/vector/app/gplay/push/fcm/VectorFirebaseMessagingService.kt index 4cefeadb62..ddedfb93e3 100755 --- a/vector/src/gplay/java/im/vector/app/gplay/push/fcm/VectorFirebaseMessagingService.kt +++ b/vector/src/gplay/java/im/vector/app/gplay/push/fcm/VectorFirebaseMessagingService.kt @@ -39,17 +39,22 @@ import im.vector.app.features.notifications.NotifiableMessageEvent import im.vector.app.features.notifications.NotificationDrawerManager import im.vector.app.features.notifications.NotificationUtils import im.vector.app.features.notifications.SimpleNotifiableEvent +import im.vector.app.features.settings.VectorDataStore import im.vector.app.features.settings.VectorPreferences import im.vector.app.push.fcm.FcmHelper import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.logger.LoggerTag import org.matrix.android.sdk.api.pushrules.Action import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.events.model.Event import timber.log.Timber +private val loggerTag = LoggerTag("Push", LoggerTag.SYNC) + /** * Class extending FirebaseMessagingService. */ @@ -60,6 +65,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { private lateinit var pusherManager: PushersManager private lateinit var activeSessionHolder: ActiveSessionHolder private lateinit var vectorPreferences: VectorPreferences + private lateinit var vectorDataStore: VectorDataStore private lateinit var wifiDetector: WifiDetector private val coroutineScope = CoroutineScope(SupervisorJob()) @@ -77,6 +83,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { pusherManager = pusherManager() activeSessionHolder = activeSessionHolder() vectorPreferences = vectorPreferences() + vectorDataStore = vectorDataStore() wifiDetector = wifiDetector() } } @@ -88,9 +95,13 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { */ override fun onMessageReceived(message: RemoteMessage) { if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) { - Timber.d("## onMessageReceived() %s", message.data.toString()) + Timber.tag(loggerTag.value).d("## onMessageReceived() %s", message.data.toString()) + } + Timber.tag(loggerTag.value).d("## onMessageReceived() from FCM with priority %s", message.priority) + + runBlocking { + vectorDataStore.incrementPushCounter() } - Timber.d("## onMessageReceived() from FCM with priority %s", message.priority) // Diagnostic Push if (message.data["event_id"] == PushersManager.TEST_EVENT_ID) { @@ -100,14 +111,14 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { } if (!vectorPreferences.areNotificationEnabledForDevice()) { - Timber.i("Notification are disabled for this device") + Timber.tag(loggerTag.value).i("Notification are disabled for this device") return } mUIHandler.post { if (ProcessLifecycleOwner.get().lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) { // we are in foreground, let the sync do the things? - Timber.d("PUSH received in a foreground state, ignore") + Timber.tag(loggerTag.value).d("PUSH received in a foreground state, ignore") } else { onMessageReceivedInternal(message.data) } @@ -121,7 +132,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { * you retrieve the token. */ override fun onNewToken(refreshedToken: String) { - Timber.i("onNewToken: FCM Token has been updated") + Timber.tag(loggerTag.value).i("onNewToken: FCM Token has been updated") FcmHelper.storeFcmToken(this, refreshedToken) if (vectorPreferences.areNotificationEnabledForDevice() && activeSessionHolder.hasActiveSession()) { pusherManager.registerPusherWithFcmKey(refreshedToken) @@ -138,7 +149,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { * It is recommended that the app do a full sync with the app server after receiving this call. */ override fun onDeletedMessages() { - Timber.v("## onDeletedMessages()") + Timber.tag(loggerTag.value).v("## onDeletedMessages()") } /** @@ -150,9 +161,9 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { private fun onMessageReceivedInternal(data: Map) { try { if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) { - Timber.d("## onMessageReceivedInternal() : $data") + Timber.tag(loggerTag.value).d("## onMessageReceivedInternal() : $data") } else { - Timber.d("## onMessageReceivedInternal() : $data") + Timber.tag(loggerTag.value).d("## onMessageReceivedInternal()") } // update the badge counter @@ -162,24 +173,24 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { val session = activeSessionHolder.getSafeActiveSession() if (session == null) { - Timber.w("## Can't sync from push, no current session") + Timber.tag(loggerTag.value).w("## Can't sync from push, no current session") } else { val eventId = data["event_id"] val roomId = data["room_id"] if (isEventAlreadyKnown(eventId, roomId)) { - Timber.d("Ignoring push, event already known") + Timber.tag(loggerTag.value).d("Ignoring push, event already known") } else { // Try to get the Event content faster - Timber.d("Requesting event in fast lane") + Timber.tag(loggerTag.value).d("Requesting event in fast lane") getEventFastLane(session, roomId, eventId) - Timber.d("Requesting background sync") + Timber.tag(loggerTag.value).d("Requesting background sync") session.requireBackgroundSync() } } } catch (e: Exception) { - Timber.e(e, "## onMessageReceivedInternal() failed") + Timber.tag(loggerTag.value).e(e, "## onMessageReceivedInternal() failed") } } @@ -193,18 +204,18 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { } if (wifiDetector.isConnectedToWifi().not()) { - Timber.d("No WiFi network, do not get Event") + Timber.tag(loggerTag.value).d("No WiFi network, do not get Event") return } coroutineScope.launch { - Timber.d("Fast lane: start request") + Timber.tag(loggerTag.value).d("Fast lane: start request") val event = tryOrNull { session.getEvent(roomId, eventId) } ?: return@launch val resolvedEvent = notifiableEventResolver.resolveInMemoryEvent(session, event) resolvedEvent - ?.also { Timber.d("Fast lane: notify drawer") } + ?.also { Timber.tag(loggerTag.value).d("Fast lane: notify drawer") } ?.let { it.isPushGatewayEvent = true notificationDrawerManager.onNotifiableEventReceived(it) @@ -222,7 +233,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { val room = session.getRoom(roomId) ?: return false return room.getTimeLineEvent(eventId) != null } catch (e: Exception) { - Timber.e(e, "## isEventAlreadyKnown() : failed to check if the event was already defined") + Timber.tag(loggerTag.value).e(e, "## isEventAlreadyKnown() : failed to check if the event was already defined") } } return false @@ -230,7 +241,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { private fun handleNotificationWithoutSyncingMode(data: Map, session: Session?) { if (session == null) { - Timber.e("## handleNotificationWithoutSyncingMode cannot find session") + Timber.tag(loggerTag.value).e("## handleNotificationWithoutSyncingMode cannot find session") return } @@ -263,9 +274,9 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { val notifiableEvent = notifiableEventResolver.resolveEvent(event, session) if (notifiableEvent == null) { - Timber.e("Unsupported notifiable event $eventId") + Timber.tag(loggerTag.value).e("Unsupported notifiable event $eventId") if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) { - Timber.e("--> $event") + Timber.tag(loggerTag.value).e("--> $event") } } else { if (notifiableEvent is NotifiableMessageEvent) { diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index df85e31208..61867521d5 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -83,6 +83,10 @@ android:name="android.max_aspect" android:value="9.9" /> + + @@ -105,8 +109,8 @@ @@ -122,8 +126,8 @@ @@ -180,7 +184,12 @@ - + + + @@ -196,6 +205,32 @@ + + + + + + + + + + + + + + + + + @@ -230,27 +265,6 @@ - - - - - - - - - - - - - - - + android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation" + android:excludeFromRecents="true" + android:launchMode="singleTask" + android:supportsPictureInPicture="true" + android:taskAffinity=".features.call.VectorCallActivity" /> + +
    +
  • + hyuwah/DraggableView +
    + hyuwah/DraggableView is licensed under the MIT License + Copyright (c) 2018 Muhammad Wahyudin +
  • +
+
+
+Copyright (c) 2018 Muhammad Wahyudin
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+
  • com.github.piasy:BigImageViewer diff --git a/vector/src/main/java/im/vector/app/AppStateHandler.kt b/vector/src/main/java/im/vector/app/AppStateHandler.kt index 06174b9573..e6bc1b08a2 100644 --- a/vector/src/main/java/im/vector/app/AppStateHandler.kt +++ b/vector/src/main/java/im/vector/app/AppStateHandler.kt @@ -59,7 +59,20 @@ class AppStateHandler @Inject constructor( val selectedRoomGroupingObservable = selectedSpaceDataSource.observe() - fun getCurrentRoomGroupingMethod(): RoomGroupingMethod? = selectedSpaceDataSource.currentValue?.orNull() + fun getCurrentRoomGroupingMethod(): RoomGroupingMethod? { + // XXX we should somehow make it live :/ just a work around + // For example just after creating a space and switching to it the + // name in the app Bar could show Empty Room, and it will not update unless you + // switch space + return selectedSpaceDataSource.currentValue?.orNull()?.let { + if (it is RoomGroupingMethod.BySpace) { + // try to refresh sum? + it.spaceSummary?.roomId?.let { activeSessionHolder.getSafeActiveSession()?.getRoomSummary(it) }?.let { + RoomGroupingMethod.BySpace(it) + } ?: it + } else it + } + } fun setCurrentSpace(spaceId: String?, session: Session? = null) { val uSession = session ?: activeSessionHolder.getSafeActiveSession() ?: return diff --git a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt index cce0c2a66f..99c94a7804 100644 --- a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt @@ -106,6 +106,7 @@ import im.vector.app.features.roomprofile.RoomProfileFragment import im.vector.app.features.roomprofile.alias.RoomAliasFragment import im.vector.app.features.roomprofile.banned.RoomBannedMemberListFragment import im.vector.app.features.roomprofile.members.RoomMemberListFragment +import im.vector.app.features.roomprofile.notifications.RoomNotificationSettingsFragment import im.vector.app.features.roomprofile.permissions.RoomPermissionsFragment import im.vector.app.features.roomprofile.settings.RoomSettingsFragment import im.vector.app.features.roomprofile.settings.joinrule.RoomJoinRuleChooseRestrictedFragment @@ -141,9 +142,11 @@ import im.vector.app.features.signout.soft.SoftLogoutFragment import im.vector.app.features.spaces.SpaceListFragment import im.vector.app.features.spaces.create.ChoosePrivateSpaceTypeFragment import im.vector.app.features.spaces.create.ChooseSpaceTypeFragment +import im.vector.app.features.spaces.create.CreateSpaceAdd3pidInvitesFragment import im.vector.app.features.spaces.create.CreateSpaceDefaultRoomsFragment import im.vector.app.features.spaces.create.CreateSpaceDetailsFragment import im.vector.app.features.spaces.explore.SpaceDirectoryFragment +import im.vector.app.features.spaces.leave.SpaceLeaveAdvancedFragment import im.vector.app.features.spaces.manage.SpaceAddRoomFragment import im.vector.app.features.spaces.manage.SpaceManageRoomsFragment import im.vector.app.features.spaces.manage.SpaceSettingsFragment @@ -717,6 +720,11 @@ interface FragmentModule { @FragmentKey(RoomBannedMemberListFragment::class) fun bindRoomBannedMemberListFragment(fragment: RoomBannedMemberListFragment): Fragment + @Binds + @IntoMap + @FragmentKey(RoomNotificationSettingsFragment::class) + fun bindRoomNotificationSettingsFragment(fragment: RoomNotificationSettingsFragment): Fragment + @Binds @IntoMap @FragmentKey(SearchFragment::class) @@ -787,6 +795,11 @@ interface FragmentModule { @FragmentKey(ChoosePrivateSpaceTypeFragment::class) fun bindChoosePrivateSpaceTypeFragment(fragment: ChoosePrivateSpaceTypeFragment): Fragment + @Binds + @IntoMap + @FragmentKey(CreateSpaceAdd3pidInvitesFragment::class) + fun bindCreateSpaceAdd3pidInvitesFragment(fragment: CreateSpaceAdd3pidInvitesFragment): Fragment + @Binds @IntoMap @FragmentKey(SpaceAddRoomFragment::class) @@ -816,4 +829,9 @@ interface FragmentModule { @IntoMap @FragmentKey(RoomJoinRuleChooseRestrictedFragment::class) fun bindRoomJoinRuleChooseRestrictedFragment(fragment: RoomJoinRuleChooseRestrictedFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(SpaceLeaveAdvancedFragment::class) + fun bindSpaceLeaveAdvancedFragment(fragment: SpaceLeaveAdvancedFragment): Fragment } diff --git a/vector/src/main/java/im/vector/app/core/di/ScreenComponent.kt b/vector/src/main/java/im/vector/app/core/di/ScreenComponent.kt index 2ec330efe6..76b511d2bd 100644 --- a/vector/src/main/java/im/vector/app/core/di/ScreenComponent.kt +++ b/vector/src/main/java/im/vector/app/core/di/ScreenComponent.kt @@ -62,7 +62,6 @@ import im.vector.app.features.matrixto.MatrixToBottomSheet import im.vector.app.features.media.BigImageViewerActivity import im.vector.app.features.media.VectorAttachmentViewerActivity import im.vector.app.features.navigation.Navigator -import im.vector.app.features.permalink.PermalinkHandlerActivity import im.vector.app.features.pin.PinLocker import im.vector.app.features.qrcode.QrCodeScannerActivity import im.vector.app.features.rageshake.BugReportActivity @@ -84,10 +83,12 @@ import im.vector.app.features.settings.devices.DeviceVerificationInfoBottomSheet import im.vector.app.features.share.IncomingShareActivity import im.vector.app.features.signout.soft.SoftLogoutActivity import im.vector.app.features.spaces.InviteRoomSpaceChooserBottomSheet +import im.vector.app.features.spaces.LeaveSpaceBottomSheet import im.vector.app.features.spaces.SpaceCreationActivity import im.vector.app.features.spaces.SpaceExploreActivity import im.vector.app.features.spaces.SpaceSettingsMenuBottomSheet import im.vector.app.features.spaces.invite.SpaceInviteBottomSheet +import im.vector.app.features.spaces.leave.SpaceLeaveAdvancedActivity import im.vector.app.features.spaces.manage.SpaceManageActivity import im.vector.app.features.spaces.share.ShareSpaceBottomSheet import im.vector.app.features.terms.ReviewTermsActivity @@ -96,6 +97,7 @@ import im.vector.app.features.usercode.UserCodeActivity import im.vector.app.features.widgets.WidgetActivity import im.vector.app.features.widgets.permissions.RoomWidgetPermissionBottomSheet import im.vector.app.features.workers.signout.SignOutBottomSheetDialogFragment +import kotlinx.coroutines.CoroutineScope @Component( dependencies = [ @@ -127,6 +129,7 @@ interface ScreenComponent { fun uiStateRepository(): UiStateRepository fun unrecognizedCertificateDialog(): UnrecognizedCertificateDialog fun autoAcceptInvites(): AutoAcceptInvites + fun appCoroutineScope(): CoroutineScope /* ========================================================================================== * Activities @@ -151,7 +154,6 @@ interface ScreenComponent { fun inject(activity: CreateDirectRoomActivity) fun inject(activity: IncomingShareActivity) fun inject(activity: SoftLogoutActivity) - fun inject(activity: PermalinkHandlerActivity) fun inject(activity: QrCodeScannerActivity) fun inject(activity: DebugMenuActivity) fun inject(activity: SharedSecureStorageActivity) @@ -171,6 +173,7 @@ interface ScreenComponent { fun inject(activity: SpaceExploreActivity) fun inject(activity: SpaceManageActivity) fun inject(activity: RoomJoinRuleActivity) + fun inject(activity: SpaceLeaveAdvancedActivity) /* ========================================================================================== * BottomSheets @@ -199,6 +202,7 @@ interface ScreenComponent { fun inject(bottomSheet: SpaceInviteBottomSheet) fun inject(bottomSheet: JoinReplacementRoomBottomSheet) fun inject(bottomSheet: MigrateRoomBottomSheet) + fun inject(bottomSheet: LeaveSpaceBottomSheet) /* ========================================================================================== * Others diff --git a/vector/src/main/java/im/vector/app/core/di/VectorComponent.kt b/vector/src/main/java/im/vector/app/core/di/VectorComponent.kt index 4a3379cb5a..a8bf128367 100644 --- a/vector/src/main/java/im/vector/app/core/di/VectorComponent.kt +++ b/vector/src/main/java/im/vector/app/core/di/VectorComponent.kt @@ -26,11 +26,13 @@ import im.vector.app.EmojiCompatFontProvider import im.vector.app.EmojiCompatWrapper import im.vector.app.VectorApplication import im.vector.app.core.dialogs.UnrecognizedCertificateDialog +import im.vector.app.core.dispatchers.CoroutineDispatchers import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.network.WifiDetector import im.vector.app.core.pushers.PushersManager import im.vector.app.core.utils.AssetReader import im.vector.app.core.utils.DimensionConverter +import im.vector.app.features.call.conference.JitsiActiveConferenceHolder import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.configuration.VectorConfiguration import im.vector.app.features.crypto.keysrequest.KeyRequestHandler @@ -39,7 +41,6 @@ import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.CurrentSpaceSuggestedRoomListDataSource import im.vector.app.features.home.room.detail.RoomDetailPendingActionStore import im.vector.app.features.home.room.detail.timeline.helper.MatrixItemColorProvider -import im.vector.app.features.home.room.detail.timeline.helper.RoomSummariesHolder import im.vector.app.features.html.EventHtmlRenderer import im.vector.app.features.html.VectorHtmlCompressor import im.vector.app.features.invite.AutoAcceptInvites @@ -58,8 +59,10 @@ import im.vector.app.features.rageshake.VectorFileLogger import im.vector.app.features.rageshake.VectorUncaughtExceptionHandler import im.vector.app.features.reactions.data.EmojiDataSource import im.vector.app.features.session.SessionListener +import im.vector.app.features.settings.VectorDataStore import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.ui.UiStateRepository +import kotlinx.coroutines.CoroutineScope import org.matrix.android.sdk.api.Matrix import org.matrix.android.sdk.api.auth.AuthenticationService import org.matrix.android.sdk.api.auth.HomeServerHistoryService @@ -145,6 +148,8 @@ interface VectorComponent { fun vectorPreferences(): VectorPreferences + fun vectorDataStore(): VectorDataStore + fun wifiDetector(): WifiDetector fun vectorFileLogger(): VectorFileLogger @@ -165,7 +170,11 @@ interface VectorComponent { fun webRtcCallManager(): WebRtcCallManager - fun roomSummaryHolder(): RoomSummariesHolder + fun appCoroutineScope(): CoroutineScope + + fun coroutineDispatchers(): CoroutineDispatchers + + fun jitsiActiveConferenceHolder(): JitsiActiveConferenceHolder @Component.Factory interface Factory { diff --git a/vector/src/main/java/im/vector/app/core/di/VectorModule.kt b/vector/src/main/java/im/vector/app/core/di/VectorModule.kt index 006a2f5aa0..ddb765cef8 100644 --- a/vector/src/main/java/im/vector/app/core/di/VectorModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/VectorModule.kt @@ -23,6 +23,7 @@ import android.content.res.Resources import dagger.Binds import dagger.Module import dagger.Provides +import im.vector.app.core.dispatchers.CoroutineDispatchers import im.vector.app.core.error.DefaultErrorFormatter import im.vector.app.core.error.ErrorFormatter import im.vector.app.features.invite.AutoAcceptInvites @@ -33,12 +34,16 @@ import im.vector.app.features.pin.PinCodeStore import im.vector.app.features.pin.SharedPrefPinCodeStore import im.vector.app.features.ui.SharedPreferencesUiStateRepository import im.vector.app.features.ui.UiStateRepository +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob import org.matrix.android.sdk.api.Matrix import org.matrix.android.sdk.api.auth.AuthenticationService import org.matrix.android.sdk.api.auth.HomeServerHistoryService import org.matrix.android.sdk.api.legacy.LegacySessionImporter import org.matrix.android.sdk.api.raw.RawService import org.matrix.android.sdk.api.session.Session +import javax.inject.Singleton @Module abstract class VectorModule { @@ -94,6 +99,19 @@ abstract class VectorModule { fun providesHomeServerHistoryService(matrix: Matrix): HomeServerHistoryService { return matrix.homeServerHistoryService() } + + @Provides + @JvmStatic + @Singleton + fun providesApplicationCoroutineScope(): CoroutineScope { + return CoroutineScope(SupervisorJob() + Dispatchers.Main) + } + + @Provides + @JvmStatic + fun providesCoroutineDispatchers(): CoroutineDispatchers { + return CoroutineDispatchers(io = Dispatchers.IO) + } } @Binds diff --git a/vector/src/main/java/im/vector/app/core/dispatchers/CoroutineDispatchers.kt b/vector/src/main/java/im/vector/app/core/dispatchers/CoroutineDispatchers.kt new file mode 100644 index 0000000000..c489290a55 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/dispatchers/CoroutineDispatchers.kt @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.core.dispatchers + +import kotlinx.coroutines.CoroutineDispatcher +import javax.inject.Inject + +data class CoroutineDispatchers @Inject constructor(val io: CoroutineDispatcher) diff --git a/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetRadioActionItem.kt b/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetRadioActionItem.kt index 91361aa89d..8899532d04 100644 --- a/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetRadioActionItem.kt +++ b/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetRadioActionItem.kt @@ -27,6 +27,7 @@ import im.vector.app.core.epoxy.ClickListener import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.epoxy.onClick +import im.vector.app.core.extensions.setAttributeTintedImageResource import im.vector.app.core.extensions.setTextOrHide /** @@ -62,7 +63,7 @@ abstract class BottomSheetRadioActionItem : VectorEpoxyModel() { + + @EpoxyAttribute + var encrypted: Boolean = false + + @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) + var clickListener: ClickListener? = null + + override fun bind(holder: Holder) { + super.bind(holder) + val accountSettingsString = holder.view.context.getString(R.string.room_settings_room_notifications_account_settings) + val manageNotificationsString = holder.view.context.getString( + R.string.room_settings_room_notifications_manage_notifications, + accountSettingsString + ) + val manageNotificationsBuilder = StringBuilder(manageNotificationsString) + if (encrypted) { + val encryptionNotice = holder.view.context.getString(R.string.room_settings_room_notifications_encryption_notice) + manageNotificationsBuilder.appendLine().append(encryptionNotice) + } + + holder.textView.setTextWithColoredPart( + manageNotificationsBuilder.toString(), + accountSettingsString, + underline = true + ) { + clickListener?.invoke(holder.textView) + } + } + + class Holder : VectorEpoxyHolder() { + val textView by bind(R.id.footerText) + } +} diff --git a/vector/src/main/java/im/vector/app/core/epoxy/profiles/notifications/RadioButtonItem.kt b/vector/src/main/java/im/vector/app/core/epoxy/profiles/notifications/RadioButtonItem.kt new file mode 100644 index 0000000000..721d84e050 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/epoxy/profiles/notifications/RadioButtonItem.kt @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.core.epoxy.profiles.notifications + +import android.widget.ImageView +import android.widget.TextView +import androidx.annotation.StringRes +import androidx.core.content.ContextCompat +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.app.R +import im.vector.app.core.epoxy.ClickListener +import im.vector.app.core.epoxy.VectorEpoxyHolder +import im.vector.app.core.epoxy.VectorEpoxyModel +import im.vector.app.core.epoxy.onClick +import im.vector.app.core.extensions.setAttributeTintedImageResource + +@EpoxyModelClass(layout = R.layout.item_radio) +abstract class RadioButtonItem : VectorEpoxyModel() { + + @EpoxyAttribute + var title: CharSequence? = null + + @StringRes + @EpoxyAttribute + var titleRes: Int? = null + + @EpoxyAttribute + var selected = false + + @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) + lateinit var listener: ClickListener + + override fun bind(holder: Holder) { + super.bind(holder) + holder.view.onClick(listener) + if (titleRes != null) { + holder.titleText.setText(titleRes!!) + } else { + holder.titleText.text = title + } + + if (selected) { + holder.radioImage.setAttributeTintedImageResource(R.drawable.ic_radio_on, R.attr.colorPrimary) + holder.radioImage.contentDescription = holder.view.context.getString(R.string.a11y_checked) + } else { + holder.radioImage.setImageDrawable(ContextCompat.getDrawable(holder.view.context, R.drawable.ic_radio_off)) + holder.radioImage.contentDescription = holder.view.context.getString(R.string.a11y_unchecked) + } + } + + class Holder : VectorEpoxyHolder() { + val titleText by bind(R.id.actionTitle) + val radioImage by bind(R.id.radioIcon) + } +} diff --git a/vector/src/main/java/im/vector/app/core/epoxy/profiles/notifications/TextHeaderItem.kt b/vector/src/main/java/im/vector/app/core/epoxy/profiles/notifications/TextHeaderItem.kt new file mode 100644 index 0000000000..2dfe7be2e6 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/epoxy/profiles/notifications/TextHeaderItem.kt @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.core.epoxy.profiles.notifications + +import android.widget.TextView +import androidx.annotation.StringRes +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.app.R +import im.vector.app.core.epoxy.VectorEpoxyHolder +import im.vector.app.core.epoxy.VectorEpoxyModel + +@EpoxyModelClass(layout = R.layout.item_text_header) +abstract class TextHeaderItem : VectorEpoxyModel() { + + @EpoxyAttribute + var text: String? = null + + @StringRes + @EpoxyAttribute + var textRes: Int? = null + + override fun bind(holder: Holder) { + super.bind(holder) + val textResource = textRes + if (textResource != null) { + holder.textView.setText(textResource) + } else { + holder.textView.text = text + } + } + + class Holder : VectorEpoxyHolder() { + val textView by bind(R.id.headerText) + } +} diff --git a/vector/src/main/java/im/vector/app/core/extensions/TextView.kt b/vector/src/main/java/im/vector/app/core/extensions/TextView.kt index bb991ac32c..0872edeafd 100644 --- a/vector/src/main/java/im/vector/app/core/extensions/TextView.kt +++ b/vector/src/main/java/im/vector/app/core/extensions/TextView.kt @@ -39,13 +39,15 @@ import im.vector.app.features.themes.ThemeUtils /** * Set a text in the TextView, or set visibility to GONE if the text is null */ -fun TextView.setTextOrHide(newText: CharSequence?, hideWhenBlank: Boolean = true) { +fun TextView.setTextOrHide(newText: CharSequence?, hideWhenBlank: Boolean = true, vararg relatedViews: View = emptyArray()) { if (newText == null || (newText.isBlank() && hideWhenBlank)) { isVisible = false + relatedViews.forEach { it.isVisible = false } } else { this.text = newText isVisible = true + relatedViews.forEach { it.isVisible = true } } } @@ -65,6 +67,23 @@ fun TextView.setTextWithColoredPart(@StringRes fullTextRes: Int, val coloredPart = resources.getString(coloredTextRes) // Insert colored part into the full text val fullText = resources.getString(fullTextRes, coloredPart) + + setTextWithColoredPart(fullText, coloredPart, colorAttribute, underline, onClick) +} + +/** + * Set text with a colored part + * @param fullText The full text. + * @param coloredPart The colored part of the text + * @param colorAttribute attribute of the color. Default to colorPrimary + * @param underline true to also underline the text. Default to false + * @param onClick attributes to handle click on the colored part if needed + */ +fun TextView.setTextWithColoredPart(fullText: String, + coloredPart: String, + @AttrRes colorAttribute: Int = R.attr.colorPrimary, + underline: Boolean = false, + onClick: (() -> Unit)? = null) { val color = ThemeUtils.getColor(context, colorAttribute) val foregroundSpan = ForegroundColorSpan(color) diff --git a/vector/src/main/java/im/vector/app/core/extensions/ViewExtensions.kt b/vector/src/main/java/im/vector/app/core/extensions/ViewExtensions.kt index 92dc76670f..54fcac42d1 100644 --- a/vector/src/main/java/im/vector/app/core/extensions/ViewExtensions.kt +++ b/vector/src/main/java/im/vector/app/core/extensions/ViewExtensions.kt @@ -16,12 +16,20 @@ package im.vector.app.core.extensions +import android.graphics.drawable.Drawable import android.text.InputType import android.view.View import android.view.ViewGroup import android.widget.EditText +import android.widget.ImageView +import androidx.annotation.AttrRes +import androidx.annotation.DrawableRes import androidx.appcompat.widget.SearchView +import androidx.core.content.ContextCompat +import androidx.core.graphics.drawable.DrawableCompat +import androidx.core.view.isVisible import im.vector.app.R +import im.vector.app.features.themes.ThemeUtils /** * Remove left margin of a SearchView @@ -50,3 +58,25 @@ fun View.getMeasurements(): Pair { val height = measuredHeight return width to height } + +fun ImageView.setDrawableOrHide(drawableRes: Drawable?) { + setImageDrawable(drawableRes) + isVisible = drawableRes != null +} + +fun View.setAttributeTintedBackground(@DrawableRes drawableRes: Int, @AttrRes tint: Int) { + val drawable = ContextCompat.getDrawable(context, drawableRes)!! + DrawableCompat.setTint(drawable, ThemeUtils.getColor(context, tint)) + background = drawable +} + +fun ImageView.setAttributeTintedImageResource(@DrawableRes drawableRes: Int, @AttrRes tint: Int) { + val drawable = ContextCompat.getDrawable(context, drawableRes)!! + DrawableCompat.setTint(drawable, ThemeUtils.getColor(context, tint)) + setImageDrawable(drawable) +} + +fun View.setAttributeBackground(@AttrRes attributeId: Int) { + val attribute = ThemeUtils.getAttribute(context, attributeId)!! + setBackgroundResource(attribute.resourceId) +} diff --git a/vector/src/main/java/im/vector/app/core/platform/LifecycleAwareLazy.kt b/vector/src/main/java/im/vector/app/core/platform/LifecycleAwareLazy.kt new file mode 100644 index 0000000000..283106232e --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/platform/LifecycleAwareLazy.kt @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.core.platform + +import androidx.annotation.MainThread +import androidx.fragment.app.Fragment +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleObserver +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.OnLifecycleEvent + +fun LifecycleOwner.lifecycleAwareLazy(initializer: () -> T): Lazy = LifecycleAwareLazy(this, initializer) + +private object UninitializedValue + +class LifecycleAwareLazy( + private val owner: LifecycleOwner, + initializer: () -> T +) : Lazy, LifecycleObserver { + + private var initializer: (() -> T)? = initializer + + private var _value: Any? = UninitializedValue + + @Suppress("UNCHECKED_CAST") + override val value: T + @MainThread + get() { + if (_value === UninitializedValue) { + _value = initializer!!() + attachToLifecycle() + } + return _value as T + } + + @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) + fun resetValue() { + _value = UninitializedValue + detachFromLifecycle() + } + + private fun attachToLifecycle() { + if (getLifecycleOwner().lifecycle.currentState == Lifecycle.State.DESTROYED) { + throw IllegalStateException("Initialization failed because lifecycle has been destroyed!") + } + getLifecycleOwner().lifecycle.addObserver(this) + } + + private fun detachFromLifecycle() { + getLifecycleOwner().lifecycle.removeObserver(this) + } + + private fun getLifecycleOwner() = when (owner) { + is Fragment -> owner.viewLifecycleOwner + else -> owner + } + + override fun isInitialized(): Boolean = _value !== UninitializedValue + + override fun toString(): String = if (isInitialized()) value.toString() else "Lazy value not initialized yet." +} diff --git a/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt b/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt index 61abbd445b..dc19520865 100644 --- a/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt +++ b/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt @@ -267,6 +267,7 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasSc activeSessionHolder.getActiveSession().sessionParams.homeServerHost ?: "") is GlobalError.CertificateError -> handleCertificateError(globalError) + GlobalError.ExpiredAccount -> Unit // TODO Handle account expiration }.exhaustive } diff --git a/vector/src/main/java/im/vector/app/core/preference/KeywordPreference.kt b/vector/src/main/java/im/vector/app/core/preference/KeywordPreference.kt new file mode 100644 index 0000000000..b57bb27671 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/preference/KeywordPreference.kt @@ -0,0 +1,167 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.core.preference + +import android.content.Context +import android.text.Editable +import android.util.AttributeSet +import android.view.inputmethod.EditorInfo +import android.widget.Button +import android.widget.EditText +import androidx.core.view.children +import androidx.preference.PreferenceViewHolder +import com.google.android.material.chip.Chip +import com.google.android.material.chip.ChipGroup +import com.google.android.material.textfield.TextInputLayout +import im.vector.app.R +import im.vector.app.core.epoxy.addTextChangedListenerOnce +import im.vector.app.core.platform.SimpleTextWatcher + +class KeywordPreference : VectorPreference { + + interface Listener { + fun onFocusDidChange(hasFocus: Boolean) + fun didAddKeyword(keyword: String) + fun didRemoveKeyword(keyword: String) + } + + private var keywordsEnabled = true + private var isCurrentKeywordValid = true + + private var _keywords: LinkedHashSet = linkedSetOf() + + var keywords: Set + get() { + return _keywords + } + set(value) { + // Updates existing `LinkedHashSet` vs assign a new set. + // This preserves the order added while on the screen (avoids keywords jumping around). + _keywords.removeAll(_keywords.filter { !value.contains(it) }) + _keywords.addAll(value.sorted()) + notifyChanged() + } + + var listener: Listener? = null + + constructor(context: Context) : super(context) + + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) + + constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(context, attrs, defStyle) + + init { + layoutResource = R.layout.vector_preference_chip_group + } + + override fun setEnabled(enabled: Boolean) { + super.setEnabled(enabled) + keywordsEnabled = enabled + notifyChanged() + } + + override fun onBindViewHolder(holder: PreferenceViewHolder) { + super.onBindViewHolder(holder) + + holder.itemView.setOnClickListener(null) + holder.itemView.setOnLongClickListener(null) + + val chipEditText = holder.findViewById(R.id.chipEditText) as? EditText ?: return + val chipGroup = holder.findViewById(R.id.chipGroup) as? ChipGroup ?: return + val addKeywordButton = holder.findViewById(R.id.addKeywordButton) as? Button ?: return + val chipTextInputLayout = holder.findViewById(R.id.chipTextInputLayout) as? TextInputLayout ?: return + + chipEditText.text = null + chipGroup.removeAllViews() + + keywords.forEach { + addChipToGroup(it, chipGroup) + } + + chipEditText.isEnabled = keywordsEnabled + chipGroup.isEnabled = keywordsEnabled + chipGroup.children.forEach { it.isEnabled = keywordsEnabled } + + chipEditText.addTextChangedListenerOnce(onTextChangeListener(chipTextInputLayout, addKeywordButton)) + chipEditText.setOnEditorActionListener { _, actionId, _ -> + if (actionId != EditorInfo.IME_ACTION_DONE) { + return@setOnEditorActionListener false + } + return@setOnEditorActionListener addKeyword(chipEditText) + } + chipEditText.setOnFocusChangeListener { _, hasFocus -> + listener?.onFocusDidChange(hasFocus) + } + + addKeywordButton.setOnClickListener { + addKeyword(chipEditText) + } + } + + private fun addKeyword(chipEditText: EditText): Boolean { + val keyword = chipEditText.text.toString().trim() + + if (!isCurrentKeywordValid || keyword.isEmpty()) { + return false + } + + listener?.didAddKeyword(keyword) + onPreferenceChangeListener?.onPreferenceChange(this, _keywords) + notifyChanged() + chipEditText.text = null + return true + } + + private fun onTextChangeListener(chipTextInputLayout: TextInputLayout, addKeywordButton: Button) = object : SimpleTextWatcher() { + override fun afterTextChanged(s: Editable) { + val keyword = s.toString().trim() + val errorMessage = when { + keyword.startsWith(".") -> { + context.getString(R.string.settings_notification_keyword_contains_dot) + } + keyword.contains("\\") -> { + context.getString(R.string.settings_notification_keyword_contains_invalid_character, "\\") + } + keyword.contains("/") -> { + context.getString(R.string.settings_notification_keyword_contains_invalid_character, "/") + } + else -> null + } + + chipTextInputLayout.isErrorEnabled = errorMessage != null + chipTextInputLayout.error = errorMessage + val keywordValid = errorMessage == null + addKeywordButton.isEnabled = keywordsEnabled && keywordValid + this@KeywordPreference.isCurrentKeywordValid = keywordValid + } + } + + private fun addChipToGroup(keyword: String, chipGroup: ChipGroup) { + val chip = Chip(context, null, R.attr.vctr_keyword_style) + chip.text = keyword + chipGroup.addView(chip) + + chip.setOnCloseIconClickListener { + if (!keywordsEnabled) { + return@setOnCloseIconClickListener + } + listener?.didRemoveKeyword(keyword) + onPreferenceChangeListener?.onPreferenceChange(this, _keywords) + notifyChanged() + } + } +} diff --git a/vector/src/main/java/im/vector/app/core/pushers/PushersManager.kt b/vector/src/main/java/im/vector/app/core/pushers/PushersManager.kt index 5896122393..a27765bf4f 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/PushersManager.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/PushersManager.kt @@ -61,6 +61,23 @@ class PushersManager @Inject constructor( ) } + fun registerEmailForPush(email: String) { + val currentSession = activeSessionHolder.getActiveSession() + val appName = appNameProvider.getAppName() + currentSession.addEmailPusher( + email = email, + lang = localeProvider.current().language, + emailBranding = appName, + appDisplayName = appName, + deviceDisplayName = currentSession.sessionParams.deviceId ?: "MOBILE" + ) + } + + suspend fun unregisterEmailPusher(email: String) { + val currentSession = activeSessionHolder.getSafeActiveSession() ?: return + currentSession.removeEmailPusher(email) + } + suspend fun unregisterPusher(pushKey: String) { val currentSession = activeSessionHolder.getSafeActiveSession() ?: return currentSession.removeHttpPusher(pushKey, stringProvider.getString(R.string.pusher_app_id)) diff --git a/vector/src/main/java/im/vector/app/core/services/CallRingPlayer.kt b/vector/src/main/java/im/vector/app/core/services/CallRingPlayer.kt index f23eb07424..faa921b99e 100644 --- a/vector/src/main/java/im/vector/app/core/services/CallRingPlayer.kt +++ b/vector/src/main/java/im/vector/app/core/services/CallRingPlayer.kt @@ -98,13 +98,10 @@ class CallRingPlayerOutgoing( private var player: MediaPlayer? = null fun start() { - val audioManager: AudioManager? = applicationContext.getSystemService() + applicationContext.getSystemService()?.mode = AudioManager.MODE_IN_COMMUNICATION player?.release() player = createPlayer() - - // Check if sound is enabled - val ringerMode = audioManager?.ringerMode - if (player != null && ringerMode == AudioManager.RINGER_MODE_NORMAL) { + if (player != null) { try { if (player?.isPlaying == false) { player?.start() @@ -116,8 +113,6 @@ class CallRingPlayerOutgoing( Timber.e(failure, "## VOIP Failed to start ringing outgoing") player = null } - } else { - Timber.v("## VOIP Can't play $player ode $ringerMode") } } diff --git a/vector/src/main/java/im/vector/app/core/services/CallService.kt b/vector/src/main/java/im/vector/app/core/services/CallService.kt index d8cf8cf6b8..cd3845f41b 100644 --- a/vector/src/main/java/im/vector/app/core/services/CallService.kt +++ b/vector/src/main/java/im/vector/app/core/services/CallService.kt @@ -50,7 +50,7 @@ private val loggerTag = LoggerTag("CallService", LoggerTag.VOIP) class CallService : VectorService() { private val connections = mutableMapOf() - private val knownCalls = mutableSetOf() + private val knownCalls = mutableMapOf() private val connectedCallIds = mutableSetOf() private lateinit var notificationManager: NotificationManagerCompat @@ -190,7 +190,7 @@ class CallService : VectorService() { } else { notificationManager.notify(callId.hashCode(), notification) } - knownCalls.add(callInformation) + knownCalls[callId] = callInformation } private fun handleCallTerminated(intent: Intent) { @@ -198,20 +198,22 @@ class CallService : VectorService() { val endCallReason = intent.getSerializableExtra(EXTRA_END_CALL_REASON) as EndCallReason val rejected = intent.getBooleanExtra(EXTRA_END_CALL_REJECTED, false) alertManager.cancelAlert(callId) - val terminatedCall = knownCalls.firstOrNull { it.callId == callId } + val terminatedCall = knownCalls.remove(callId) if (terminatedCall == null) { - Timber.tag(loggerTag.value).v("Call terminated for unknown call $callId$") + Timber.tag(loggerTag.value).v("Call terminated for unknown call $callId") handleUnexpectedState(callId) return } - knownCalls.remove(terminatedCall) + val notification = notificationUtils.buildCallEndedNotification(false) + val notificationId = callId.hashCode() + startForeground(notificationId, notification) if (knownCalls.isEmpty()) { + Timber.tag(loggerTag.value).v("No more call, stop the service") + stopForeground(true) mediaSession?.isActive = false myStopSelf() } val wasConnected = connectedCallIds.remove(callId) - val notification = notificationUtils.buildCallEndedNotification(terminatedCall.isVideoCall) - notificationManager.notify(callId.hashCode(), notification) if (!wasConnected && !terminatedCall.isOutgoing && !rejected && endCallReason != EndCallReason.ANSWERED_ELSEWHERE) { val missedCallNotification = notificationUtils.buildCallMissedNotification(terminatedCall) notificationManager.notify(MISSED_CALL_TAG, terminatedCall.nativeRoomId.hashCode(), missedCallNotification) @@ -243,7 +245,7 @@ class CallService : VectorService() { } else { notificationManager.notify(callId.hashCode(), notification) } - knownCalls.add(callInformation) + knownCalls[callId] = callInformation } /** @@ -267,18 +269,19 @@ class CallService : VectorService() { } else { notificationManager.notify(callId.hashCode(), notification) } - knownCalls.add(callInformation) + knownCalls[callId] = callInformation } private fun handleUnexpectedState(callId: String?) { Timber.tag(loggerTag.value).v("Fallback to clear everything") callRingPlayerIncoming?.stop() callRingPlayerOutgoing?.stop() - if (callId != null) { - notificationManager.cancel(callId.hashCode()) - } val notification = notificationUtils.buildCallEndedNotification(false) - startForeground(DEFAULT_NOTIFICATION_ID, notification) + if (callId != null) { + startForeground(callId.hashCode(), notification) + } else { + startForeground(DEFAULT_NOTIFICATION_ID, notification) + } if (knownCalls.isEmpty()) { mediaSession?.isActive = false myStopSelf() @@ -371,7 +374,7 @@ class CallService : VectorService() { putExtra(EXTRA_END_CALL_REASON, endCallReason) putExtra(EXTRA_END_CALL_REJECTED, rejected) } - ContextCompat.startForegroundService(context, intent) + context.startService(intent) } } diff --git a/vector/src/main/java/im/vector/app/core/services/WiredHeadsetStateReceiver.kt b/vector/src/main/java/im/vector/app/core/services/WiredHeadsetStateReceiver.kt index e276e24851..ac6ced002e 100644 --- a/vector/src/main/java/im/vector/app/core/services/WiredHeadsetStateReceiver.kt +++ b/vector/src/main/java/im/vector/app/core/services/WiredHeadsetStateReceiver.kt @@ -21,7 +21,6 @@ import android.content.Context import android.content.Intent import android.content.IntentFilter import android.media.AudioManager -import android.os.Build import timber.log.Timber import java.lang.ref.WeakReference @@ -69,11 +68,7 @@ class WiredHeadsetStateReceiver : BroadcastReceiver() { fun createAndRegister(context: Context, listener: HeadsetEventListener): WiredHeadsetStateReceiver { val receiver = WiredHeadsetStateReceiver() receiver.delegate = WeakReference(listener) - val action = if (Build.VERSION.SDK_INT >= 21) { - AudioManager.ACTION_HEADSET_PLUG - } else { - Intent.ACTION_HEADSET_PLUG - } + val action = AudioManager.ACTION_HEADSET_PLUG context.registerReceiver(receiver, IntentFilter(action)) return receiver } diff --git a/vector/src/main/java/im/vector/app/core/ui/views/ActiveConferenceView.kt b/vector/src/main/java/im/vector/app/core/ui/views/ActiveConferenceView.kt deleted file mode 100644 index 256f2d963e..0000000000 --- a/vector/src/main/java/im/vector/app/core/ui/views/ActiveConferenceView.kt +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright (c) 2020 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package im.vector.app.core.ui.views - -import android.content.Context -import android.text.SpannableString -import android.text.method.LinkMovementMethod -import android.text.style.ClickableSpan -import android.util.AttributeSet -import android.view.View -import android.widget.RelativeLayout -import androidx.core.view.isVisible -import im.vector.app.R -import im.vector.app.core.utils.tappableMatchingText -import im.vector.app.databinding.ViewActiveConferenceViewBinding -import im.vector.app.features.home.room.detail.RoomDetailViewState -import im.vector.app.features.themes.ThemeUtils -import org.matrix.android.sdk.api.session.room.model.Membership -import org.matrix.android.sdk.api.session.widgets.model.Widget -import org.matrix.android.sdk.api.session.widgets.model.WidgetType - -class ActiveConferenceView @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0 -) : RelativeLayout(context, attrs, defStyleAttr) { - - interface Callback { - fun onTapJoinAudio(jitsiWidget: Widget) - fun onTapJoinVideo(jitsiWidget: Widget) - fun onDelete(jitsiWidget: Widget) - } - - var callback: Callback? = null - private var jitsiWidget: Widget? = null - - private lateinit var views: ViewActiveConferenceViewBinding - - init { - setupView() - } - - private fun setupView() { - inflate(context, R.layout.view_active_conference_view, this) - views = ViewActiveConferenceViewBinding.bind(this) - setBackgroundColor(ThemeUtils.getColor(context, R.attr.colorPrimary)) - - // "voice" and "video" texts are underlined and clickable - val voiceString = context.getString(R.string.ongoing_conference_call_voice) - val videoString = context.getString(R.string.ongoing_conference_call_video) - - val fullMessage = context.getString(R.string.ongoing_conference_call, voiceString, videoString) - - val styledText = SpannableString(fullMessage) - styledText.tappableMatchingText(voiceString, object : ClickableSpan() { - override fun onClick(widget: View) { - jitsiWidget?.let { - callback?.onTapJoinAudio(it) - } - } - }) - styledText.tappableMatchingText(videoString, object : ClickableSpan() { - override fun onClick(widget: View) { - jitsiWidget?.let { - callback?.onTapJoinVideo(it) - } - } - }) - - views.activeConferenceInfo.apply { - text = styledText - movementMethod = LinkMovementMethod.getInstance() - } - - views.deleteWidgetButton.setOnClickListener { - jitsiWidget?.let { callback?.onDelete(it) } - } - } - - fun render(state: RoomDetailViewState) { - val summary = state.asyncRoomSummary() - if (summary?.membership == Membership.JOIN) { - // We only display banner for 'live' widgets - jitsiWidget = state.activeRoomWidgets()?.firstOrNull { - // for now only jitsi? - it.type == WidgetType.Jitsi - } - - isVisible = jitsiWidget != null - // if sent by me or if i can moderate? - views.deleteWidgetButton.isVisible = state.isAllowedToManageWidgets - } else { - isVisible = false - } - } -} diff --git a/vector/src/main/java/im/vector/app/core/ui/views/BottomSheetActionButton.kt b/vector/src/main/java/im/vector/app/core/ui/views/BottomSheetActionButton.kt index 97194f2c03..a3e8b3780c 100644 --- a/vector/src/main/java/im/vector/app/core/ui/views/BottomSheetActionButton.kt +++ b/vector/src/main/java/im/vector/app/core/ui/views/BottomSheetActionButton.kt @@ -26,6 +26,7 @@ import androidx.core.view.isGone import androidx.core.view.isInvisible import androidx.core.view.isVisible import im.vector.app.R +import im.vector.app.core.extensions.setDrawableOrHide import im.vector.app.core.extensions.setTextOrHide import im.vector.app.databinding.ViewBottomSheetActionButtonBinding import im.vector.app.features.themes.ThemeUtils @@ -80,7 +81,7 @@ class BottomSheetActionButton @JvmOverloads constructor( var rightIcon: Drawable? = null set(value) { field = value - views.bottomSheetActionIcon.setImageDrawable(value) + views.bottomSheetActionIcon.setDrawableOrHide(value) } var tint: Int? = null @@ -95,6 +96,12 @@ class BottomSheetActionButton @JvmOverloads constructor( value?.let { views.bottomSheetActionTitle.setTextColor(it) } } + var isBetaAction: Boolean? = null + set(value) { + field = value + views.bottomSheetActionBeta.isVisible = field ?: false + } + init { inflate(context, R.layout.view_bottom_sheet_action_button, this) views = ViewBottomSheetActionButtonBinding.bind(this) @@ -109,6 +116,8 @@ class BottomSheetActionButton @JvmOverloads constructor( tint = getColor(R.styleable.BottomSheetActionButton_tint, ThemeUtils.getColor(context, android.R.attr.textColor)) titleTextColor = getColor(R.styleable.BottomSheetActionButton_titleTextColor, ThemeUtils.getColor(context, R.attr.colorPrimary)) + + isBetaAction = getBoolean(R.styleable.BottomSheetActionButton_betaAction, false) } } } diff --git a/vector/src/main/java/im/vector/app/core/ui/views/CurrentCallsView.kt b/vector/src/main/java/im/vector/app/core/ui/views/CurrentCallsView.kt index d1332f18dc..2f7eecc22c 100644 --- a/vector/src/main/java/im/vector/app/core/ui/views/CurrentCallsView.kt +++ b/vector/src/main/java/im/vector/app/core/ui/views/CurrentCallsView.kt @@ -18,7 +18,9 @@ package im.vector.app.core.ui.views import android.content.Context import android.util.AttributeSet -import android.widget.RelativeLayout +import android.util.TypedValue +import android.widget.FrameLayout +import androidx.appcompat.content.res.AppCompatResources import im.vector.app.R import im.vector.app.databinding.ViewCurrentCallsBinding import im.vector.app.features.call.webrtc.WebRtcCall @@ -29,7 +31,7 @@ class CurrentCallsView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 -) : RelativeLayout(context, attrs, defStyleAttr) { +) : FrameLayout(context, attrs, defStyleAttr) { interface Callback { fun onTapToReturnToCall() @@ -42,25 +44,33 @@ class CurrentCallsView @JvmOverloads constructor( inflate(context, R.layout.view_current_calls, this) views = ViewCurrentCallsBinding.bind(this) setBackgroundColor(ThemeUtils.getColor(context, R.attr.colorPrimary)) + val outValue = TypedValue().also { + context.theme.resolveAttribute(android.R.attr.selectableItemBackground, it, true) + } + foreground = AppCompatResources.getDrawable(context, outValue.resourceId) setOnClickListener { callback?.onTapToReturnToCall() } } fun render(calls: List, formattedDuration: String) { - val connectedCalls = calls.filter { - it.mxCall.state is CallState.Connected - } - val heldCalls = connectedCalls.filter { - it.isLocalOnHold || it.remoteOnHold - } - if (connectedCalls.isEmpty()) return - views.currentCallsInfo.text = if (connectedCalls.size == heldCalls.size) { - resources.getQuantityString(R.plurals.call_only_paused, heldCalls.size, heldCalls.size) - } else { - if (heldCalls.isEmpty()) { - resources.getString(R.string.call_only_active, formattedDuration) - } else { - resources.getQuantityString(R.plurals.call_one_active_and_other_paused, heldCalls.size, formattedDuration, heldCalls.size) + val tapToReturnFormat = if (calls.size == 1) { + val firstCall = calls.first() + when (firstCall.mxCall.state) { + is CallState.Idle, + is CallState.CreateOffer, + is CallState.LocalRinging, + is CallState.Dialing -> { + resources.getString(R.string.call_ringing) + } + is CallState.Answering -> { + resources.getString(R.string.call_connecting) + } + else -> { + resources.getString(R.string.call_one_active, formattedDuration) + } } + } else { + resources.getQuantityString(R.plurals.call_active_status, calls.size, calls.size) } + views.currentCallsInfo.text = resources.getString(R.string.call_tap_to_return, tapToReturnFormat) } } diff --git a/vector/src/main/java/im/vector/app/core/ui/views/CurrentCallsViewPresenter.kt b/vector/src/main/java/im/vector/app/core/ui/views/CurrentCallsViewPresenter.kt new file mode 100644 index 0000000000..13db5ddcb3 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/ui/views/CurrentCallsViewPresenter.kt @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.core.ui.views + +import androidx.core.view.isVisible +import im.vector.app.features.call.webrtc.WebRtcCall + +class CurrentCallsViewPresenter { + + private var currentCallsView: CurrentCallsView? = null + private var currentCall: WebRtcCall? = null + private var calls: List = emptyList() + + private val tickListener = object : WebRtcCall.Listener { + override fun onTick(formattedDuration: String) { + currentCallsView?.render(calls, formattedDuration) + } + } + + fun updateCall(currentCall: WebRtcCall?, calls: List) { + this.currentCall?.removeListener(tickListener) + this.currentCall = currentCall + this.currentCall?.addListener(tickListener) + this.calls = calls + val hasActiveCall = calls.isNotEmpty() + currentCallsView?.isVisible = hasActiveCall + currentCallsView?.render(calls, currentCall?.formattedDuration() ?: "") + } + + fun bind(activeCallView: CurrentCallsView, interactionListener: CurrentCallsView.Callback) { + this.currentCallsView = activeCallView + this.currentCallsView?.callback = interactionListener + this.currentCall?.addListener(tickListener) + } + + fun unBind() { + this.currentCallsView?.callback = null + this.currentCall?.removeListener(tickListener) + currentCallsView = null + } +} diff --git a/vector/src/main/java/im/vector/app/core/ui/views/FailedMessagesWarningView.kt b/vector/src/main/java/im/vector/app/core/ui/views/FailedMessagesWarningView.kt index f9518552a3..755230b5bf 100644 --- a/vector/src/main/java/im/vector/app/core/ui/views/FailedMessagesWarningView.kt +++ b/vector/src/main/java/im/vector/app/core/ui/views/FailedMessagesWarningView.kt @@ -19,7 +19,6 @@ package im.vector.app.core.ui.views import android.content.Context import android.util.AttributeSet import androidx.constraintlayout.widget.ConstraintLayout -import androidx.core.view.isVisible import im.vector.app.R import im.vector.app.databinding.ViewFailedMessagesWarningBinding @@ -49,8 +48,4 @@ class FailedMessagesWarningView @JvmOverloads constructor( views.failedMessagesDeleteAllButton.setOnClickListener { callback?.onDeleteAllClicked() } views.failedMessagesRetryButton.setOnClickListener { callback?.onRetryClicked() } } - - fun render(hasFailedMessages: Boolean) { - isVisible = hasFailedMessages - } } diff --git a/vector/src/main/java/im/vector/app/core/ui/views/JoinConferenceView.kt b/vector/src/main/java/im/vector/app/core/ui/views/JoinConferenceView.kt new file mode 100644 index 0000000000..fa1e2c1403 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/ui/views/JoinConferenceView.kt @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.core.ui.views + +import android.animation.Animator +import android.animation.ArgbEvaluator +import android.animation.ValueAnimator +import android.annotation.SuppressLint +import android.content.Context +import android.util.AttributeSet +import android.widget.FrameLayout +import androidx.core.content.ContextCompat +import im.vector.app.R +import im.vector.app.databinding.ViewJoinConferenceBinding + +class JoinConferenceView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : FrameLayout(context, attrs, defStyleAttr) { + + var views: ViewJoinConferenceBinding? = null + var onJoinClicked: (() -> Unit)? = null + var backgroundAnimator: Animator? = null + + init { + inflate(context, R.layout.view_join_conference, this) + } + + @SuppressLint("Recycle") + override fun onAttachedToWindow() { + super.onAttachedToWindow() + views = ViewJoinConferenceBinding.bind(this) + views?.joinConferenceButton?.setOnClickListener { onJoinClicked?.invoke() } + val colorFrom = ContextCompat.getColor(context, R.color.palette_element_green) + val colorTo = ContextCompat.getColor(context, R.color.join_conference_animated_color) + // Animate button color to highlight + backgroundAnimator = ValueAnimator.ofObject(ArgbEvaluator(), colorFrom, colorTo).apply { + repeatMode = ValueAnimator.REVERSE + repeatCount = ValueAnimator.INFINITE + duration = 500 + addUpdateListener { animator -> + val color = animator.animatedValue as Int + views?.joinConferenceButton?.setBackgroundColor(color) + } + } + backgroundAnimator?.start() + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + views = null + backgroundAnimator?.cancel() + } +} diff --git a/vector/src/main/java/im/vector/app/core/ui/views/KnownCallsViewHolder.kt b/vector/src/main/java/im/vector/app/core/ui/views/KnownCallsViewHolder.kt deleted file mode 100644 index d49cf929b6..0000000000 --- a/vector/src/main/java/im/vector/app/core/ui/views/KnownCallsViewHolder.kt +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright (c) 2020 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package im.vector.app.core.ui.views - -import androidx.core.view.isVisible -import com.google.android.material.card.MaterialCardView -import im.vector.app.core.epoxy.onClick -import im.vector.app.features.call.utils.EglUtils -import im.vector.app.features.call.webrtc.WebRtcCall -import org.matrix.android.sdk.api.session.call.CallState -import org.webrtc.RendererCommon -import org.webrtc.SurfaceViewRenderer - -class KnownCallsViewHolder { - - private var activeCallPiP: SurfaceViewRenderer? = null - private var currentCallsView: CurrentCallsView? = null - private var pipWrapper: MaterialCardView? = null - private var currentCall: WebRtcCall? = null - private var calls: List = emptyList() - - private var activeCallPipInitialized = false - - private val tickListener = object : WebRtcCall.Listener { - override fun onTick(formattedDuration: String) { - currentCallsView?.render(calls, formattedDuration) - } - } - - fun updateCall(currentCall: WebRtcCall?, calls: List) { - activeCallPiP?.let { - this.currentCall?.detachRenderers(listOf(it)) - } - this.currentCall?.removeListener(tickListener) - this.currentCall = currentCall - this.currentCall?.addListener(tickListener) - this.calls = calls - val hasActiveCall = currentCall?.mxCall?.state is CallState.Connected - if (hasActiveCall) { - val isVideoCall = currentCall?.mxCall?.isVideoCall == true - if (isVideoCall) initIfNeeded() - currentCallsView?.isVisible = !isVideoCall - currentCallsView?.render(calls, currentCall?.formattedDuration() ?: "") - pipWrapper?.isVisible = isVideoCall - activeCallPiP?.isVisible = isVideoCall - activeCallPiP?.let { - currentCall?.attachViewRenderers(null, it, null) - } - } else { - currentCallsView?.isVisible = false - activeCallPiP?.isVisible = false - pipWrapper?.isVisible = false - activeCallPiP?.let { - currentCall?.detachRenderers(listOf(it)) - } - } - } - - private fun initIfNeeded() { - if (!activeCallPipInitialized && activeCallPiP != null) { - activeCallPiP?.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FILL) - EglUtils.rootEglBase?.let { eglBase -> - activeCallPiP?.init(eglBase.eglBaseContext, null) - activeCallPiP?.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_BALANCED) - activeCallPiP?.setEnableHardwareScaler(true /* enabled */) - activeCallPiP?.setZOrderMediaOverlay(true) - activeCallPipInitialized = true - } - } - } - - fun bind(activeCallPiP: SurfaceViewRenderer, - activeCallView: CurrentCallsView, - pipWrapper: MaterialCardView, - interactionListener: CurrentCallsView.Callback) { - this.activeCallPiP = activeCallPiP - this.currentCallsView = activeCallView - this.pipWrapper = pipWrapper - this.currentCallsView?.callback = interactionListener - pipWrapper.onClick { - interactionListener.onTapToReturnToCall() - } - this.currentCall?.addListener(tickListener) - } - - fun unBind() { - activeCallPiP?.let { - currentCall?.detachRenderers(listOf(it)) - } - if (activeCallPipInitialized) { - activeCallPiP?.release() - } - this.currentCallsView?.callback = null - this.currentCall?.removeListener(tickListener) - pipWrapper?.setOnClickListener(null) - activeCallPiP = null - currentCallsView = null - pipWrapper = null - } -} diff --git a/vector/src/main/java/im/vector/app/core/utils/Dialogs.kt b/vector/src/main/java/im/vector/app/core/utils/Dialogs.kt index 7806f2603d..c73fa70388 100644 --- a/vector/src/main/java/im/vector/app/core/utils/Dialogs.kt +++ b/vector/src/main/java/im/vector/app/core/utils/Dialogs.kt @@ -20,6 +20,7 @@ import android.content.Context import android.webkit.WebView import android.webkit.WebViewClient import com.google.android.material.dialog.MaterialAlertDialogBuilder +import im.vector.app.R /** * Open a web view above the current activity. @@ -38,3 +39,14 @@ fun Context.displayInWebView(url: String) { .setPositiveButton(android.R.string.ok, null) .show() } + +fun Context.showIdentityServerConsentDialog(configuredIdentityServer: String?, consentCallBack: (() -> Unit)) { + MaterialAlertDialogBuilder(this) + .setTitle(R.string.identity_server_consent_dialog_title) + .setMessage(getString(R.string.identity_server_consent_dialog_content, configuredIdentityServer ?: "")) + .setPositiveButton(R.string.yes) { _, _ -> + consentCallBack.invoke() + } + .setNegativeButton(R.string.no, null) + .show() +} diff --git a/vector/src/main/java/im/vector/app/core/utils/ExternalApplicationsUtil.kt b/vector/src/main/java/im/vector/app/core/utils/ExternalApplicationsUtil.kt index d0535b667f..bdaf520ba1 100644 --- a/vector/src/main/java/im/vector/app/core/utils/ExternalApplicationsUtil.kt +++ b/vector/src/main/java/im/vector/app/core/utils/ExternalApplicationsUtil.kt @@ -36,6 +36,7 @@ import androidx.activity.result.ActivityResultLauncher import androidx.browser.customtabs.CustomTabColorSchemeParams import androidx.browser.customtabs.CustomTabsIntent import androidx.browser.customtabs.CustomTabsSession +import androidx.core.app.ShareCompat import androidx.core.content.FileProvider import androidx.core.content.getSystemService import im.vector.app.BuildConfig @@ -297,23 +298,19 @@ fun openMedia(activity: Activity, savedMediaPath: String, mimeType: String) { } fun shareMedia(context: Context, file: File, mediaMimeType: String?) { - var mediaUri: Uri? = null - try { - mediaUri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileProvider", file) + val mediaUri = try { + FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileProvider", file) } catch (e: Exception) { Timber.e(e, "onMediaAction Selected File cannot be shared") + return } - if (null != mediaUri) { - val sendIntent = Intent() - sendIntent.action = Intent.ACTION_SEND - // Grant temporary read permission to the content URI - sendIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - sendIntent.type = mediaMimeType - sendIntent.putExtra(Intent.EXTRA_STREAM, mediaUri) + val sendIntent = ShareCompat.IntentBuilder(context) + .setType(mediaMimeType) + .setStream(mediaUri) + .getIntent() - sendShareIntent(context, sendIntent) - } + sendShareIntent(context, sendIntent) } fun shareText(context: Context, text: String) { diff --git a/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorView.kt b/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorView.kt index c0d4669108..eba5dadeda 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorView.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorView.kt @@ -103,11 +103,11 @@ class AttachmentTypeSelectorView(context: Context, animateWindowInCircular(anchor, contentView) } animateButtonIn(views.attachmentGalleryButton, ANIMATION_DURATION / 2) - animateButtonIn(views.attachmentCameraButton, ANIMATION_DURATION / 2) - animateButtonIn(views.attachmentFileButton, ANIMATION_DURATION / 4) - animateButtonIn(views.attachmentAudioButton, ANIMATION_DURATION / 2) + animateButtonIn(views.attachmentCameraButton, ANIMATION_DURATION / 4) + animateButtonIn(views.attachmentFileButton, ANIMATION_DURATION / 2) + animateButtonIn(views.attachmentAudioButton, 0) animateButtonIn(views.attachmentContactButton, ANIMATION_DURATION / 4) - animateButtonIn(views.attachmentStickersButton, 0) + animateButtonIn(views.attachmentStickersButton, ANIMATION_DURATION / 2) } override fun dismiss() { diff --git a/vector/src/main/java/im/vector/app/features/autocomplete/emoji/AutocompleteEmojiPresenter.kt b/vector/src/main/java/im/vector/app/features/autocomplete/emoji/AutocompleteEmojiPresenter.kt index bf180746de..4f272c7a24 100644 --- a/vector/src/main/java/im/vector/app/features/autocomplete/emoji/AutocompleteEmojiPresenter.kt +++ b/vector/src/main/java/im/vector/app/features/autocomplete/emoji/AutocompleteEmojiPresenter.kt @@ -21,6 +21,11 @@ import androidx.recyclerview.widget.RecyclerView import im.vector.app.features.autocomplete.AutocompleteClickListener import im.vector.app.features.autocomplete.RecyclerViewPresenter import im.vector.app.features.reactions.data.EmojiDataSource +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancelChildren +import kotlinx.coroutines.launch import javax.inject.Inject class AutocompleteEmojiPresenter @Inject constructor(context: Context, @@ -28,11 +33,14 @@ class AutocompleteEmojiPresenter @Inject constructor(context: Context, private val controller: AutocompleteEmojiController) : RecyclerViewPresenter(context), AutocompleteClickListener { + private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main) + init { controller.listener = this } fun clear() { + coroutineScope.coroutineContext.cancelChildren() controller.listener = null } @@ -45,12 +53,14 @@ class AutocompleteEmojiPresenter @Inject constructor(context: Context, } override fun onQuery(query: CharSequence?) { - val data = if (query.isNullOrBlank()) { - // Return common emojis - emojiDataSource.getQuickReactions() - } else { - emojiDataSource.filterWith(query.toString()) + coroutineScope.launch { + val data = if (query.isNullOrBlank()) { + // Return common emojis + emojiDataSource.getQuickReactions() + } else { + emojiDataSource.filterWith(query.toString()) + } + controller.setData(data) } - controller.setData(data) } } diff --git a/vector/src/main/java/im/vector/app/features/autocomplete/member/AutocompleteMemberPresenter.kt b/vector/src/main/java/im/vector/app/features/autocomplete/member/AutocompleteMemberPresenter.kt index ecc607f08d..4976cb39b9 100644 --- a/vector/src/main/java/im/vector/app/features/autocomplete/member/AutocompleteMemberPresenter.kt +++ b/vector/src/main/java/im/vector/app/features/autocomplete/member/AutocompleteMemberPresenter.kt @@ -19,8 +19,8 @@ package im.vector.app.features.autocomplete.member import android.content.Context import androidx.recyclerview.widget.RecyclerView import dagger.assisted.Assisted -import dagger.assisted.AssistedInject import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import im.vector.app.features.autocomplete.AutocompleteClickListener import im.vector.app.features.autocomplete.RecyclerViewPresenter import org.matrix.android.sdk.api.query.QueryStringValue @@ -35,7 +35,7 @@ class AutocompleteMemberPresenter @AssistedInject constructor(context: Context, private val controller: AutocompleteMemberController ) : RecyclerViewPresenter(context), AutocompleteClickListener { - private val room = session.getRoom(roomId)!! + private val room by lazy { session.getRoom(roomId)!! } init { controller.listener = this @@ -71,6 +71,23 @@ class AutocompleteMemberPresenter @AssistedInject constructor(context: Context, val members = room.getRoomMembers(queryParams) .asSequence() .sortedBy { it.displayName } + .disambiguate() controller.setData(members.toList()) } } + +private fun Sequence.disambiguate(): Sequence { + val displayNames = hashMapOf().also { map -> + for (item in this) { + item.displayName?.lowercase()?.also { displayName -> + map[displayName] = map.getOrPut(displayName, { 0 }) + 1 + } + } + } + + return map { roomMemberSummary -> + if (displayNames[roomMemberSummary.displayName?.lowercase()] ?: 0 > 1) { + roomMemberSummary.copy(displayName = roomMemberSummary.displayName + " " + roomMemberSummary.userId) + } else roomMemberSummary + } +} diff --git a/vector/src/main/java/im/vector/app/features/call/CallControlsBottomSheet.kt b/vector/src/main/java/im/vector/app/features/call/CallControlsBottomSheet.kt index f23b26883a..f9e2338077 100644 --- a/vector/src/main/java/im/vector/app/features/call/CallControlsBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/call/CallControlsBottomSheet.kt @@ -23,13 +23,9 @@ import android.view.ViewGroup import androidx.core.content.ContextCompat import androidx.core.view.isVisible import com.airbnb.mvrx.activityViewModel -import com.google.android.material.dialog.MaterialAlertDialogBuilder import im.vector.app.R import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment import im.vector.app.databinding.BottomSheetCallControlsBinding -import im.vector.app.features.call.audio.CallAudioManager - -import me.gujun.android.span.span class CallControlsBottomSheet : VectorBaseBottomSheetDialogFragment() { override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): BottomSheetCallControlsBinding { @@ -45,10 +41,6 @@ class CallControlsBottomSheet : VectorBaseBottomSheetDialogFragment { - showSoundDeviceChooser(it.available, it.current) - } - else -> { - } - } - } - } - - private fun showSoundDeviceChooser(available: Set, current: CallAudioManager.Device) { - val soundDevices = available.map { - when (it) { - CallAudioManager.Device.WIRELESS_HEADSET -> span { - text = getString(R.string.sound_device_wireless_headset) - textStyle = if (current == it) "bold" else "normal" - } - CallAudioManager.Device.PHONE -> span { - text = getString(R.string.sound_device_phone) - textStyle = if (current == it) "bold" else "normal" - } - CallAudioManager.Device.SPEAKER -> span { - text = getString(R.string.sound_device_speaker) - textStyle = if (current == it) "bold" else "normal" - } - CallAudioManager.Device.HEADSET -> span { - text = getString(R.string.sound_device_headset) - textStyle = if (current == it) "bold" else "normal" - } - } - } - MaterialAlertDialogBuilder(requireContext()) - .setItems(soundDevices.toTypedArray()) { d, n -> - d.cancel() - when (soundDevices[n].toString()) { - // TODO Make an adapter and handle multiple Bluetooth headsets. Also do not use translations. - getString(R.string.sound_device_phone) -> { - callViewModel.handle(VectorCallViewActions.ChangeAudioDevice(CallAudioManager.Device.PHONE)) - } - getString(R.string.sound_device_speaker) -> { - callViewModel.handle(VectorCallViewActions.ChangeAudioDevice(CallAudioManager.Device.SPEAKER)) - } - getString(R.string.sound_device_headset) -> { - callViewModel.handle(VectorCallViewActions.ChangeAudioDevice(CallAudioManager.Device.HEADSET)) - } - getString(R.string.sound_device_wireless_headset) -> { - callViewModel.handle(VectorCallViewActions.ChangeAudioDevice(CallAudioManager.Device.WIRELESS_HEADSET)) - } - } - } - .setNegativeButton(R.string.cancel, null) - .show() } private fun renderState(state: VectorCallViewState) { - views.callControlsSoundDevice.title = getString(R.string.call_select_sound_device) - views.callControlsSoundDevice.subTitle = when (state.device) { - CallAudioManager.Device.PHONE -> getString(R.string.sound_device_phone) - CallAudioManager.Device.SPEAKER -> getString(R.string.sound_device_speaker) - CallAudioManager.Device.HEADSET -> getString(R.string.sound_device_headset) - CallAudioManager.Device.WIRELESS_HEADSET -> getString(R.string.sound_device_wireless_headset) - } - views.callControlsSwitchCamera.isVisible = state.isVideoCall && state.canSwitchCamera views.callControlsSwitchCamera.subTitle = getString(if (state.isFrontCamera) R.string.call_camera_front else R.string.call_camera_back) - if (state.isVideoCall) { views.callControlsToggleSDHD.isVisible = true if (state.isHD) { diff --git a/vector/src/main/java/im/vector/app/features/call/CallControlsView.kt b/vector/src/main/java/im/vector/app/features/call/CallControlsView.kt index 3742de6271..f0f75370e3 100644 --- a/vector/src/main/java/im/vector/app/features/call/CallControlsView.kt +++ b/vector/src/main/java/im/vector/app/features/call/CallControlsView.kt @@ -36,16 +36,19 @@ class CallControlsView @JvmOverloads constructor( init { inflate(context, R.layout.view_call_controls, this) views = ViewCallControlsBinding.bind(this) - + views.audioSettingsIcon.setOnClickListener { didTapAudioSettings() } views.ringingControlAccept.setOnClickListener { acceptIncomingCall() } views.ringingControlDecline.setOnClickListener { declineIncomingCall() } views.endCallIcon.setOnClickListener { endOngoingCall() } views.muteIcon.setOnClickListener { toggleMute() } views.videoToggleIcon.setOnClickListener { toggleVideo() } - views.openChatIcon.setOnClickListener { returnToChat() } views.moreIcon.setOnClickListener { moreControlOption() } } + private fun didTapAudioSettings() { + interactionListener?.didTapAudioSettings() + } + private fun acceptIncomingCall() { interactionListener?.didAcceptIncomingCall() } @@ -66,10 +69,6 @@ class CallControlsView @JvmOverloads constructor( interactionListener?.didTapToggleVideo() } - private fun returnToChat() { - interactionListener?.returnToChat() - } - private fun moreControlOption() { interactionListener?.didTapMore() } @@ -77,49 +76,36 @@ class CallControlsView @JvmOverloads constructor( fun updateForState(state: VectorCallViewState) { val callState = state.callState.invoke() if (state.isAudioMuted) { - views.muteIcon.setImageResource(R.drawable.ic_microphone_off) + views.muteIcon.setImageResource(R.drawable.ic_mic_off) views.muteIcon.contentDescription = resources.getString(R.string.a11y_unmute_microphone) } else { - views.muteIcon.setImageResource(R.drawable.ic_microphone_on) + views.muteIcon.setImageResource(R.drawable.ic_mic_on) views.muteIcon.contentDescription = resources.getString(R.string.a11y_mute_microphone) } if (state.isVideoEnabled) { views.videoToggleIcon.setImageResource(R.drawable.ic_video) views.videoToggleIcon.contentDescription = resources.getString(R.string.a11y_stop_camera) } else { - views.videoToggleIcon.setImageResource(R.drawable.ic_video_off) + views.videoToggleIcon.setImageResource(R.drawable.ic_video_off) views.videoToggleIcon.contentDescription = resources.getString(R.string.a11y_start_camera) } when (callState) { - is CallState.Idle, - is CallState.Dialing, - is CallState.Answering -> { - views.ringingControls.isVisible = true - views.ringingControlAccept.isVisible = false - views.ringingControlDecline.isVisible = true - views.connectedControls.isVisible = false - } is CallState.LocalRinging -> { views.ringingControls.isVisible = true views.ringingControlAccept.isVisible = true views.ringingControlDecline.isVisible = true views.connectedControls.isVisible = false } - is CallState.Connected -> { - if (callState.iceConnectionState == MxPeerConnectionState.CONNECTED) { - views.ringingControls.isVisible = false - views.connectedControls.isVisible = true - views.videoToggleIcon.isVisible = state.isVideoCall - } else { - views.ringingControls.isVisible = true - views.ringingControlAccept.isVisible = false - views.ringingControlDecline.isVisible = true - views.connectedControls.isVisible = false - } + is CallState.Connected, + is CallState.Dialing, + is CallState.Answering -> { + views.ringingControls.isVisible = false + views.connectedControls.isVisible = true + views.videoToggleIcon.isVisible = state.isVideoCall + views.moreIcon.isVisible = callState is CallState.Connected && callState.iceConnectionState == MxPeerConnectionState.CONNECTED } - is CallState.Ended, - null -> { + else -> { views.ringingControls.isVisible = false views.connectedControls.isVisible = false } @@ -127,12 +113,12 @@ class CallControlsView @JvmOverloads constructor( } interface InteractionListener { + fun didTapAudioSettings() fun didAcceptIncomingCall() fun didDeclineIncomingCall() fun didEndCall() fun didTapToggleMute() fun didTapToggleVideo() - fun returnToChat() fun didTapMore() } } diff --git a/vector/src/main/java/im/vector/app/features/call/CallSoundDeviceChooserBottomSheet.kt b/vector/src/main/java/im/vector/app/features/call/CallSoundDeviceChooserBottomSheet.kt new file mode 100644 index 0000000000..649b7fee3e --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/call/CallSoundDeviceChooserBottomSheet.kt @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.call + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.airbnb.epoxy.SimpleEpoxyController +import com.airbnb.mvrx.activityViewModel +import im.vector.app.core.epoxy.bottomsheet.BottomSheetActionItem_ +import im.vector.app.core.extensions.configureWith +import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment +import im.vector.app.databinding.BottomSheetGenericListBinding +import im.vector.app.features.call.audio.CallAudioManager +import im.vector.app.features.home.room.list.actions.RoomListQuickActionsBottomSheet + +class CallSoundDeviceChooserBottomSheet : VectorBaseBottomSheetDialogFragment() { + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): BottomSheetGenericListBinding { + return BottomSheetGenericListBinding.inflate(inflater, container, false) + } + + private val callViewModel: VectorCallViewModel by activityViewModel() + private val controller = SimpleEpoxyController() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + views.bottomSheetRecyclerView.configureWith(controller, hasFixedSize = false) + callViewModel.observeViewEvents { + when (it) { + is VectorCallViewEvents.ShowSoundDeviceChooser -> { + render(it.available, it.current) + } + else -> { + } + } + } + callViewModel.handle(VectorCallViewActions.SwitchSoundDevice) + } + + private fun render(available: Set, current: CallAudioManager.Device) { + val models = available.map { device -> + val title = when (device) { + is CallAudioManager.Device.WirelessHeadset -> device.name ?: getString(device.titleRes) + else -> getString(device.titleRes) + } + BottomSheetActionItem_().apply { + id(device.titleRes) + text(title) + iconRes(device.drawableRes) + selected(current == device) + listener { + callViewModel.handle(VectorCallViewActions.ChangeAudioDevice(device)) + dismiss() + } + } + } + controller.setModels(models) + } + + companion object { + fun newInstance(): RoomListQuickActionsBottomSheet { + return RoomListQuickActionsBottomSheet() + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/call/SharedKnownCallsViewModel.kt b/vector/src/main/java/im/vector/app/features/call/SharedKnownCallsViewModel.kt index b33edd09e0..4b0ea412f3 100644 --- a/vector/src/main/java/im/vector/app/features/call/SharedKnownCallsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/call/SharedKnownCallsViewModel.kt @@ -32,18 +32,16 @@ class SharedKnownCallsViewModel @Inject constructor( val callListener = object : WebRtcCall.Listener { override fun onStateUpdate(call: MxCall) { - // post it-self - liveKnownCalls.postValue(liveKnownCalls.value) + liveKnownCalls.postValue(callManager.getCalls()) } override fun onHoldUnhold() { super.onHoldUnhold() - // post it-self - liveKnownCalls.postValue(liveKnownCalls.value) + liveKnownCalls.postValue(callManager.getCalls()) } } - private val currentCallListener = object : WebRtcCallManager.CurrentCallListener { + private val callManagerListener = object : WebRtcCallManager.Listener { override fun onCurrentCallChange(call: WebRtcCall?) { val knownCalls = callManager.getCalls() liveKnownCalls.postValue(knownCalls) @@ -52,12 +50,17 @@ class SharedKnownCallsViewModel @Inject constructor( it.addListener(callListener) } } + + override fun onCallEnded(callId: String) { + val knownCalls = callManager.getCalls() + liveKnownCalls.postValue(knownCalls) + } } init { val knownCalls = callManager.getCalls() liveKnownCalls.postValue(knownCalls) - callManager.addCurrentCallListener(currentCallListener) + callManager.addListener(callManagerListener) knownCalls.forEach { it.addListener(callListener) } @@ -67,7 +70,7 @@ class SharedKnownCallsViewModel @Inject constructor( callManager.getCalls().forEach { it.removeListener(callListener) } - callManager.removeCurrentCallListener(currentCallListener) + callManager.removeListener(callManagerListener) super.onCleared() } } diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt index a1e3717329..f71dcc0635 100644 --- a/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt @@ -17,15 +17,20 @@ package im.vector.app.features.call import android.app.KeyguardManager +import android.app.PictureInPictureParams import android.content.Context import android.content.Intent import android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP +import android.content.res.Configuration import android.graphics.Color import android.os.Build import android.os.Bundle import android.os.Parcelable +import android.util.Rational +import android.view.MenuItem import android.view.View import android.view.WindowManager +import androidx.annotation.StringRes import androidx.core.content.ContextCompat import androidx.core.content.getSystemService import androidx.core.view.isInvisible @@ -34,9 +39,11 @@ import com.airbnb.mvrx.Fail import com.airbnb.mvrx.MvRx import com.airbnb.mvrx.viewModel import com.airbnb.mvrx.withState +import com.google.android.material.card.MaterialCardView import com.google.android.material.dialog.MaterialAlertDialogBuilder import im.vector.app.R import im.vector.app.core.di.ScreenComponent +import im.vector.app.core.extensions.setTextOrHide import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.utils.PERMISSIONS_FOR_AUDIO_IP_CALL import im.vector.app.core.utils.PERMISSIONS_FOR_VIDEO_IP_CALL @@ -51,6 +58,8 @@ import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.detail.RoomDetailActivity import im.vector.app.features.home.room.detail.RoomDetailArgs +import io.github.hyuwah.draggableviewlib.DraggableView +import io.github.hyuwah.draggableviewlib.setupDraggable import io.reactivex.android.schedulers.AndroidSchedulers import kotlinx.parcelize.Parcelize import org.matrix.android.sdk.api.extensions.orFalse @@ -58,6 +67,7 @@ import org.matrix.android.sdk.api.logger.LoggerTag import org.matrix.android.sdk.api.session.call.CallState import org.matrix.android.sdk.api.session.call.MxPeerConnectionState import org.matrix.android.sdk.api.session.call.TurnServerResponse +import org.matrix.android.sdk.api.session.room.model.call.EndCallReason import org.webrtc.EglBase import org.webrtc.RendererCommon import timber.log.Timber @@ -85,7 +95,6 @@ class VectorCallActivity : VectorBaseActivity(), CallContro } private val callViewModel: VectorCallViewModel by viewModel() - private lateinit var callArgs: CallArgs @Inject lateinit var callManager: WebRtcCallManager @Inject lateinit var viewModelFactory: VectorCallViewModel.Factory @@ -97,6 +106,8 @@ class VectorCallActivity : VectorBaseActivity(), CallContro } private var rootEglBase: EglBase? = null + private var pipDraggrableView: DraggableView? = null + private var otherCallDraggableView: DraggableView? = null var surfaceRenderersAreInitialized = false @@ -113,13 +124,6 @@ class VectorCallActivity : VectorBaseActivity(), CallContro window.navigationBarColor = Color.BLACK super.onCreate(savedInstanceState) - if (intent.hasExtra(MvRx.KEY_ARG)) { - callArgs = intent.getParcelableExtra(MvRx.KEY_ARG)!! - } else { - Timber.tag(loggerTag.value).e("missing callArgs for VectorCall Activity") - finish() - } - Timber.tag(loggerTag.value).v("EXTRA_MODE is ${intent.getStringExtra(EXTRA_MODE)}") if (intent.getStringExtra(EXTRA_MODE) == INCOMING_RINGING) { turnScreenOnAndKeyguardOff() @@ -127,12 +131,19 @@ class VectorCallActivity : VectorBaseActivity(), CallContro if (savedInstanceState != null) { (supportFragmentManager.findFragmentByTag(FRAGMENT_DIAL_PAD_TAG) as? CallDialPadBottomSheet)?.callback = dialPadCallback } + setSupportActionBar(views.callToolbar) configureCallViews() callViewModel.subscribe(this) { renderState(it) } + callViewModel.asyncSubscribe(this, VectorCallViewState::callState) { + if (it is CallState.Ended) { + handleCallEnded(it) + } + } + callViewModel.viewEvents .observe() .observeOn(AndroidSchedulers.mainThread()) @@ -141,25 +152,89 @@ class VectorCallActivity : VectorBaseActivity(), CallContro } .disposeOnDestroy() - if (callArgs.isVideoCall) { - if (checkPermissions(PERMISSIONS_FOR_VIDEO_IP_CALL, this, permissionCameraLauncher, R.string.permissions_rationale_msg_camera_and_audio)) { - start() - } - } else { - if (checkPermissions(PERMISSIONS_FOR_AUDIO_IP_CALL, this, permissionCameraLauncher, R.string.permissions_rationale_msg_record_audio)) { - start() + callViewModel.selectSubscribe(this, VectorCallViewState::callId, VectorCallViewState::isVideoCall) { _, isVideoCall -> + if (isVideoCall) { + if (checkPermissions(PERMISSIONS_FOR_VIDEO_IP_CALL, this, permissionCameraLauncher, R.string.permissions_rationale_msg_camera_and_audio)) { + setupRenderersIfNeeded() + } + } else { + if (checkPermissions(PERMISSIONS_FOR_AUDIO_IP_CALL, this, permissionCameraLauncher, R.string.permissions_rationale_msg_record_audio)) { + setupRenderersIfNeeded() + } } } } + override fun onNewIntent(intent: Intent?) { + super.onNewIntent(intent) + intent?.takeIf { it.hasExtra(MvRx.KEY_ARG) } + ?.let { intent.getParcelableExtra(MvRx.KEY_ARG) } + ?.let { + callViewModel.handle(VectorCallViewActions.SwitchCall(it)) + } + } + + override fun getMenuRes() = R.menu.vector_call + + override fun onUserLeaveHint() { + enterPictureInPictureIfRequired() + } + + override fun onBackPressed() { + if (!enterPictureInPictureIfRequired()) { + super.onBackPressed() + } + } + + private fun enterPictureInPictureIfRequired(): Boolean = withState(callViewModel) { + if (!it.isVideoCall) { + false + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val aspectRatio = Rational(resources.getDimensionPixelSize(R.dimen.call_pip_width), resources.getDimensionPixelSize(R.dimen.call_pip_height)) + val params = PictureInPictureParams.Builder() + .setAspectRatio(aspectRatio) + .build() + renderPiPMode(it) + enterPictureInPictureMode(params) + } else { + false + } + } + + private fun isInPictureInPictureModeSafe(): Boolean { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && isInPictureInPictureMode + } + + override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, newConfig: Configuration) = withState(callViewModel) { + renderState(it) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + if (item.itemId == R.id.menu_call_open_chat) { + returnToChat() + return true + } else if (item.itemId == android.R.id.home) { + // We check here as we want PiP in some cases + onBackPressed() + return true + } + return super.onOptionsItemSelected(item) + } + override fun onDestroy() { - callManager.getCallById(callArgs.callId)?.detachRenderers(listOf(views.pipRenderer, views.fullscreenRenderer)) + detachRenderersIfNeeded() + turnScreenOffAndKeyguardOn() + super.onDestroy() + } + + private fun detachRenderersIfNeeded() { + val callId = withState(callViewModel) { it.callId } + callManager.getCallById(callId)?.detachRenderers(listOf(views.pipRenderer, views.fullscreenRenderer)) if (surfaceRenderersAreInitialized) { views.pipRenderer.release() views.fullscreenRenderer.release() + surfaceRenderersAreInitialized = false } - turnScreenOffAndKeyguardOn() - super.onDestroy() } private fun renderState(state: VectorCallViewState) { @@ -168,53 +243,57 @@ class VectorCallActivity : VectorBaseActivity(), CallContro finish() return } + if (isInPictureInPictureModeSafe()) { + renderPiPMode(state) + } else { + renderFullScreenMode(state) + } + } + private fun renderFullScreenMode(state: VectorCallViewState) { + views.callToolbar.isVisible = true + views.callControlsView.isVisible = true views.callControlsView.updateForState(state) val callState = state.callState.invoke() - views.callConnectingProgress.isVisible = false views.callActionText.setOnClickListener(null) views.callActionText.isVisible = false views.smallIsHeldIcon.isVisible = false when (callState) { is CallState.Idle, is CallState.CreateOffer, - is CallState.Dialing -> { - views.callVideoGroup.isInvisible = true + is CallState.LocalRinging, + is CallState.Dialing -> { + views.fullscreenRenderer.isVisible = false + views.pipRendererWrapper.isVisible = false views.callInfoGroup.isVisible = true - views.callStatusText.setText(R.string.call_ring) + views.callToolbar.setSubtitle(R.string.call_ringing) configureCallInfo(state) } - - is CallState.LocalRinging -> { - views.callVideoGroup.isInvisible = true + is CallState.Answering -> { + views.fullscreenRenderer.isVisible = false + views.pipRendererWrapper.isVisible = false views.callInfoGroup.isVisible = true - views.callStatusText.text = null - configureCallInfo(state) - } - - is CallState.Answering -> { - views.callVideoGroup.isInvisible = true - views.callInfoGroup.isVisible = true - views.callStatusText.setText(R.string.call_connecting) - views.callConnectingProgress.isVisible = true + views.callToolbar.setSubtitle(R.string.call_connecting) configureCallInfo(state) } is CallState.Connected -> { + views.callToolbar.subtitle = state.formattedDuration if (callState.iceConnectionState == MxPeerConnectionState.CONNECTED) { if (state.isLocalOnHold || state.isRemoteOnHold) { views.smallIsHeldIcon.isVisible = true - views.callVideoGroup.isInvisible = true + views.fullscreenRenderer.isVisible = false + views.pipRendererWrapper.isVisible = false views.callInfoGroup.isVisible = true configureCallInfo(state, blurAvatar = true) if (state.isRemoteOnHold) { views.callActionText.setText(R.string.call_resume_action) views.callActionText.isVisible = true views.callActionText.setOnClickListener { callViewModel.handle(VectorCallViewActions.ToggleHoldResume) } - views.callStatusText.setText(R.string.call_held_by_you) + views.callToolbar.setSubtitle(R.string.call_held_by_you) } else { views.callActionText.isInvisible = true state.callInfo?.opponentUserItem?.let { - views.callStatusText.text = getString(R.string.call_held_by_user, it.getBestName()) + views.callToolbar.subtitle = getString(R.string.call_held_by_user, it.getBestName()) } } } else if (state.transferee !is VectorCallViewState.TransfereeState.NoTransferee) { @@ -226,45 +305,127 @@ class VectorCallActivity : VectorBaseActivity(), CallContro views.callActionText.text = getString(R.string.call_transfer_transfer_to_title, transfereeName) views.callActionText.isVisible = true views.callActionText.setOnClickListener { callViewModel.handle(VectorCallViewActions.TransferCall) } - views.callStatusText.text = state.formattedDuration configureCallInfo(state) } else { - views.callStatusText.text = state.formattedDuration configureCallInfo(state) - if (callArgs.isVideoCall) { - views.callVideoGroup.isVisible = true + if (state.isVideoCall) { + views.fullscreenRenderer.isVisible = true + views.pipRendererWrapper.isVisible = true views.callInfoGroup.isVisible = false views.pipRenderer.isVisible = !state.isVideoCaptureInError && state.otherKnownCallInfo == null } else { - views.callVideoGroup.isInvisible = true + views.fullscreenRenderer.isVisible = false + views.pipRendererWrapper.isVisible = false views.callInfoGroup.isVisible = true } } } else { // This state is not final, if you change network, new candidates will be sent - views.callVideoGroup.isInvisible = true + views.fullscreenRenderer.isVisible = false + views.pipRendererWrapper.isVisible = false views.callInfoGroup.isVisible = true configureCallInfo(state) - views.callStatusText.setText(R.string.call_connecting) - views.callConnectingProgress.isVisible = true + views.callToolbar.setSubtitle(R.string.call_connecting) } } is CallState.Ended -> { - finish() + views.fullscreenRenderer.isVisible = false + views.pipRendererWrapper.isVisible = false + views.callInfoGroup.isVisible = true + views.callToolbar.setSubtitle(R.string.call_ended) + configureCallInfo(state) } - null -> { + else -> { + views.fullscreenRenderer.isVisible = false + views.pipRendererWrapper.isVisible = false + views.callInfoGroup.isInvisible = true } } } + private fun renderPiPMode(state: VectorCallViewState) { + val callState = state.callState.invoke() + views.callToolbar.isVisible = false + views.callControlsView.isVisible = false + views.pipRendererWrapper.isVisible = false + views.pipRenderer.isVisible = false + views.callActionText.isVisible = false + when (callState) { + is CallState.Idle, + is CallState.CreateOffer, + is CallState.LocalRinging, + is CallState.Dialing, + is CallState.Answering -> { + views.fullscreenRenderer.isVisible = false + views.callInfoGroup.isVisible = false + } + is CallState.Connected -> { + if (callState.iceConnectionState == MxPeerConnectionState.CONNECTED) { + if (state.isLocalOnHold || state.isRemoteOnHold) { + views.smallIsHeldIcon.isVisible = true + views.fullscreenRenderer.isVisible = false + views.callInfoGroup.isVisible = true + configureCallInfo(state, blurAvatar = true) + } else { + configureCallInfo(state) + views.fullscreenRenderer.isVisible = true + views.callInfoGroup.isVisible = false + } + } else { + views.callInfoGroup.isVisible = false + } + } + else -> { + views.fullscreenRenderer.isVisible = false + views.callInfoGroup.isVisible = false + } + } + } + + private fun handleCallEnded(callState: CallState.Ended) { + if (isInPictureInPictureModeSafe()) { + val startIntent = Intent(this, VectorCallActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_REORDER_TO_FRONT + } + startActivity(startIntent) + } + when (callState.reason) { + EndCallReason.USER_BUSY -> { + showEndCallDialog(R.string.call_ended_user_busy_title, R.string.call_ended_user_busy_description) + } + EndCallReason.INVITE_TIMEOUT -> { + showEndCallDialog(R.string.call_ended_invite_timeout_title, R.string.call_error_user_not_responding) + } + else -> { + finish() + } + } + } + + private fun showEndCallDialog(@StringRes title: Int, @StringRes description: Int) { + MaterialAlertDialogBuilder(this) + .setTitle(title) + .setMessage(description) + .setNegativeButton(R.string.ok, null) + .setOnDismissListener { + finish() + } + .show() + } + private fun configureCallInfo(state: VectorCallViewState, blurAvatar: Boolean = false) { state.callInfo?.opponentUserItem?.let { val colorFilter = ContextCompat.getColor(this, R.color.bg_call_screen_blur) avatarRenderer.renderBlur(it, views.bgCallView, sampling = 20, rounded = false, colorFilter = colorFilter, addPlaceholder = false) if (state.transferee is VectorCallViewState.TransfereeState.NoTransferee) { - views.participantNameText.text = it.getBestName() + views.participantNameText.setTextOrHide(null) + views.callToolbar.title = if (state.isVideoCall) { + getString(R.string.video_call_with_participant, it.getBestName()) + } else { + getString(R.string.audio_call_with_participant, it.getBestName()) + } } else { - views.participantNameText.text = getString(R.string.call_transfer_consulting_with, it.getBestName()) + views.participantNameText.setTextOrHide(getString(R.string.call_transfer_consulting_with, it.getBestName())) } if (blurAvatar) { avatarRenderer.renderBlur(it, views.otherMemberAvatar, sampling = 2, rounded = true, colorFilter = colorFilter, addPlaceholder = true) @@ -272,7 +433,7 @@ class VectorCallActivity : VectorBaseActivity(), CallContro avatarRenderer.render(it, views.otherMemberAvatar) } } - if (state.otherKnownCallInfo?.opponentUserItem == null) { + if (state.otherKnownCallInfo?.opponentUserItem == null || isInPictureInPictureModeSafe()) { views.otherKnownCallLayout.isVisible = false } else { val otherCall = callManager.getCallById(state.otherKnownCallInfo.callId) @@ -286,7 +447,7 @@ class VectorCallActivity : VectorBaseActivity(), CallContro addPlaceholder = true ) views.otherKnownCallLayout.isVisible = true - views.otherSmallIsHeldIcon.isVisible = otherCall?.let { it.isLocalOnHold || it.remoteOnHold }.orFalse() + views.otherSmallIsHeldIcon.isVisible = otherCall?.let { it.isLocalOnHold || it.isRemoteOnHold }.orFalse() } } @@ -295,44 +456,60 @@ class VectorCallActivity : VectorBaseActivity(), CallContro views.otherKnownCallLayout.setOnClickListener { withState(callViewModel) { val otherCall = callManager.getCallById(it.otherKnownCallInfo?.callId ?: "") ?: return@withState - startActivity(newIntent(this, otherCall, null)) - finish() + val callArgs = CallArgs( + signalingRoomId = otherCall.nativeRoomId, + callId = otherCall.callId, + participantUserId = otherCall.mxCall.opponentUserId, + isIncomingCall = !otherCall.mxCall.isOutgoing, + isVideoCall = otherCall.mxCall.isVideoCall + ) + callViewModel.handle(VectorCallViewActions.SwitchCall(callArgs)) } } + views.pipRendererWrapper.setOnClickListener { + callViewModel.handle(VectorCallViewActions.ToggleCamera) + } + pipDraggrableView = views.pipRendererWrapper.setupDraggable() + .setStickyMode(DraggableView.Mode.STICKY_XY) + .build() + + otherCallDraggableView = views.otherKnownCallLayout.setupDraggable() + .setStickyMode(DraggableView.Mode.STICKY_XY) + .build() } private val permissionCameraLauncher = registerForPermissionsResult { allGranted, _ -> if (allGranted) { - start() + setupRenderersIfNeeded() } else { // TODO display something finish() } } - private fun start() { + private fun setupRenderersIfNeeded() { + detachRenderersIfNeeded() rootEglBase = EglUtils.rootEglBase ?: return Unit.also { Timber.tag(loggerTag.value).v("rootEglBase is null") finish() } // Init Picture in Picture renderer - views.pipRenderer.init(rootEglBase!!.eglBaseContext, null) - views.pipRenderer.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT) - + views.pipRenderer.apply { + init(rootEglBase!!.eglBaseContext, null) + setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_BALANCED) + setEnableHardwareScaler(true) + setZOrderMediaOverlay(true) + } // Init Full Screen renderer views.fullscreenRenderer.init(rootEglBase!!.eglBaseContext, null) views.fullscreenRenderer.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FILL) - - views.pipRenderer.setZOrderMediaOverlay(true) - views.pipRenderer.setEnableHardwareScaler(true /* enabled */) views.fullscreenRenderer.setEnableHardwareScaler(true /* enabled */) - callManager.getCallById(callArgs.callId)?.attachViewRenderers(views.pipRenderer, views.fullscreenRenderer, - intent.getStringExtra(EXTRA_MODE)?.takeIf { isFirstCreation() }) - - views.pipRenderer.setOnClickListener { - callViewModel.handle(VectorCallViewActions.ToggleCamera) + val callId = withState(callViewModel) { it.callId } + callManager.getCallById(callId)?.also { webRtcCall -> + webRtcCall.attachViewRenderers(views.pipRenderer, views.fullscreenRenderer, intent.getStringExtra(EXTRA_MODE)) + intent.removeExtra(EXTRA_MODE) } surfaceRenderersAreInitialized = true } @@ -340,9 +517,6 @@ class VectorCallActivity : VectorBaseActivity(), CallContro private fun handleViewEvents(event: VectorCallViewEvents?) { Timber.tag(loggerTag.value).v("handleViewEvents $event") when (event) { - VectorCallViewEvents.DismissNoCall -> { - finish() - } is VectorCallViewEvents.ConnectionTimeout -> { onErrorTimoutConnect(event.turn) } @@ -352,7 +526,8 @@ class VectorCallActivity : VectorBaseActivity(), CallContro }.show(supportFragmentManager, FRAGMENT_DIAL_PAD_TAG) } is VectorCallViewEvents.ShowCallTransferScreen -> { - navigator.openCallTransfer(this, callArgs.callId) + val callId = withState(callViewModel) { it.callId } + navigator.openCallTransfer(this, callId) } null -> { } @@ -364,44 +539,15 @@ class VectorCallActivity : VectorBaseActivity(), CallContro // TODO ask to use default stun, etc... MaterialAlertDialogBuilder(this) .setTitle(R.string.call_failed_no_connection) - .setMessage(getString(R.string.call_failed_no_connection_description)) + .setMessage(R.string.call_failed_no_connection_description) .setNegativeButton(R.string.ok) { _, _ -> callViewModel.handle(VectorCallViewActions.EndCall) } .show() } - companion object { - private const val EXTRA_MODE = "EXTRA_MODE" - private const val FRAGMENT_DIAL_PAD_TAG = "FRAGMENT_DIAL_PAD_TAG" - - const val OUTGOING_CREATED = "OUTGOING_CREATED" - const val INCOMING_RINGING = "INCOMING_RINGING" - const val INCOMING_ACCEPT = "INCOMING_ACCEPT" - - fun newIntent(context: Context, call: WebRtcCall, mode: String?): Intent { - return Intent(context, VectorCallActivity::class.java).apply { - // what could be the best flags? - flags = Intent.FLAG_ACTIVITY_NEW_TASK - putExtra(MvRx.KEY_ARG, CallArgs(call.nativeRoomId, call.callId, call.mxCall.opponentUserId, !call.mxCall.isOutgoing, call.mxCall.isVideoCall)) - putExtra(EXTRA_MODE, mode) - } - } - - fun newIntent(context: Context, - callId: String, - signalingRoomId: String, - otherUserId: String, - isIncomingCall: Boolean, - isVideoCall: Boolean, - mode: String?): Intent { - return Intent(context, VectorCallActivity::class.java).apply { - // what could be the best flags? - flags = FLAG_ACTIVITY_CLEAR_TOP - putExtra(MvRx.KEY_ARG, CallArgs(signalingRoomId, callId, otherUserId, isIncomingCall, isVideoCall)) - putExtra(EXTRA_MODE, mode) - } - } + override fun didTapAudioSettings() { + CallSoundDeviceChooserBottomSheet().show(supportFragmentManager, "SoundDeviceChooser") } override fun didAcceptIncomingCall() { @@ -424,8 +570,9 @@ class VectorCallActivity : VectorBaseActivity(), CallContro callViewModel.handle(VectorCallViewActions.ToggleVideo) } - override fun returnToChat() { - val args = RoomDetailArgs(callArgs.signalingRoomId) + private fun returnToChat() { + val roomId = withState(callViewModel) { it.roomId } + val args = RoomDetailArgs(roomId) val intent = RoomDetailActivity.newIntent(this, args).apply { flags = FLAG_ACTIVITY_CLEAR_TOP } @@ -473,4 +620,37 @@ class VectorCallActivity : VectorBaseActivity(), CallContro ) } } + + companion object { + private const val EXTRA_MODE = "EXTRA_MODE" + private const val FRAGMENT_DIAL_PAD_TAG = "FRAGMENT_DIAL_PAD_TAG" + + const val OUTGOING_CREATED = "OUTGOING_CREATED" + const val INCOMING_RINGING = "INCOMING_RINGING" + const val INCOMING_ACCEPT = "INCOMING_ACCEPT" + + fun newIntent(context: Context, call: WebRtcCall, mode: String?): Intent { + return Intent(context, VectorCallActivity::class.java).apply { + // what could be the best flags? + flags = Intent.FLAG_ACTIVITY_NEW_TASK + putExtra(MvRx.KEY_ARG, CallArgs(call.nativeRoomId, call.callId, call.mxCall.opponentUserId, !call.mxCall.isOutgoing, call.mxCall.isVideoCall)) + putExtra(EXTRA_MODE, mode) + } + } + + fun newIntent(context: Context, + callId: String, + signalingRoomId: String, + otherUserId: String, + isIncomingCall: Boolean, + isVideoCall: Boolean, + mode: String?): Intent { + return Intent(context, VectorCallActivity::class.java).apply { + // what could be the best flags? + flags = Intent.FLAG_ACTIVITY_NEW_TASK + putExtra(MvRx.KEY_ARG, CallArgs(signalingRoomId, callId, otherUserId, isIncomingCall, isVideoCall)) + putExtra(EXTRA_MODE, mode) + } + } + } } diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallViewActions.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallViewActions.kt index a332153aaa..1834c05e41 100644 --- a/vector/src/main/java/im/vector/app/features/call/VectorCallViewActions.kt +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallViewActions.kt @@ -29,6 +29,8 @@ sealed class VectorCallViewActions : VectorViewModelAction { data class ChangeAudioDevice(val device: CallAudioManager.Device) : VectorCallViewActions() object OpenDialPad: VectorCallViewActions() data class SendDtmfDigit(val digit: String) : VectorCallViewActions() + data class SwitchCall(val callArgs: CallArgs) : VectorCallViewActions() + object SwitchSoundDevice : VectorCallViewActions() object HeadSetButtonPressed : VectorCallViewActions() object ToggleCamera : VectorCallViewActions() diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallViewEvents.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallViewEvents.kt index 91c3154d0a..9f19429c00 100644 --- a/vector/src/main/java/im/vector/app/features/call/VectorCallViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallViewEvents.kt @@ -22,7 +22,6 @@ import org.matrix.android.sdk.api.session.call.TurnServerResponse sealed class VectorCallViewEvents : VectorViewEvents { - object DismissNoCall : VectorCallViewEvents() data class ConnectionTimeout(val turn: TurnServerResponse?) : VectorCallViewEvents() data class ShowSoundDeviceChooser( val available: Set, diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt index e7b2b629e1..90df595f8f 100644 --- a/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt @@ -60,7 +60,7 @@ class VectorCallViewModel @AssistedInject constructor( setState { copy( isLocalOnHold = call?.isLocalOnHold ?: false, - isRemoteOnHold = call?.remoteOnHold ?: false + isRemoteOnHold = call?.isRemoteOnHold ?: false ) } } @@ -134,19 +134,25 @@ class VectorCallViewModel @AssistedInject constructor( } ?: VectorCallViewState.TransfereeState.UnknownTransferee } - private val currentCallListener = object : WebRtcCallManager.CurrentCallListener { + private val callManagerListener = object : WebRtcCallManager.Listener { + + override fun onCallEnded(callId: String) { + withState { state -> + if (state.otherKnownCallInfo?.callId == callId) { + setState { copy(otherKnownCallInfo = null) } + } + } + } override fun onCurrentCallChange(call: WebRtcCall?) { - if (call == null) { - _viewEvents.post(VectorCallViewEvents.DismissNoCall) - } else { + if (call != null) { updateOtherKnownCall(call) } } override fun onAudioDevicesChange() { val currentSoundDevice = callManager.audioManager.selectedDevice ?: return - if (currentSoundDevice == CallAudioManager.Device.PHONE) { + if (currentSoundDevice == CallAudioManager.Device.Phone) { proximityManager.start() } else { proximityManager.stop() @@ -161,9 +167,7 @@ class VectorCallViewModel @AssistedInject constructor( } private fun updateOtherKnownCall(currentCall: WebRtcCall) { - val otherCall = callManager.getCalls().firstOrNull { - it.callId != currentCall.callId && it.mxCall.state is CallState.Connected - } + val otherCall = getOtherKnownCall(currentCall) setState { if (otherCall == null) { copy(otherKnownCallInfo = null) @@ -173,28 +177,41 @@ class VectorCallViewModel @AssistedInject constructor( } } + private fun getOtherKnownCall(currentCall: WebRtcCall): WebRtcCall? { + return callManager.getCalls().firstOrNull { + it.callId != currentCall.callId && it.mxCall.state is CallState.Connected + } + } + init { - val webRtcCall = callManager.getCallById(initialState.callId) + setupCallWithCurrentState() + } + + private fun setupCallWithCurrentState() = withState { state -> + call?.removeListener(callListener) + val webRtcCall = callManager.getCallById(state.callId) if (webRtcCall == null) { setState { copy(callState = Fail(IllegalArgumentException("No call"))) } } else { call = webRtcCall - callManager.addCurrentCallListener(currentCallListener) + callManager.addListener(callManagerListener) webRtcCall.addListener(callListener) val currentSoundDevice = callManager.audioManager.selectedDevice - if (currentSoundDevice == CallAudioManager.Device.PHONE) { + if (currentSoundDevice == CallAudioManager.Device.Phone) { proximityManager.start() } setState { copy( + isAudioMuted = webRtcCall.micMuted, + isVideoEnabled = !webRtcCall.videoMuted, isVideoCall = webRtcCall.mxCall.isVideoCall, callState = Success(webRtcCall.mxCall.state), callInfo = webRtcCall.extractCallInfo(), - device = currentSoundDevice ?: CallAudioManager.Device.PHONE, + device = currentSoundDevice ?: CallAudioManager.Device.Phone, isLocalOnHold = webRtcCall.isLocalOnHold, - isRemoteOnHold = webRtcCall.remoteOnHold, + isRemoteOnHold = webRtcCall.isRemoteOnHold, availableDevices = callManager.audioManager.availableDevices, isFrontCamera = webRtcCall.currentCameraType() == CameraType.FRONT, canSwitchCamera = webRtcCall.canSwitchCamera(), @@ -225,8 +242,9 @@ class VectorCallViewModel @AssistedInject constructor( } override fun onCleared() { - callManager.removeCurrentCallListener(currentCallListener) + callManager.removeListener(callManagerListener) call?.removeListener(callListener) + call = null proximityManager.stop() super.onCleared() } @@ -307,6 +325,10 @@ class VectorCallViewModel @AssistedInject constructor( VectorCallViewActions.TransferCall -> { handleCallTransfer() } + is VectorCallViewActions.SwitchCall -> { + setState { VectorCallViewState(action.callArgs) } + setupCallWithCurrentState() + } }.exhaustive } diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallViewState.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallViewState.kt index 3e7791cc08..a351806e1a 100644 --- a/vector/src/main/java/im/vector/app/features/call/VectorCallViewState.kt +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallViewState.kt @@ -35,7 +35,7 @@ data class VectorCallViewState( val isHD: Boolean = false, val isFrontCamera: Boolean = true, val canSwitchCamera: Boolean = true, - val device: CallAudioManager.Device = CallAudioManager.Device.PHONE, + val device: CallAudioManager.Device = CallAudioManager.Device.Phone, val availableDevices: Set = emptySet(), val callState: Async = Uninitialized, val otherKnownCallInfo: CallInfo? = null, diff --git a/vector/src/main/java/im/vector/app/features/call/audio/API21AudioDeviceDetector.kt b/vector/src/main/java/im/vector/app/features/call/audio/API21AudioDeviceDetector.kt index 4f54f703b4..eafd1eab20 100644 --- a/vector/src/main/java/im/vector/app/features/call/audio/API21AudioDeviceDetector.kt +++ b/vector/src/main/java/im/vector/app/features/call/audio/API21AudioDeviceDetector.kt @@ -50,13 +50,17 @@ internal class API21AudioDeviceDetector(private val context: Context, private fun getAvailableSoundDevices(): Set { return HashSet().apply { - if (isBluetoothHeadsetOn()) add(CallAudioManager.Device.WIRELESS_HEADSET) - if (isWiredHeadsetOn()) { - add(CallAudioManager.Device.HEADSET) - } else { - add(CallAudioManager.Device.PHONE) + if (isBluetoothHeadsetOn()) { + connectedBlueToothHeadset?.connectedDevices?.forEach { + add(CallAudioManager.Device.WirelessHeadset(it.name)) + } } - add(CallAudioManager.Device.SPEAKER) + if (isWiredHeadsetOn()) { + add(CallAudioManager.Device.Headset) + } else { + add(CallAudioManager.Device.Phone) + } + add(CallAudioManager.Device.Speaker) } } diff --git a/vector/src/main/java/im/vector/app/features/call/audio/API23AudioDeviceDetector.kt b/vector/src/main/java/im/vector/app/features/call/audio/API23AudioDeviceDetector.kt index 7174554d5f..fb17338fd1 100644 --- a/vector/src/main/java/im/vector/app/features/call/audio/API23AudioDeviceDetector.kt +++ b/vector/src/main/java/im/vector/app/features/call/audio/API23AudioDeviceDetector.kt @@ -33,10 +33,10 @@ internal class API23AudioDeviceDetector(private val audioManager: AudioManager, val deviceInfos = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS) for (info in deviceInfos) { when (info.type) { - AudioDeviceInfo.TYPE_BLUETOOTH_SCO -> devices.add(CallAudioManager.Device.WIRELESS_HEADSET) - AudioDeviceInfo.TYPE_BUILTIN_EARPIECE -> devices.add(CallAudioManager.Device.PHONE) - AudioDeviceInfo.TYPE_BUILTIN_SPEAKER -> devices.add(CallAudioManager.Device.SPEAKER) - AudioDeviceInfo.TYPE_WIRED_HEADPHONES, AudioDeviceInfo.TYPE_WIRED_HEADSET, TYPE_USB_HEADSET -> devices.add(CallAudioManager.Device.HEADSET) + AudioDeviceInfo.TYPE_BLUETOOTH_SCO -> devices.add(CallAudioManager.Device.WirelessHeadset(info.productName.toString())) + AudioDeviceInfo.TYPE_BUILTIN_EARPIECE -> devices.add(CallAudioManager.Device.Phone) + AudioDeviceInfo.TYPE_BUILTIN_SPEAKER -> devices.add(CallAudioManager.Device.Speaker) + AudioDeviceInfo.TYPE_WIRED_HEADPHONES, AudioDeviceInfo.TYPE_WIRED_HEADSET, TYPE_USB_HEADSET -> devices.add(CallAudioManager.Device.Headset) } } callAudioManager.replaceDevices(devices) diff --git a/vector/src/main/java/im/vector/app/features/call/audio/CallAudioManager.kt b/vector/src/main/java/im/vector/app/features/call/audio/CallAudioManager.kt index 36a11b5923..f4f56f9844 100644 --- a/vector/src/main/java/im/vector/app/features/call/audio/CallAudioManager.kt +++ b/vector/src/main/java/im/vector/app/features/call/audio/CallAudioManager.kt @@ -19,7 +19,10 @@ package im.vector.app.features.call.audio import android.content.Context import android.media.AudioManager import android.os.Build +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes import androidx.core.content.getSystemService +import im.vector.app.R import org.matrix.android.sdk.api.extensions.orFalse import timber.log.Timber import java.util.HashSet @@ -31,11 +34,11 @@ class CallAudioManager(private val context: Context, val configChange: (() -> Un private var audioDeviceDetector: AudioDeviceDetector? = null private var audioDeviceRouter: AudioDeviceRouter? = null - enum class Device { - PHONE, - SPEAKER, - HEADSET, - WIRELESS_HEADSET + sealed class Device(@StringRes val titleRes: Int, @DrawableRes val drawableRes: Int) { + object Phone : Device(R.string.sound_device_phone, R.drawable.ic_sound_device_phone) + object Speaker : Device(R.string.sound_device_speaker, R.drawable.ic_sound_device_speaker) + object Headset : Device(R.string.sound_device_headset, R.drawable.ic_sound_device_headphone) + data class WirelessHeadset(val name: String?) : Device(R.string.sound_device_wireless_headset, R.drawable.ic_sound_device_wireless) } enum class Mode { @@ -133,19 +136,19 @@ class CallAudioManager(private val context: Context, val configChange: (() -> Un userSelectedDevice = null return true } - val bluetoothAvailable = _availableDevices.contains(Device.WIRELESS_HEADSET) - val headsetAvailable = _availableDevices.contains(Device.HEADSET) + val availableBluetoothDevice = _availableDevices.firstOrNull { it is Device.WirelessHeadset } + val headsetAvailable = _availableDevices.contains(Device.Headset) // Pick the desired device based on what's available and the mode. var audioDevice: Device - audioDevice = if (bluetoothAvailable) { - Device.WIRELESS_HEADSET + audioDevice = if (availableBluetoothDevice != null) { + availableBluetoothDevice } else if (headsetAvailable) { - Device.HEADSET + Device.Headset } else if (mode == Mode.VIDEO_CALL) { - Device.SPEAKER + Device.Speaker } else { - Device.PHONE + Device.Phone } // Consider the user's selection if (userSelectedDevice != null && _availableDevices.contains(userSelectedDevice)) { diff --git a/vector/src/main/java/im/vector/app/features/call/audio/DefaultAudioDeviceRouter.kt b/vector/src/main/java/im/vector/app/features/call/audio/DefaultAudioDeviceRouter.kt index c252cc9f89..fd85ce075f 100644 --- a/vector/src/main/java/im/vector/app/features/call/audio/DefaultAudioDeviceRouter.kt +++ b/vector/src/main/java/im/vector/app/features/call/audio/DefaultAudioDeviceRouter.kt @@ -31,8 +31,8 @@ class DefaultAudioDeviceRouter(private val audioManager: AudioManager, private var focusRequestCompat: AudioFocusRequestCompat? = null override fun setAudioRoute(device: CallAudioManager.Device) { - audioManager.isSpeakerphoneOn = device === CallAudioManager.Device.SPEAKER - setBluetoothAudioRoute(device === CallAudioManager.Device.WIRELESS_HEADSET) + audioManager.isSpeakerphoneOn = device is CallAudioManager.Device.Speaker + setBluetoothAudioRoute(device is CallAudioManager.Device.WirelessHeadset) } override fun setMode(mode: CallAudioManager.Mode): Boolean { diff --git a/vector/src/main/java/im/vector/app/features/call/conference/ConferenceEvent.kt b/vector/src/main/java/im/vector/app/features/call/conference/ConferenceEvent.kt new file mode 100644 index 0000000000..cfa076f31b --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/call/conference/ConferenceEvent.kt @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.call.conference + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleObserver +import androidx.lifecycle.OnLifecycleEvent +import androidx.localbroadcastmanager.content.LocalBroadcastManager +import com.facebook.react.bridge.JavaOnlyMap +import org.jitsi.meet.sdk.BroadcastEmitter +import org.jitsi.meet.sdk.BroadcastEvent +import org.jitsi.meet.sdk.JitsiMeet +import timber.log.Timber + +private const val CONFERENCE_URL_DATA_KEY = "url" + +sealed class ConferenceEvent(open val data: Map) { + data class Terminated(override val data: Map) : ConferenceEvent(data) + data class WillJoin(override val data: Map) : ConferenceEvent(data) + data class Joined(override val data: Map) : ConferenceEvent(data) + + fun extractConferenceUrl(): String? { + return data[CONFERENCE_URL_DATA_KEY] as? String + } +} + +class ConferenceEventEmitter(private val context: Context) { + + fun emitConferenceEnded() { + val broadcastEventData = JavaOnlyMap.of(CONFERENCE_URL_DATA_KEY, JitsiMeet.getCurrentConference()) + BroadcastEmitter(context).sendBroadcast(BroadcastEvent.Type.CONFERENCE_TERMINATED.name, broadcastEventData) + } +} + +class ConferenceEventObserver(private val context: Context, + private val onBroadcastEvent: (ConferenceEvent) -> Unit) + : LifecycleObserver { + + // See https://jitsi.github.io/handbook/docs/dev-guide/dev-guide-android-sdk#listening-for-broadcasted-events + private val broadcastReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + intent?.let { onBroadcastReceived(it) } + } + } + + @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) + fun unregisterForBroadcastMessages() { + try { + LocalBroadcastManager.getInstance(context).unregisterReceiver(broadcastReceiver) + } catch (throwable: Throwable) { + Timber.v("Unable to unregister receiver") + } + } + + @OnLifecycleEvent(Lifecycle.Event.ON_CREATE) + fun registerForBroadcastMessages() { + val intentFilter = IntentFilter() + for (type in BroadcastEvent.Type.values()) { + intentFilter.addAction(type.action) + } + try { + LocalBroadcastManager.getInstance(context).registerReceiver(broadcastReceiver, intentFilter) + } catch (throwable: Throwable) { + Timber.v("Unable to register receiver") + } + } + + private fun onBroadcastReceived(intent: Intent) { + val event = BroadcastEvent(intent) + val conferenceEvent = when (event.type) { + BroadcastEvent.Type.CONFERENCE_JOINED -> ConferenceEvent.Joined(event.data) + BroadcastEvent.Type.CONFERENCE_TERMINATED -> ConferenceEvent.Terminated(event.data) + BroadcastEvent.Type.CONFERENCE_WILL_JOIN -> ConferenceEvent.WillJoin(event.data) + else -> null + } + if (conferenceEvent != null) { + onBroadcastEvent(conferenceEvent) + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/call/conference/JitsiActiveConferenceHolder.kt b/vector/src/main/java/im/vector/app/features/call/conference/JitsiActiveConferenceHolder.kt new file mode 100644 index 0000000000..179956612d --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/call/conference/JitsiActiveConferenceHolder.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.call.conference + +import android.content.Context +import androidx.lifecycle.ProcessLifecycleOwner +import org.matrix.android.sdk.api.extensions.orFalse +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class JitsiActiveConferenceHolder @Inject constructor(context: Context) { + + private var activeConference: String? = null + + init { + ProcessLifecycleOwner.get().lifecycle.addObserver(ConferenceEventObserver(context, this::onBroadcastEvent)) + } + + fun isJoined(confId: String?): Boolean { + return confId != null && activeConference?.endsWith(confId).orFalse() + } + + private fun onBroadcastEvent(conferenceEvent: ConferenceEvent) { + when (conferenceEvent) { + is ConferenceEvent.Joined -> activeConference = conferenceEvent.extractConferenceUrl() + is ConferenceEvent.Terminated -> activeConference = null + else -> Unit + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/call/conference/JitsiService.kt b/vector/src/main/java/im/vector/app/features/call/conference/JitsiService.kt index 7b01824c6c..b8b6d83dd1 100644 --- a/vector/src/main/java/im/vector/app/features/call/conference/JitsiService.kt +++ b/vector/src/main/java/im/vector/app/features/call/conference/JitsiService.kt @@ -44,12 +44,16 @@ class JitsiService @Inject constructor( private val rawService: RawService, private val stringProvider: StringProvider, private val themeProvider: ThemeProvider, - private val jitsiWidgetPropertiesFactory: JitsiWidgetPropertiesFactory, private val jitsiJWTFactory: JitsiJWTFactory) { companion object { const val JITSI_OPEN_ID_TOKEN_JWT_AUTH = "openidtoken-jwt" - private const val JITSI_AUTH_KEY = "auth" + } + + private val jitsiWidgetDataFactory by lazy { + JitsiWidgetDataFactory(stringProvider.getString(R.string.preferred_jitsi_domain)) { widget -> + session.widgetService().getWidgetComputedUrl(widget, themeProvider.isLightTheme()) + } } suspend fun createJitsiWidget(roomId: String, withVideo: Boolean): Widget { @@ -85,17 +89,11 @@ class JitsiService @Inject constructor( val widgetEventContent = mapOf( "url" to url, "type" to WidgetType.Jitsi.legacy, - "data" to mapOf( - "conferenceId" to confId, - "domain" to jitsiDomain, - "isAudioOnly" to !withVideo, - JITSI_AUTH_KEY to jitsiAuth - ), + "data" to JitsiWidgetData(jitsiDomain, confId, !withVideo, jitsiAuth), "creatorUserId" to session.myUserId, "id" to widgetId, "name" to "jitsi" ) - return session.widgetService().createRoomWidget(roomId, widgetId, widgetEventContent) } @@ -108,26 +106,30 @@ class JitsiService @Inject constructor( this.avatar = userAvatar?.let { URL(it) } } val roomName = session.getRoomSummary(roomId)?.displayName - val properties = session.widgetService().getWidgetComputedUrl(jitsiWidget, themeProvider.isLightTheme()) - ?.let { url -> jitsiWidgetPropertiesFactory.create(url) } ?: throw IllegalStateException() - - val token = if (jitsiWidget.isOpenIdJWTAuthenticationRequired()) { - getOpenIdJWTToken(roomId, properties.domain, userDisplayName ?: session.myUserId, userAvatar ?: "") + val widgetData = jitsiWidgetDataFactory.create(jitsiWidget) + val token = if (widgetData.isOpenIdJWTAuthenticationRequired()) { + getOpenIdJWTToken(roomId, widgetData.domain, userDisplayName ?: session.myUserId, userAvatar ?: "") } else { null } return JitsiCallViewEvents.JoinConference( enableVideo = enableVideo, - jitsiUrl = properties.domain.ensureProtocol(), + jitsiUrl = widgetData.domain.ensureProtocol(), subject = roomName ?: "", - confId = properties.confId ?: "", + confId = widgetData.confId, userInfo = userInfo, token = token ) } - private fun Widget.isOpenIdJWTAuthenticationRequired(): Boolean { - return widgetContent.data[JITSI_AUTH_KEY] == JITSI_OPEN_ID_TOKEN_JWT_AUTH + fun extractJitsiWidgetData(widget: Widget): JitsiWidgetData? { + return tryOrNull { + jitsiWidgetDataFactory.create(widget) + } + } + + private fun JitsiWidgetData.isOpenIdJWTAuthenticationRequired(): Boolean { + return auth == JITSI_OPEN_ID_TOKEN_JWT_AUTH } private suspend fun getOpenIdJWTToken(roomId: String, domain: String, userDisplayName: String, userAvatar: String): String { diff --git a/vector/src/main/java/im/vector/app/features/call/conference/JitsiWidgetData.kt b/vector/src/main/java/im/vector/app/features/call/conference/JitsiWidgetData.kt new file mode 100644 index 0000000000..323de826a1 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/call/conference/JitsiWidgetData.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.call.conference + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * This is jitsi widget data + * https://github.com/matrix-org/matrix-doc/blob/b910b8966524febe7ffe78f723127a5037defe64/api/widgets/definitions/jitsi_data.yaml + */ +@JsonClass(generateAdapter = true) +data class JitsiWidgetData( + @Json(name = "domain") val domain: String, + @Json(name = "conferenceId") val confId: String, + @Json(name = "isAudioOnly") val isAudioOnly: Boolean = false, + @Json(name = "auth") val auth: String? = null +) diff --git a/vector/src/main/java/im/vector/app/features/call/conference/JitsiWidgetDataFactory.kt b/vector/src/main/java/im/vector/app/features/call/conference/JitsiWidgetDataFactory.kt new file mode 100644 index 0000000000..bceb38d544 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/call/conference/JitsiWidgetDataFactory.kt @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.call.conference + +import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.widgets.model.Widget +import java.net.URL +import java.net.URLDecoder + +class JitsiWidgetDataFactory(private val fallbackJitsiDomain: String, private val urlComputer: (Widget) -> String?) { + + /** + * Extract JitsiWidgetData from a widget. + * For Widget V2, it will extract data from content.data + * For Widget V1, it will extract data from url. + */ + fun create(widget: Widget): JitsiWidgetData { + return widget.widgetContent.data.toModel() ?: widget.createFromUrl() + } + + /** + * This creates a JitsiWidgetData from the url. + * It's a fallback for Widget V1. + * It first get the computed url and then tries to extract JitsiWidgetData from it. + */ + private fun Widget.createFromUrl(): JitsiWidgetData { + return urlComputer(this)?.let { url -> createFromUrl(url) } ?: throw IllegalStateException() + } + + private fun createFromUrl(url: String): JitsiWidgetData { + val configString = tryOrNull { URL(url) }?.query + val configs = configString?.split("&") + ?.map { it.split("=") } + ?.filter { it.size == 2 } + ?.map { (key, value) -> key to URLDecoder.decode(value, "UTF-8") } + ?.toMap() + .orEmpty() + + return JitsiWidgetData( + domain = configs["conferenceDomain"] ?: fallbackJitsiDomain, + confId = configs["conferenceId"] ?: configs["confId"] ?: throw IllegalStateException(), + isAudioOnly = configs["isAudioOnly"].toBoolean(), + auth = configs["auth"] + ) + } +} diff --git a/vector/src/main/java/im/vector/app/features/call/conference/JitsiWidgetPropertiesFactory.kt b/vector/src/main/java/im/vector/app/features/call/conference/JitsiWidgetPropertiesFactory.kt deleted file mode 100644 index 8014e01fb2..0000000000 --- a/vector/src/main/java/im/vector/app/features/call/conference/JitsiWidgetPropertiesFactory.kt +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (c) 2021 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package im.vector.app.features.call.conference - -import android.net.Uri -import im.vector.app.R -import im.vector.app.core.resources.StringProvider -import org.matrix.android.sdk.api.extensions.tryOrNull -import java.net.URLDecoder -import javax.inject.Inject - -class JitsiWidgetPropertiesFactory @Inject constructor( - private val stringProvider: StringProvider -) { - fun create(url: String): JitsiWidgetProperties { - val configString = tryOrNull { Uri.parse(url) }?.fragment - - val configs = configString?.split("&") - ?.map { it.split("=") } - ?.filter { it.size == 2 } - ?.map { (key, value) -> key to URLDecoder.decode(value, "UTF-8") } - ?.toMap() - .orEmpty() - - return JitsiWidgetProperties( - domain = configs["conferenceDomain"] ?: stringProvider.getString(R.string.preferred_jitsi_domain), - confId = configs["conferenceId"], - displayName = configs["displayName"], - avatarUrl = configs["avatarUrl"] - ) - } -} diff --git a/vector/src/main/java/im/vector/app/features/call/conference/RemoveJitsiWidgetView.kt b/vector/src/main/java/im/vector/app/features/call/conference/RemoveJitsiWidgetView.kt new file mode 100644 index 0000000000..391471d2f2 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/call/conference/RemoveJitsiWidgetView.kt @@ -0,0 +1,162 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.call.conference + +import android.annotation.SuppressLint +import android.content.Context +import android.content.res.ColorStateList +import android.util.AttributeSet +import android.view.MotionEvent +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.content.ContextCompat +import androidx.core.view.isVisible +import androidx.core.widget.ImageViewCompat +import im.vector.app.R +import im.vector.app.databinding.ViewRemoveJitsiWidgetBinding +import im.vector.app.features.home.room.detail.RoomDetailViewState +import org.matrix.android.sdk.api.session.room.model.Membership + +@SuppressLint("ClickableViewAccessibility") class RemoveJitsiWidgetView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : ConstraintLayout(context, attrs, defStyleAttr) { + + private sealed class State { + object Unmount : State() + object Idle : State() + data class Sliding(val initialX: Float, val translationX: Float, val hasReachedActivationThreshold: Boolean) : State() + object Progress : State() + } + + private val views: ViewRemoveJitsiWidgetBinding + private var state: State = State.Unmount + var onCompleteSliding: (() -> Unit)? = null + + init { + inflate(context, R.layout.view_remove_jitsi_widget, this) + views = ViewRemoveJitsiWidgetBinding.bind(this) + views.removeJitsiSlidingContainer.setOnTouchListener { _, event -> + val currentState = state + return@setOnTouchListener when (event.action) { + MotionEvent.ACTION_DOWN -> { + if (currentState == State.Idle) { + val initialX = views.removeJitsiSlidingContainer.x - event.rawX + updateState(State.Sliding(initialX, 0f, false)) + } + true + } + MotionEvent.ACTION_UP, + MotionEvent.ACTION_CANCEL -> { + if (currentState is State.Sliding) { + if (currentState.hasReachedActivationThreshold) { + updateState(State.Progress) + } else { + updateState(State.Idle) + } + } + true + } + MotionEvent.ACTION_MOVE -> { + if (currentState is State.Sliding) { + val translationX = (currentState.initialX + event.rawX).coerceAtLeast(0f) + val hasReachedActivationThreshold = translationX >= views.root.width / 4 + updateState(State.Sliding(currentState.initialX, translationX, hasReachedActivationThreshold)) + } + true + } + else -> false + } + } + renderInternalState(state) + } + + fun render(roomDetailViewState: RoomDetailViewState) { + val summary = roomDetailViewState.asyncRoomSummary() + val newState = if (summary?.membership != Membership.JOIN + || roomDetailViewState.isWebRTCCallOptionAvailable() + || !roomDetailViewState.isAllowedToManageWidgets + || roomDetailViewState.jitsiState.widgetId == null) { + State.Unmount + } else if (roomDetailViewState.jitsiState.deleteWidgetInProgress) { + State.Progress + } else { + State.Idle + } + // Don't force Idle if we are already sliding + if (state is State.Sliding && newState is State.Idle) { + return + } else { + updateState(newState) + } + } + + private fun updateState(newState: State) { + if (newState == state) { + return + } + renderInternalState(newState) + state = newState + if (state == State.Progress) { + onCompleteSliding?.invoke() + } + } + + private fun renderInternalState(state: State) { + isVisible = state != State.Unmount + when (state) { + State.Progress -> { + isVisible = true + views.updateVisibilities(true) + views.updateHangupColors(true) + } + State.Idle -> { + isVisible = true + views.updateVisibilities(false) + views.removeJitsiSlidingContainer.translationX = 0f + views.updateHangupColors(false) + } + is State.Sliding -> { + isVisible = true + views.updateVisibilities(false) + views.removeJitsiSlidingContainer.translationX = state.translationX + views.updateHangupColors(state.hasReachedActivationThreshold) + } + else -> Unit + } + } + + private fun ViewRemoveJitsiWidgetBinding.updateVisibilities(isProgress: Boolean) { + removeJitsiProgressContainer.isVisible = isProgress + removeJitsiHangupContainer.isVisible = !isProgress + removeJitsiSlidingContainer.isVisible = !isProgress + } + + private fun ViewRemoveJitsiWidgetBinding.updateHangupColors(activated: Boolean) { + val iconTintColor: Int + val bgColor: Int + if (activated) { + bgColor = ContextCompat.getColor(context, R.color.palette_vermilion) + iconTintColor = ContextCompat.getColor(context, R.color.palette_white) + } else { + bgColor = ContextCompat.getColor(context, android.R.color.transparent) + iconTintColor = ContextCompat.getColor(context, R.color.palette_vermilion) + } + removeJitsiHangupContainer.setBackgroundColor(bgColor) + ImageViewCompat.setImageTintList(removeJitsiHangupIcon, ColorStateList.valueOf(iconTintColor)) + } +} diff --git a/vector/src/main/java/im/vector/app/features/call/conference/VectorJitsiActivity.kt b/vector/src/main/java/im/vector/app/features/call/conference/VectorJitsiActivity.kt index 11b84f4f44..e7fd541f3d 100644 --- a/vector/src/main/java/im/vector/app/features/call/conference/VectorJitsiActivity.kt +++ b/vector/src/main/java/im/vector/app/features/call/conference/VectorJitsiActivity.kt @@ -16,18 +16,17 @@ package im.vector.app.features.call.conference -import android.content.BroadcastReceiver import android.content.Context import android.content.Intent -import android.content.IntentFilter import android.content.pm.PackageManager import android.content.res.Configuration +import android.os.Build import android.os.Bundle import android.os.Parcelable import android.widget.FrameLayout import android.widget.Toast import androidx.core.view.isVisible -import androidx.localbroadcastmanager.content.LocalBroadcastManager +import androidx.lifecycle.Lifecycle import com.airbnb.mvrx.Fail import com.airbnb.mvrx.MvRx import com.airbnb.mvrx.Success @@ -40,12 +39,13 @@ import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.databinding.ActivityJitsiBinding import kotlinx.parcelize.Parcelize -import org.jitsi.meet.sdk.BroadcastEvent +import org.jitsi.meet.sdk.JitsiMeet import org.jitsi.meet.sdk.JitsiMeetActivityDelegate import org.jitsi.meet.sdk.JitsiMeetActivityInterface import org.jitsi.meet.sdk.JitsiMeetConferenceOptions import org.jitsi.meet.sdk.JitsiMeetView import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.util.JsonDict import timber.log.Timber import java.net.URL import javax.inject.Inject @@ -71,13 +71,6 @@ class VectorJitsiActivity : VectorBaseActivity(), JitsiMee injector.inject(this) } - // See https://jitsi.github.io/handbook/docs/dev-guide/dev-guide-android-sdk#listening-for-broadcasted-events - private val broadcastReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context?, intent: Intent?) { - intent?.let { onBroadcastReceived(it) } - } - } - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -94,8 +87,47 @@ class VectorJitsiActivity : VectorBaseActivity(), JitsiMee JitsiCallViewEvents.LeaveConference -> handleLeaveConference() }.exhaustive } + lifecycle.addObserver(ConferenceEventObserver(this, this::onBroadcastEvent)) + } - registerForBroadcastMessages() + override fun onResume() { + super.onResume() + JitsiMeetActivityDelegate.onHostResume(this) + } + + override fun initUiAndData() { + super.initUiAndData() + jitsiMeetView = JitsiMeetView(this) + val params = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT) + views.jitsiLayout.addView(jitsiMeetView, params) + } + + override fun onStop() { + JitsiMeetActivityDelegate.onHostPause(this) + super.onStop() + } + + override fun onDestroy() { + val currentConf = JitsiMeet.getCurrentConference() + jitsiMeetView?.leave() + jitsiMeetView?.dispose() + // Fake emitting CONFERENCE_TERMINATED event when currentConf is not null (probably when closing the PiP screen). + if (currentConf != null) { + ConferenceEventEmitter(this).emitConferenceEnded() + } + JitsiMeetActivityDelegate.onHostDestroy(this) + super.onDestroy() + } + + override fun onBackPressed() { + JitsiMeetActivityDelegate.onBackPressed() + } + + override fun onUserLeaveHint() { + super.onUserLeaveHint() + if (packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)) { + jitsiMeetView?.enterPictureInPicture() + } } private fun handleLeaveConference() { @@ -116,14 +148,16 @@ class VectorJitsiActivity : VectorBaseActivity(), JitsiMee override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, newConfig: Configuration) { super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig) + checkIfActivityShouldBeFinished() Timber.w("onPictureInPictureModeChanged($isInPictureInPictureMode)") } - override fun initUiAndData() { - super.initUiAndData() - jitsiMeetView = JitsiMeetView(this) - val params = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT) - views.jitsiLayout.addView(jitsiMeetView, params) + private fun checkIfActivityShouldBeFinished() { + // OnStop is called when PiP mode is closed directly from the ui + // If stopped is called and PiP mode is not active, we should finish the activity and remove the task as Android creates a new one for PiP. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && !lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED) && !isInPictureInPictureMode) { + finishAndRemoveTask() + } } private fun renderState(viewState: JitsiCallViewState) { @@ -167,34 +201,6 @@ class VectorJitsiActivity : VectorBaseActivity(), JitsiMee jitsiMeetView?.join(jitsiMeetConferenceOptions) } - override fun onStop() { - JitsiMeetActivityDelegate.onHostPause(this) - super.onStop() - } - - override fun onResume() { - JitsiMeetActivityDelegate.onHostResume(this) - super.onResume() - } - - override fun onBackPressed() { - JitsiMeetActivityDelegate.onBackPressed() - super.onBackPressed() - } - - override fun onDestroy() { - JitsiMeetActivityDelegate.onHostDestroy(this) - unregisterForBroadcastMessages() - super.onDestroy() - } - - override fun onUserLeaveHint() { - super.onUserLeaveHint() - if (packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)) { - jitsiMeetView?.enterPictureInPicture() - } - } - override fun onNewIntent(intent: Intent?) { JitsiMeetActivityDelegate.onNewIntent(intent) @@ -217,32 +223,15 @@ class VectorJitsiActivity : VectorBaseActivity(), JitsiMee JitsiMeetActivityDelegate.onRequestPermissionsResult(requestCode, permissions, grantResults) } - private fun registerForBroadcastMessages() { - val intentFilter = IntentFilter() - for (type in BroadcastEvent.Type.values()) { - intentFilter.addAction(type.action) - } - tryOrNull("Unable to register receiver") { - LocalBroadcastManager.getInstance(this).registerReceiver(broadcastReceiver, intentFilter) + private fun onBroadcastEvent(event: ConferenceEvent) { + Timber.v("Broadcast received: $event") + when (event) { + is ConferenceEvent.Terminated -> onConferenceTerminated(event.data) + else -> Unit } } - private fun unregisterForBroadcastMessages() { - tryOrNull("Unable to unregister receiver") { - LocalBroadcastManager.getInstance(this).unregisterReceiver(broadcastReceiver) - } - } - - private fun onBroadcastReceived(intent: Intent) { - val event = BroadcastEvent(intent) - Timber.v("Broadcast received: ${event.type}") - when (event.type) { - BroadcastEvent.Type.CONFERENCE_TERMINATED -> onConferenceTerminated(event.data) - else -> Unit - } - } - - private fun onConferenceTerminated(data: Map) { + private fun onConferenceTerminated(data: JsonDict) { Timber.v("JitsiMeetViewListener.onConferenceTerminated()") // Do not finish if there is an error if (data["error"] == null) { diff --git a/vector/src/main/java/im/vector/app/features/call/dialpad/CallDialPadBottomSheet.kt b/vector/src/main/java/im/vector/app/features/call/dialpad/CallDialPadBottomSheet.kt index e7c8602698..3472d01c72 100644 --- a/vector/src/main/java/im/vector/app/features/call/dialpad/CallDialPadBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/call/dialpad/CallDialPadBottomSheet.kt @@ -89,6 +89,10 @@ class CallDialPadBottomSheet : VectorBaseBottomSheetDialogFragment? = null + // Mute status var micMuted = false private set var videoMuted = false private set - var remoteOnHold = false + var isRemoteOnHold = false private set var isLocalOnHold = false private set @@ -239,6 +244,10 @@ class WebRtcCall( if (mxCall.state == CallState.CreateOffer) { // send offer to peer mxCall.offerSdp(sessionDescription.description) + inviteTimeout = async { + delay(INVITE_TIMEOUT_IN_MS) + endCall(EndCallReason.INVITE_TIMEOUT) + } } else { mxCall.negotiate(sessionDescription.description, SdpType.OFFER) } @@ -348,7 +357,7 @@ class WebRtcCall( fun attachViewRenderers(localViewRenderer: SurfaceViewRenderer?, remoteViewRenderer: SurfaceViewRenderer, mode: String?) { sessionScope?.launch(dispatcher) { - Timber.tag(loggerTag.value).v("attachViewRenderers localRendeder $localViewRenderer / $remoteViewRenderer") + Timber.tag(loggerTag.value).v("attachViewRenderers localRenderer $localViewRenderer / $remoteViewRenderer") localSurfaceRenderers.addIfNeeded(localViewRenderer) remoteSurfaceRenderers.addIfNeeded(remoteViewRenderer) when (mode) { @@ -605,12 +614,12 @@ class WebRtcCall( } private fun updateMuteStatus() { - val micShouldBeMuted = micMuted || remoteOnHold + val micShouldBeMuted = micMuted || isRemoteOnHold localAudioTrack?.setEnabled(!micShouldBeMuted) - remoteAudioTrack?.setEnabled(!remoteOnHold) - val vidShouldBeMuted = videoMuted || remoteOnHold + remoteAudioTrack?.setEnabled(!isRemoteOnHold) + val vidShouldBeMuted = videoMuted || isRemoteOnHold localVideoTrack?.setEnabled(!vidShouldBeMuted) - remoteVideoTrack?.setEnabled(!remoteOnHold) + remoteVideoTrack?.setEnabled(!isRemoteOnHold) } /** @@ -636,16 +645,16 @@ class WebRtcCall( fun updateRemoteOnHold(onHold: Boolean) { sessionScope?.launch(dispatcher) { - if (remoteOnHold == onHold) return@launch + if (isRemoteOnHold == onHold) return@launch val direction: RtpTransceiver.RtpTransceiverDirection if (onHold) { wasLocalOnHold = isLocalOnHold - remoteOnHold = true + isRemoteOnHold = true isLocalOnHold = true direction = RtpTransceiver.RtpTransceiverDirection.SEND_ONLY timer.pause() } else { - remoteOnHold = false + isRemoteOnHold = false isLocalOnHold = wasLocalOnHold onCallBecomeActive(this@WebRtcCall) direction = RtpTransceiver.RtpTransceiverDirection.SEND_RECV @@ -801,17 +810,19 @@ class WebRtcCall( } } - fun endCall(reason: EndCallReason = EndCallReason.USER_HANGUP) { + fun endCall(reason: EndCallReason = EndCallReason.USER_HANGUP, sendSignaling: Boolean = true) { sessionScope?.launch(dispatcher) { if (mxCall.state is CallState.Ended) { return@launch } val reject = mxCall.state is CallState.LocalRinging - terminate(EndCallReason.USER_HANGUP, reject) - if (reject) { - mxCall.reject() - } else { - mxCall.hangUp(reason) + terminate(reason, reject) + if (sendSignaling) { + if (reject) { + mxCall.reject() + } else { + mxCall.hangUp(reason) + } } } } @@ -824,6 +835,8 @@ class WebRtcCall( val cameraManager = context.getSystemService()!! cameraManager.unregisterAvailabilityCallback(cameraAvailabilityCallback) } + inviteTimeout?.cancel() + inviteTimeout = null mxCall.state = CallState.Ended(reason ?: EndCallReason.USER_HANGUP) release() onCallEnded(callId, reason ?: EndCallReason.USER_HANGUP, rejected) @@ -845,6 +858,8 @@ class WebRtcCall( } fun onCallAnswerReceived(callAnswerContent: CallAnswerContent) { + inviteTimeout?.cancel() + inviteTimeout = null sessionScope?.launch(dispatcher) { Timber.tag(loggerTag.value).v("onCallAnswerReceived ${callAnswerContent.callId}") val sdp = SessionDescription(SessionDescription.Type.ANSWER, callAnswerContent.answer.sdp) diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallExt.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallExt.kt index ef9ef3ef9a..ac9d169633 100644 --- a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallExt.kt +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallExt.kt @@ -16,17 +16,20 @@ package im.vector.app.features.call.webrtc +import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.util.MatrixItem import org.matrix.android.sdk.api.util.toMatrixItem fun WebRtcCall.getOpponentAsMatrixItem(session: Session): MatrixItem? { - return session.getRoomSummary(nativeRoomId)?.let { roomSummary -> + return session.getRoom(nativeRoomId)?.let { room -> + val roomSummary = room.roomSummary() ?: return@let null // Fallback to RoomSummary if there is no other member. - if (roomSummary.otherMemberIds.isEmpty()) { + if (roomSummary.otherMemberIds.isEmpty().orFalse()) { roomSummary.toMatrixItem() } else { - roomSummary.otherMemberIds.first().let { session.getUser(it)?.toMatrixItem() } + val userId = roomSummary.otherMemberIds.first() + return room.getRoomMember(userId)?.toMatrixItem() } } } diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt index 73a6c07d6a..9e620174f3 100644 --- a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt @@ -84,9 +84,10 @@ class WebRtcCallManager @Inject constructor( private val sessionScope: CoroutineScope? get() = currentSession?.coroutineScope - interface CurrentCallListener { - fun onCurrentCallChange(call: WebRtcCall?) {} - fun onAudioDevicesChange() {} + interface Listener { + fun onCallEnded(callId: String) = Unit + fun onCurrentCallChange(call: WebRtcCall?) = Unit + fun onAudioDevicesChange() = Unit } val supportedPSTNProtocol: String? @@ -106,13 +107,13 @@ class WebRtcCallManager @Inject constructor( protocolsChecker?.removeListener(listener) } - private val currentCallsListeners = CopyOnWriteArrayList() + private val currentCallsListeners = CopyOnWriteArrayList() - fun addCurrentCallListener(listener: CurrentCallListener) { + fun addListener(listener: Listener) { currentCallsListeners.add(listener) } - fun removeCurrentCallListener(listener: CurrentCallListener) { + fun removeListener(listener: Listener) { currentCallsListeners.remove(listener) } @@ -250,10 +251,13 @@ class WebRtcCallManager @Inject constructor( callsByRoomId[webRtcCall.signalingRoomId]?.remove(webRtcCall) callsByRoomId[webRtcCall.nativeRoomId]?.remove(webRtcCall) transferees.remove(callId) - if (getCurrentCall()?.callId == callId) { + if (currentCall.get()?.callId == callId) { val otherCall = getCalls().lastOrNull() currentCall.setAndNotify(otherCall) } + tryOrNull { + currentCallsListeners.forEach { it.onCallEnded(callId) } + } // There is no active calls if (getCurrentCall() == null) { Timber.tag(loggerTag.value).v("Dispose peerConnectionFactory as there is no need to keep one") @@ -424,7 +428,11 @@ class WebRtcCallManager @Inject constructor( override fun onCallManagedByOtherSession(callId: String) { Timber.tag(loggerTag.value).v("onCallManagedByOtherSession: $callId") - onCallEnded(callId, EndCallReason.ANSWERED_ELSEWHERE, false) + val call = callsByCallId[callId] + ?: return Unit.also { + Timber.tag(loggerTag.value).w("onCallManagedByOtherSession for non active call? $callId") + } + call.endCall(EndCallReason.ANSWERED_ELSEWHERE, sendSignaling = false) } override fun onCallAssertedIdentityReceived(callAssertedIdentityContent: CallAssertedIdentityContent) { diff --git a/vector/src/main/java/im/vector/app/features/command/Command.kt b/vector/src/main/java/im/vector/app/features/command/Command.kt index 3719618d31..206c5af17a 100644 --- a/vector/src/main/java/im/vector/app/features/command/Command.kt +++ b/vector/src/main/java/im/vector/app/features/command/Command.kt @@ -48,7 +48,7 @@ enum class Command(val command: String, val parameters: String, @StringRes val d CONFETTI("/confetti", "", R.string.command_confetti, false), SNOWFALL("/snowfall", "", R.string.command_snow, false), CREATE_SPACE("/createspace", " *", R.string.command_description_create_space, true), - ADD_TO_SPACE("/addToSpace", "spaceId", R.string.command_description_create_space, true), + ADD_TO_SPACE("/addToSpace", "spaceId", R.string.command_description_add_to_space, true), JOIN_SPACE("/joinSpace", "spaceId", R.string.command_description_join_space, true), LEAVE_ROOM("/leave", "", R.string.command_description_leave_room, true), UPGRADE_ROOM("/upgraderoom", "newVersion", R.string.command_description_upgrade_room, true); diff --git a/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookFragment.kt b/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookFragment.kt index 9291352821..19024fcf8b 100644 --- a/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookFragment.kt +++ b/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookFragment.kt @@ -23,14 +23,13 @@ import android.view.ViewGroup import androidx.core.view.isVisible import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.withState -import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.jakewharton.rxbinding3.widget.checkedChanges import com.jakewharton.rxbinding3.widget.textChanges -import im.vector.app.R import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.configureWith import im.vector.app.core.extensions.hideKeyboard import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.core.utils.showIdentityServerConsentDialog import im.vector.app.databinding.FragmentContactsBookBinding import im.vector.app.features.userdirectory.PendingSelection import im.vector.app.features.userdirectory.UserListAction @@ -76,14 +75,9 @@ class ContactsBookFragment @Inject constructor( private fun setupConsentView() { views.phoneBookSearchForMatrixContacts.setOnClickListener { withState(contactsBookViewModel) { state -> - MaterialAlertDialogBuilder(requireActivity()) - .setTitle(R.string.identity_server_consent_dialog_title) - .setMessage(getString(R.string.identity_server_consent_dialog_content, state.identityServerUrl ?: "")) - .setPositiveButton(R.string.yes) { _, _ -> - contactsBookViewModel.handle(ContactsBookAction.UserConsentGranted) - } - .setNegativeButton(R.string.no, null) - .show() + requireContext().showIdentityServerConsentDialog(state.identityServerUrl) { + contactsBookViewModel.handle(ContactsBookAction.UserConsentGranted) + } } } } diff --git a/vector/src/main/java/im/vector/app/features/crypto/keys/KeysExporter.kt b/vector/src/main/java/im/vector/app/features/crypto/keys/KeysExporter.kt index 2c66a14cb4..3db67df8e1 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/keys/KeysExporter.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/keys/KeysExporter.kt @@ -18,24 +18,43 @@ package im.vector.app.features.crypto.keys import android.content.Context import android.net.Uri -import kotlinx.coroutines.Dispatchers +import im.vector.app.core.dispatchers.CoroutineDispatchers import kotlinx.coroutines.withContext import org.matrix.android.sdk.api.session.Session import javax.inject.Inject class KeysExporter @Inject constructor( private val session: Session, - private val context: Context + private val context: Context, + private val dispatchers: CoroutineDispatchers ) { /** * Export keys and write them to the provided uri */ suspend fun export(password: String, uri: Uri) { - return withContext(Dispatchers.IO) { + withContext(dispatchers.io) { val data = session.cryptoService().exportRoomKeys(password) context.contentResolver.openOutputStream(uri) ?.use { it.write(data) } - ?: throw IllegalStateException("Unable to open file for writting") + ?: throw IllegalStateException("Unable to open file for writing") + verifyExportedKeysOutputFileSize(uri, expectedSize = data.size.toLong()) + } + } + + private fun verifyExportedKeysOutputFileSize(uri: Uri, expectedSize: Long) { + val output = context.contentResolver.openFileDescriptor(uri, "r", null) + when { + output == null -> throw IllegalStateException("Exported file not found") + output.statSize != expectedSize -> { + throw UnexpectedExportKeysFileSizeException( + expectedFileSize = expectedSize, + actualFileSize = output.statSize + ) + } } } } + +class UnexpectedExportKeysFileSizeException(expectedFileSize: Long, actualFileSize: Long) : IllegalStateException( + "Exported Keys file has unexpected file size, got: $actualFileSize but expected: $expectedFileSize" +) diff --git a/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageViewModel.kt b/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageViewModel.kt index 9a5fc4ca06..b4ff9ab22c 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageViewModel.kt @@ -76,7 +76,6 @@ class SharedSecureStorageViewModel @AssistedInject constructor( } init { - setState { copy(userId = session.myUserId) } @@ -167,10 +166,14 @@ class SharedSecureStorageViewModel @AssistedInject constructor( if (state.checkingSSSSAction is Loading) return@withState // ignore when (state.step) { SharedSecureStorageViewState.Step.EnterKey -> { - setState { - copy( - step = SharedSecureStorageViewState.Step.EnterPassphrase - ) + if (state.hasPassphrase) { + setState { + copy( + step = SharedSecureStorageViewState.Step.EnterPassphrase + ) + } + } else { + _viewEvents.post(SharedSecureStorageViewEvent.Dismiss) } } SharedSecureStorageViewState.Step.ResetAll -> { diff --git a/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolActivity.kt b/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolActivity.kt index c502d92a4c..36ccef1fca 100644 --- a/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolActivity.kt +++ b/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolActivity.kt @@ -147,10 +147,6 @@ class RoomDevToolActivity : SimpleFragmentActivity(), RoomDevToolViewModel.Facto } override fun onOptionsItemSelected(item: MenuItem): Boolean { - if (item.itemId == android.R.id.home) { - onBackPressed() - return true - } if (item.itemId == R.id.menuItemEdit) { viewModel.handle(RoomDevToolAction.MenuEdit) return true diff --git a/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsFragment.kt b/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsFragment.kt index 0b8674ec6f..41b83c627d 100644 --- a/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsFragment.kt @@ -32,6 +32,7 @@ import im.vector.app.core.extensions.observeEvent import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.utils.ensureProtocol +import im.vector.app.core.utils.showIdentityServerConsentDialog import im.vector.app.databinding.FragmentGenericRecyclerBinding import im.vector.app.features.discovery.change.SetIdentityServerFragment import im.vector.app.features.settings.VectorSettingsActivity @@ -179,14 +180,9 @@ class DiscoverySettingsFragment @Inject constructor( override fun onTapUpdateUserConsent(newValue: Boolean) { if (newValue) { withState(viewModel) { state -> - MaterialAlertDialogBuilder(requireActivity()) - .setTitle(R.string.identity_server_consent_dialog_title) - .setMessage(getString(R.string.identity_server_consent_dialog_content, state.identityServer.invoke())) - .setPositiveButton(R.string.yes) { _, _ -> - viewModel.handle(DiscoverySettingsAction.UpdateUserConsent(true)) - } - .setNegativeButton(R.string.no, null) - .show() + requireContext().showIdentityServerConsentDialog(state.identityServer.invoke()) { + viewModel.handle(DiscoverySettingsAction.UpdateUserConsent(true)) + } } } else { viewModel.handle(DiscoverySettingsAction.UpdateUserConsent(false)) diff --git a/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsViewModel.kt b/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsViewModel.kt index 11fd796534..3cd6c31ab2 100644 --- a/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsViewModel.kt @@ -65,7 +65,7 @@ class DiscoverySettingsViewModel @AssistedInject constructor( setState { copy( identityServer = Success(identityServerUrl), - userConsent = false + userConsent = identityService.getUserConsent() ) } if (currentIS != identityServerUrl) retrieveBinding() diff --git a/vector/src/main/java/im/vector/app/features/home/AvatarRenderer.kt b/vector/src/main/java/im/vector/app/features/home/AvatarRenderer.kt index 787027e0e2..2ee3233637 100644 --- a/vector/src/main/java/im/vector/app/features/home/AvatarRenderer.kt +++ b/vector/src/main/java/im/vector/app/features/home/AvatarRenderer.kt @@ -138,13 +138,13 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active target: Target) { val placeholder = getPlaceholderDrawable(matrixItem) glideRequests.loadResolvedUrl(matrixItem.avatarUrl) - .apply { + .let { when (matrixItem) { is MatrixItem.SpaceItem -> { - transform(MultiTransformation(CenterCrop(), RoundedCorners(dimensionConverter.dpToPx(8)))) + it.transform(MultiTransformation(CenterCrop(), RoundedCorners(dimensionConverter.dpToPx(8)))) } else -> { - apply(RequestOptions.circleCropTransform()) + it.apply(RequestOptions.circleCropTransform()) } } } @@ -157,13 +157,13 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active fun shortcutDrawable(glideRequests: GlideRequests, matrixItem: MatrixItem, iconSize: Int): Bitmap { return glideRequests .asBitmap() - .apply { + .let { val resolvedUrl = resolvedUrl(matrixItem.avatarUrl) if (resolvedUrl != null) { - load(resolvedUrl) + it.load(resolvedUrl) } else { val avatarColor = matrixItemColorProvider.getColor(matrixItem) - load(TextDrawable.builder() + it.load(TextDrawable.builder() .beginConfig() .bold() .endConfig() @@ -171,6 +171,7 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active .toBitmap(width = iconSize, height = iconSize)) } } + .apply(RequestOptions.centerCropTransform()) .submit(iconSize, iconSize) .get() } diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt index 4a563b563a..1513431196 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt @@ -24,12 +24,12 @@ import android.os.Bundle import android.os.Parcelable import android.view.Menu import android.view.MenuItem -import com.google.android.material.appbar.MaterialToolbar import androidx.core.view.GravityCompat import androidx.core.view.isVisible import androidx.drawerlayout.widget.DrawerLayout import com.airbnb.mvrx.MvRx import com.airbnb.mvrx.viewModel +import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.dialog.MaterialAlertDialogBuilder import im.vector.app.AppStateHandler import im.vector.app.R @@ -51,6 +51,9 @@ import im.vector.app.features.navigation.Navigator import im.vector.app.features.notifications.NotificationDrawerManager import im.vector.app.features.permalink.NavigationInterceptor import im.vector.app.features.permalink.PermalinkHandler +import im.vector.app.features.permalink.PermalinkHandler.Companion.MATRIX_TO_CUSTOM_SCHEME_URL_BASE +import im.vector.app.features.permalink.PermalinkHandler.Companion.ROOM_LINK_PREFIX +import im.vector.app.features.permalink.PermalinkHandler.Companion.USER_LINK_PREFIX import im.vector.app.features.popup.DefaultVectorAlert import im.vector.app.features.popup.PopupAlertManager import im.vector.app.features.popup.VerificationVectorAlert @@ -58,6 +61,7 @@ import im.vector.app.features.rageshake.ReportType import im.vector.app.features.rageshake.VectorUncaughtExceptionHandler import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorSettingsActivity +import im.vector.app.features.spaces.RestrictedPromoBottomSheet import im.vector.app.features.spaces.SpaceCreationActivity import im.vector.app.features.spaces.SpacePreviewActivity import im.vector.app.features.spaces.SpaceSettingsMenuBottomSheet @@ -69,7 +73,7 @@ import im.vector.app.features.workers.signout.ServerBackupStatusViewState import im.vector.app.push.fcm.FcmHelper import io.reactivex.android.schedulers.AndroidSchedulers import kotlinx.parcelize.Parcelize -import org.matrix.android.sdk.api.session.initsync.InitialSyncProgressService +import org.matrix.android.sdk.api.session.initsync.SyncStatusService import org.matrix.android.sdk.api.session.permalinks.PermalinkService import org.matrix.android.sdk.api.util.MatrixItem import org.matrix.android.sdk.internal.session.sync.InitialSyncStrategy @@ -80,7 +84,8 @@ import javax.inject.Inject @Parcelize data class HomeActivityArgs( val clearNotification: Boolean, - val accountCreation: Boolean + val accountCreation: Boolean, + val inviteNotificationRoomId: String? = null ) : Parcelable class HomeActivity : @@ -89,6 +94,7 @@ class HomeActivity : UnknownDeviceDetectorSharedViewModel.Factory, ServerBackupStatusViewModel.Factory, UnreadMessagesSharedViewModel.Factory, + PromoteRestrictedViewModel.Factory, NavigationInterceptor, SpaceInviteBottomSheet.InteractionListener { @@ -99,6 +105,8 @@ class HomeActivity : private val serverBackupStatusViewModel: ServerBackupStatusViewModel by viewModel() @Inject lateinit var serverBackupviewModelFactory: ServerBackupStatusViewModel.Factory + @Inject lateinit var promoteRestrictedViewModelFactory: PromoteRestrictedViewModel.Factory + private val promoteRestrictedViewModel: PromoteRestrictedViewModel by viewModel() @Inject lateinit var activeSessionHolder: ActiveSessionHolder @Inject lateinit var vectorUncaughtExceptionHandler: VectorUncaughtExceptionHandler @@ -143,6 +151,8 @@ class HomeActivity : } } + override fun getCoordinatorLayout() = views.coordinatorLayout + override fun getBinding() = ActivityHomeBinding.inflate(layoutInflater) override fun injectWith(injector: ScreenComponent) { @@ -171,18 +181,13 @@ class HomeActivity : replaceFragment(R.id.homeDrawerFragmentContainer, HomeDrawerFragment::class.java) } -// appStateHandler.selectedRoomGroupingObservable.subscribe { -// if (supportFragmentManager.getFragment()) -// replaceFragment(R.id.homeDetailFragmentContainer, HomeDetailFragment::class.java, allowStateLoss = true) -// }.disposeOnDestroy() - sharedActionViewModel .observe() .subscribe { sharedAction -> when (sharedAction) { - is HomeActivitySharedAction.OpenDrawer -> views.drawerLayout.openDrawer(GravityCompat.START) - is HomeActivitySharedAction.CloseDrawer -> views.drawerLayout.closeDrawer(GravityCompat.START) - is HomeActivitySharedAction.OpenGroup -> { + is HomeActivitySharedAction.OpenDrawer -> views.drawerLayout.openDrawer(GravityCompat.START) + is HomeActivitySharedAction.CloseDrawer -> views.drawerLayout.closeDrawer(GravityCompat.START) + is HomeActivitySharedAction.OpenGroup -> { views.drawerLayout.closeDrawer(GravityCompat.START) // Temporary @@ -196,10 +201,10 @@ class HomeActivity : // we might want to delay that to avoid having the drawer animation lagging // would be probably better to let the drawer do that? in the on closed callback? } - is HomeActivitySharedAction.OpenSpacePreview -> { + is HomeActivitySharedAction.OpenSpacePreview -> { startActivity(SpacePreviewActivity.newIntent(this, sharedAction.spaceId)) } - is HomeActivitySharedAction.AddSpace -> { + is HomeActivitySharedAction.AddSpace -> { createSpaceResultLauncher.launch(SpaceCreationActivity.newIntent(this)) } is HomeActivitySharedAction.ShowSpaceSettings -> { @@ -212,11 +217,11 @@ class HomeActivity : }) .show(supportFragmentManager, "SPACE_SETTINGS") } - is HomeActivitySharedAction.OpenSpaceInvite -> { + is HomeActivitySharedAction.OpenSpaceInvite -> { SpaceInviteBottomSheet.newInstance(sharedAction.spaceId) .show(supportFragmentManager, "SPACE_INVITE") } - HomeActivitySharedAction.SendSpaceFeedBack -> { + HomeActivitySharedAction.SendSpaceFeedBack -> { bugReporter.openBugReportScreen(this, ReportType.SPACE_BETA_FEEDBACK) } }.exhaustive @@ -228,13 +233,18 @@ class HomeActivity : if (args?.clearNotification == true) { notificationDrawerManager.clearAllEvents() } + if (args?.inviteNotificationRoomId != null) { + activeSessionHolder.getSafeActiveSession()?.permalinkService()?.createPermalink(args.inviteNotificationRoomId)?.let { + navigator.openMatrixToBottomSheet(this, it) + } + } homeActivityViewModel.observeViewEvents { when (it) { is HomeActivityViewEvents.AskPasswordToInitCrossSigning -> handleAskPasswordToInitCrossSigning(it) - is HomeActivityViewEvents.OnNewSession -> handleOnNewSession(it) - HomeActivityViewEvents.PromptToEnableSessionPush -> handlePromptToEnablePush() - is HomeActivityViewEvents.OnCrossSignedInvalidated -> handleCrossSigningInvalidated(it) + is HomeActivityViewEvents.OnNewSession -> handleOnNewSession(it) + HomeActivityViewEvents.PromptToEnableSessionPush -> handlePromptToEnablePush() + is HomeActivityViewEvents.OnCrossSignedInvalidated -> handleCrossSigningInvalidated(it) }.exhaustive } homeActivityViewModel.subscribe(this) { renderState(it) } @@ -242,6 +252,21 @@ class HomeActivity : shortcutsHandler.observeRoomsAndBuildShortcuts() .disposeOnDestroy() + if (!vectorPreferences.didPromoteNewRestrictedFeature()) { + promoteRestrictedViewModel.subscribe(this) { + if (it.activeSpaceSummary != null && !it.activeSpaceSummary.isPublic + && it.activeSpaceSummary.otherMemberIds.isNotEmpty()) { + // It's a private space with some members show this once + if (it.canUserManageSpace && !popupAlertManager.hasAlertsToShow()) { + if (!vectorPreferences.didPromoteNewRestrictedFeature()) { + vectorPreferences.setDidPromoteNewRestrictedFeature() + RestrictedPromoBottomSheet().show(supportFragmentManager, "RestrictedPromoBottomSheet") + } + } + } + } + } + if (isFirstCreation()) { handleIntent(intent) } @@ -250,20 +275,19 @@ class HomeActivity : private fun handleIntent(intent: Intent?) { intent?.dataString?.let { deepLink -> val resolvedLink = when { - deepLink.startsWith(PermalinkService.MATRIX_TO_URL_BASE) -> deepLink - deepLink.startsWith(MATRIX_TO_CUSTOM_SCHEME_URL_BASE) -> { - // This is a bit ugly, but for now just convert to matrix.to link for compatibility - when { + // Element custom scheme is not handled by the sdk, convert it to matrix.to link for compatibility + deepLink.startsWith(MATRIX_TO_CUSTOM_SCHEME_URL_BASE) -> { + val let = when { deepLink.startsWith(USER_LINK_PREFIX) -> deepLink.substring(USER_LINK_PREFIX.length) deepLink.startsWith(ROOM_LINK_PREFIX) -> deepLink.substring(ROOM_LINK_PREFIX.length) else -> null - }?.let { - activeSessionHolder.getSafeActiveSession()?.permalinkService()?.createPermalink(it) + }?.let { permalinkId -> + activeSessionHolder.getSafeActiveSession()?.permalinkService()?.createPermalink(permalinkId) } + let } - else -> return@let + else -> deepLink } - permalinkHandler.launch( context = this, deepLink = resolvedLink, @@ -274,9 +298,11 @@ class HomeActivity : .observeOn(AndroidSchedulers.mainThread()) .subscribe { isHandled -> if (!isHandled) { + val isMatrixToLink = deepLink.startsWith(PermalinkService.MATRIX_TO_URL_BASE) + || deepLink.startsWith(MATRIX_TO_CUSTOM_SCHEME_URL_BASE) MaterialAlertDialogBuilder(this) .setTitle(R.string.dialog_title_error) - .setMessage(R.string.permalink_malformed) + .setMessage(if (isMatrixToLink) R.string.permalink_malformed else R.string.universal_link_malformed) .setPositiveButton(R.string.ok, null) .show() } @@ -286,11 +312,11 @@ class HomeActivity : } private fun renderState(state: HomeActivityViewState) { - when (val status = state.initialSyncProgressServiceStatus) { - is InitialSyncProgressService.Status.Idle -> { + when (val status = state.syncStatusServiceStatus) { + is SyncStatusService.Status.Idle -> { views.waitingView.root.isVisible = false } - is InitialSyncProgressService.Status.Progressing -> { + is SyncStatusService.Status.Progressing -> { val initSyncStepStr = initSyncStepFormatter.format(status.initSyncStep) Timber.v("$initSyncStepStr ${status.percentProgress}") views.waitingView.root.setOnClickListener { @@ -308,6 +334,7 @@ class HomeActivity : } views.waitingView.root.isVisible = true } + else -> Unit }.exhaustive } @@ -406,9 +433,17 @@ class HomeActivity : override fun onNewIntent(intent: Intent?) { super.onNewIntent(intent) - if (intent?.getParcelableExtra(MvRx.KEY_ARG)?.clearNotification == true) { + val parcelableExtra = intent?.getParcelableExtra(MvRx.KEY_ARG) + if (parcelableExtra?.clearNotification == true) { notificationDrawerManager.clearAllEvents() } + if (parcelableExtra?.inviteNotificationRoomId != null) { + activeSessionHolder.getSafeActiveSession() + ?.permalinkService() + ?.createPermalink(parcelableExtra.inviteNotificationRoomId)?.let { + navigator.openMatrixToBottomSheet(this, it) + } + } handleIntent(intent) } @@ -444,22 +479,22 @@ class HomeActivity : override fun getMenuRes() = R.menu.home override fun onPrepareOptionsMenu(menu: Menu): Boolean { - menu.findItem(R.id.menu_home_init_sync_legacy)?.isVisible = vectorPreferences.developerMode() - menu.findItem(R.id.menu_home_init_sync_optimized)?.isVisible = vectorPreferences.developerMode() + menu.findItem(R.id.menu_home_init_sync_legacy).isVisible = vectorPreferences.developerMode() + menu.findItem(R.id.menu_home_init_sync_optimized).isVisible = vectorPreferences.developerMode() return super.onPrepareOptionsMenu(menu) } override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { - R.id.menu_home_suggestion -> { + R.id.menu_home_suggestion -> { bugReporter.openBugReportScreen(this, ReportType.SUGGESTION) return true } - R.id.menu_home_report_bug -> { + R.id.menu_home_report_bug -> { bugReporter.openBugReportScreen(this, ReportType.BUG_REPORT) return true } - R.id.menu_home_init_sync_legacy -> { + R.id.menu_home_init_sync_legacy -> { // Configure the SDK initialSyncStrategy = InitialSyncStrategy.Legacy // And clear cache @@ -473,11 +508,11 @@ class HomeActivity : MainActivity.restartApp(this, MainActivityArgs(clearCache = true)) return true } - R.id.menu_home_filter -> { + R.id.menu_home_filter -> { navigator.openRoomsFiltering(this) return true } - R.id.menu_home_setting -> { + R.id.menu_home_setting -> { navigator.openSettings(this) return true } @@ -532,10 +567,15 @@ class HomeActivity : } companion object { - fun newIntent(context: Context, clearNotification: Boolean = false, accountCreation: Boolean = false): Intent { + fun newIntent(context: Context, + clearNotification: Boolean = false, + accountCreation: Boolean = false, + inviteNotificationRoomId: String? = null + ): Intent { val args = HomeActivityArgs( clearNotification = clearNotification, - accountCreation = accountCreation + accountCreation = accountCreation, + inviteNotificationRoomId = inviteNotificationRoomId ) return Intent(context, HomeActivity::class.java) @@ -543,9 +583,7 @@ class HomeActivity : putExtra(MvRx.KEY_ARG, args) } } - - private const val MATRIX_TO_CUSTOM_SCHEME_URL_BASE = "element://" - private const val ROOM_LINK_PREFIX = "${MATRIX_TO_CUSTOM_SCHEME_URL_BASE}room/" - private const val USER_LINK_PREFIX = "${MATRIX_TO_CUSTOM_SCHEME_URL_BASE}user/" } + + override fun create(initialState: ActiveSpaceViewState) = promoteRestrictedViewModelFactory.create(initialState) } diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt index bfedbd6f52..1aa2f59337 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt @@ -40,7 +40,7 @@ import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse import org.matrix.android.sdk.api.auth.registration.nextUncompletedStage import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.pushrules.RuleIds -import org.matrix.android.sdk.api.session.initsync.InitialSyncProgressService +import org.matrix.android.sdk.api.session.initsync.SyncStatusService import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams import org.matrix.android.sdk.api.util.toMatrixItem @@ -122,25 +122,26 @@ class HomeActivityViewModel @AssistedInject constructor( private fun observeInitialSync() { val session = activeSessionHolder.getSafeActiveSession() ?: return - session.getInitialSyncProgressStatus() + session.getSyncStatusLive() .asObservable() .subscribe { status -> when (status) { - is InitialSyncProgressService.Status.Progressing -> { + is SyncStatusService.Status.Progressing -> { // Schedule a check of the bootstrap when the init sync will be finished checkBootstrap = true } - is InitialSyncProgressService.Status.Idle -> { + is SyncStatusService.Status.Idle -> { if (checkBootstrap) { checkBootstrap = false maybeBootstrapCrossSigningAfterInitialSync() } } + else -> Unit } setState { copy( - initialSyncProgressServiceStatus = status + syncStatusServiceStatus = status ) } } diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewState.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewState.kt index d4df7cd073..f3bddaf0d2 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewState.kt @@ -17,8 +17,8 @@ package im.vector.app.features.home import com.airbnb.mvrx.MvRxState -import org.matrix.android.sdk.api.session.initsync.InitialSyncProgressService +import org.matrix.android.sdk.api.session.initsync.SyncStatusService data class HomeActivityViewState( - val initialSyncProgressServiceStatus: InitialSyncProgressService.Status = InitialSyncProgressService.Status.Idle + val syncStatusServiceStatus: SyncStatusService.Status = SyncStatusService.Status.Idle ) : MvRxState diff --git a/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt index 683ee40508..627f4b4581 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt @@ -22,7 +22,6 @@ import android.view.Menu import android.view.MenuItem import android.view.View import android.view.ViewGroup -import androidx.core.view.get import androidx.core.view.isVisible import androidx.core.view.iterator import androidx.fragment.app.Fragment @@ -30,6 +29,7 @@ import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState import com.google.android.material.badge.BadgeDrawable +import im.vector.app.AppStateHandler import im.vector.app.R import im.vector.app.RoomGroupingMethod import im.vector.app.core.extensions.commitTransaction @@ -39,8 +39,8 @@ import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.resources.ColorProvider import im.vector.app.core.ui.views.CurrentCallsView +import im.vector.app.core.ui.views.CurrentCallsViewPresenter import im.vector.app.core.ui.views.KeysBackupBanner -import im.vector.app.core.ui.views.KnownCallsViewHolder import im.vector.app.databinding.FragmentHomeDetailBinding import im.vector.app.features.call.SharedKnownCallsViewModel import im.vector.app.features.call.VectorCallActivity @@ -70,7 +70,8 @@ class HomeDetailFragment @Inject constructor( private val colorProvider: ColorProvider, private val alertManager: PopupAlertManager, private val callManager: WebRtcCallManager, - private val vectorPreferences: VectorPreferences + private val vectorPreferences: VectorPreferences, + private val appStateHandler: AppStateHandler ) : VectorBaseFragment(), KeysBackupBanner.Delegate, CurrentCallsView.Callback, @@ -117,7 +118,7 @@ class HomeDetailFragment @Inject constructor( return FragmentHomeDetailBinding.inflate(inflater, container, false) } - private val activeCallViewHolder = KnownCallsViewHolder() + private val currentCallsViewPresenter = CurrentCallsViewPresenter() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -190,16 +191,33 @@ class HomeDetailFragment @Inject constructor( sharedCallActionViewModel .liveKnownCalls .observe(viewLifecycleOwner, { - activeCallViewHolder.updateCall(callManager.getCurrentCall(), callManager.getCalls()) + currentCallsViewPresenter.updateCall(callManager.getCurrentCall(), callManager.getCalls()) invalidateOptionsMenu() }) } + override fun onDestroyView() { + currentCallsViewPresenter.unBind() + super.onDestroyView() + } + override fun onResume() { super.onResume() // update notification tab if needed updateTabVisibilitySafely(R.id.bottom_action_notification, vectorPreferences.labAddNotificationTab()) callManager.checkForProtocolsSupportIfNeeded() + + // Current space/group is not live so at least refresh toolbar on resume + appStateHandler.getCurrentRoomGroupingMethod()?.let { roomGroupingMethod -> + when (roomGroupingMethod) { + is RoomGroupingMethod.ByLegacyGroup -> { + onGroupChange(roomGroupingMethod.groupSummary) + } + is RoomGroupingMethod.BySpace -> { + onSpaceChange(roomGroupingMethod.spaceSummary) + } + } + } } private fun promptForNewUnknownDevices(uid: String, state: UnknownDevicesState, newest: DeviceInfo) { @@ -291,12 +309,7 @@ class HomeDetailFragment @Inject constructor( } private fun setupActiveCallView() { - activeCallViewHolder.bind( - views.activeCallPiP, - views.activeCallView, - views.activeCallPiPWrap, - this - ) + currentCallsViewPresenter.bind(views.currentCallsView, this) } private fun setupToolbar() { @@ -427,7 +440,11 @@ class HomeDetailFragment @Inject constructor( views.bottomNavigationView.getOrCreateBadge(R.id.bottom_action_people).render(it.notificationCountPeople, it.notificationHighlightPeople) views.bottomNavigationView.getOrCreateBadge(R.id.bottom_action_rooms).render(it.notificationCountRooms, it.notificationHighlightRooms) views.bottomNavigationView.getOrCreateBadge(R.id.bottom_action_notification).render(it.notificationCountCatchup, it.notificationHighlightCatchup) - views.syncStateView.render(it.syncState) + views.syncStateView.render( + it.syncState, + it.incrementalSyncStatus, + it.pushCounter, + vectorPreferences.developerShowDebugInfo()) hasUnreadRooms = it.hasUnreadMessages } diff --git a/vector/src/main/java/im/vector/app/features/home/HomeDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/HomeDetailViewModel.kt index b960402f90..460975c2c2 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeDetailViewModel.kt @@ -33,13 +33,16 @@ import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.createdirect.DirectRoomHelper import im.vector.app.features.invite.AutoAcceptInvites import im.vector.app.features.invite.showInvites +import im.vector.app.features.settings.VectorDataStore import im.vector.app.features.ui.UiStateRepository import io.reactivex.schedulers.Schedulers import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch import org.matrix.android.sdk.api.query.ActiveSpaceFilter import org.matrix.android.sdk.api.query.RoomCategoryFilter import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.initsync.SyncStatusService import org.matrix.android.sdk.api.session.room.RoomSortOrder import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams @@ -56,10 +59,11 @@ import java.util.concurrent.TimeUnit class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: HomeDetailViewState, private val session: Session, private val uiStateRepository: UiStateRepository, + private val vectorDataStore: VectorDataStore, private val callManager: WebRtcCallManager, private val directRoomHelper: DirectRoomHelper, private val appStateHandler: AppStateHandler, -private val autoAcceptInvites: AutoAcceptInvites) + private val autoAcceptInvites: AutoAcceptInvites) : VectorViewModel(initialState), CallProtocolsChecker.Listener { @@ -89,6 +93,7 @@ private val autoAcceptInvites: AutoAcceptInvites) observeRoomGroupingMethod() observeRoomSummaries() updateShowDialPadTab() + observeDataStore() callManager.addProtocolsCheckerListener(this) session.rx().liveUser(session.myUserId).execute { copy( @@ -97,6 +102,18 @@ private val autoAcceptInvites: AutoAcceptInvites) } } + private fun observeDataStore() { + viewModelScope.launch { + vectorDataStore.pushCounterFlow.collect { nbOfPush -> + setState { + copy( + pushCounter = nbOfPush + ) + } + } + } + } + override fun handle(action: HomeDetailAction) { when (action) { is HomeDetailAction.SwitchTab -> handleSwitchTab(action) @@ -173,6 +190,17 @@ private val autoAcceptInvites: AutoAcceptInvites) } } .disposeOnClear() + + session.getSyncStatusLive() + .asObservable() + .subscribe { + if (it is SyncStatusService.Status.IncrementalSyncStatus) { + setState { + copy(incrementalSyncStatus = it) + } + } + } + .disposeOnClear() } private fun observeRoomGroupingMethod() { diff --git a/vector/src/main/java/im/vector/app/features/home/HomeDetailViewState.kt b/vector/src/main/java/im/vector/app/features/home/HomeDetailViewState.kt index 304444abdd..4022a0d9fb 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeDetailViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeDetailViewState.kt @@ -22,6 +22,7 @@ import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.Uninitialized import im.vector.app.R import im.vector.app.RoomGroupingMethod +import org.matrix.android.sdk.api.session.initsync.SyncStatusService import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.sync.SyncState import org.matrix.android.sdk.api.util.MatrixItem @@ -39,6 +40,8 @@ data class HomeDetailViewState( val notificationHighlightRooms: Boolean = false, val hasUnreadMessages: Boolean = false, val syncState: SyncState = SyncState.Idle, + val incrementalSyncStatus: SyncStatusService.Status.IncrementalSyncStatus = SyncStatusService.Status.IncrementalSyncIdle, + val pushCounter: Int = 0, val showDialPadTab: Boolean = false ) : MvRxState diff --git a/vector/src/main/java/im/vector/app/features/home/PromoteRestrictedViewModel.kt b/vector/src/main/java/im/vector/app/features/home/PromoteRestrictedViewModel.kt new file mode 100644 index 0000000000..ae7b495aa2 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/PromoteRestrictedViewModel.kt @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.home + +import com.airbnb.mvrx.ActivityViewModelContext +import com.airbnb.mvrx.FragmentViewModelContext +import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.ViewModelContext +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import im.vector.app.AppStateHandler +import im.vector.app.RoomGroupingMethod +import im.vector.app.core.di.ActiveSessionHolder +import im.vector.app.core.platform.EmptyAction +import im.vector.app.core.platform.EmptyViewEvents +import im.vector.app.core.platform.VectorViewModel +import org.matrix.android.sdk.api.query.QueryStringValue +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent +import org.matrix.android.sdk.api.session.room.model.RoomSummary +import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper + +data class ActiveSpaceViewState( + val isInSpaceMode: Boolean = false, + val activeSpaceSummary: RoomSummary? = null, + val canUserManageSpace: Boolean = false +) : MvRxState + +class PromoteRestrictedViewModel @AssistedInject constructor( + @Assisted initialState: ActiveSpaceViewState, + private val activeSessionHolder: ActiveSessionHolder, + appStateHandler: AppStateHandler +) : VectorViewModel(initialState) { + + init { + appStateHandler.selectedRoomGroupingObservable.distinctUntilChanged().execute { state -> + val groupingMethod = state.invoke()?.orNull() + val isSpaceMode = groupingMethod is RoomGroupingMethod.BySpace + val currentSpace = (groupingMethod as? RoomGroupingMethod.BySpace)?.spaceSummary + val canManage = currentSpace?.roomId?.let { roomId -> + activeSessionHolder.getSafeActiveSession() + ?.getRoom(roomId) + ?.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.NoCondition) + ?.content?.toModel()?.let { + PowerLevelsHelper(it).isUserAllowedToSend(activeSessionHolder.getActiveSession().myUserId, true, EventType.STATE_SPACE_CHILD) + } ?: false + } ?: false + + copy( + isInSpaceMode = isSpaceMode, + activeSpaceSummary = currentSpace, + canUserManageSpace = canManage + ) + } + } + + @AssistedFactory + interface Factory { + fun create(initialState: ActiveSpaceViewState): PromoteRestrictedViewModel + } + + companion object : MvRxViewModelFactory { + + @JvmStatic + override fun create(viewModelContext: ViewModelContext, state: ActiveSpaceViewState): PromoteRestrictedViewModel? { + val factory = when (viewModelContext) { + is FragmentViewModelContext -> viewModelContext.fragment as? Factory + is ActivityViewModelContext -> viewModelContext.activity as? Factory + } + return factory?.create(state) ?: error("You should let your activity/fragment implements Factory interface") + } + } + + override fun handle(action: EmptyAction) {} +} diff --git a/vector/src/main/java/im/vector/app/features/home/ShortcutCreator.kt b/vector/src/main/java/im/vector/app/features/home/ShortcutCreator.kt index db396cf990..fc204a0c56 100644 --- a/vector/src/main/java/im/vector/app/features/home/ShortcutCreator.kt +++ b/vector/src/main/java/im/vector/app/features/home/ShortcutCreator.kt @@ -17,7 +17,9 @@ package im.vector.app.features.home import android.content.Context +import android.content.pm.ShortcutInfo import android.graphics.Bitmap +import android.graphics.Canvas import android.os.Build import androidx.annotation.WorkerThread import androidx.core.content.pm.ShortcutInfoCompat @@ -45,7 +47,7 @@ class ShortcutCreator @Inject constructor( private val adaptiveIconOuterSides = dimensionConverter.dpToPx(adaptiveIconOuterSidesDp) private val iconSize by lazy { if (useAdaptiveIcon) { - adaptiveIconSize - adaptiveIconOuterSides + adaptiveIconSize - (adaptiveIconOuterSides * 2) } else { dimensionConverter.dpToPx(72) } @@ -56,27 +58,37 @@ class ShortcutCreator @Inject constructor( } @WorkerThread - fun create(roomSummary: RoomSummary): ShortcutInfoCompat { + fun create(roomSummary: RoomSummary, rank: Int = 1): ShortcutInfoCompat { val intent = RoomDetailActivity.shortcutIntent(context, roomSummary.roomId) val bitmap = try { avatarRenderer.shortcutDrawable(GlideApp.with(context), roomSummary.toMatrixItem(), iconSize) } catch (failure: Throwable) { null } + val categories = if (Build.VERSION.SDK_INT >= 25) { + setOf(directShareCategory, ShortcutInfo.SHORTCUT_CATEGORY_CONVERSATION) + } else { + setOf(directShareCategory) + } + return ShortcutInfoCompat.Builder(context, roomSummary.roomId) .setShortLabel(roomSummary.displayName) .setIcon(bitmap?.toProfileImageIcon()) .setIntent(intent) - - // Make it show up in the direct share menu - .setCategories(setOf(directShareCategory)) + .setLongLived(true) + .setRank(rank) + .setCategories(categories) .build() } private fun Bitmap.toProfileImageIcon(): IconCompat { return if (useAdaptiveIcon) { - IconCompat.createWithAdaptiveBitmap(this) + val insetBmp = Bitmap.createBitmap(adaptiveIconSize, adaptiveIconSize, Bitmap.Config.ARGB_8888) + val canvas = Canvas(insetBmp) + canvas.drawBitmap(this, adaptiveIconOuterSides.toFloat(), adaptiveIconOuterSides.toFloat(), null) + + IconCompat.createWithAdaptiveBitmap(insetBmp) } else { IconCompat.createWithBitmap(this) } diff --git a/vector/src/main/java/im/vector/app/features/home/ShortcutsHandler.kt b/vector/src/main/java/im/vector/app/features/home/ShortcutsHandler.kt index 4a2d001e1d..c3249f5b26 100644 --- a/vector/src/main/java/im/vector/app/features/home/ShortcutsHandler.kt +++ b/vector/src/main/java/im/vector/app/features/home/ShortcutsHandler.kt @@ -24,7 +24,7 @@ import androidx.core.content.pm.ShortcutManagerCompat import im.vector.app.core.di.ActiveSessionHolder import io.reactivex.disposables.Disposable import io.reactivex.disposables.Disposables -import org.matrix.android.sdk.api.query.RoomTagQueryFilter +import org.matrix.android.sdk.api.session.room.RoomSortOrder import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams import org.matrix.android.sdk.rx.asObservable @@ -46,17 +46,25 @@ class ShortcutsHandler @Inject constructor( ?.getPagedRoomSummariesLive( roomSummaryQueryParams { memberships = listOf(Membership.JOIN) - roomTagQueryFilter = RoomTagQueryFilter(isFavorite = true, null, null) - } + }, + sortOrder = RoomSortOrder.PRIORITY_AND_ACTIVITY ) ?.asObservable() ?.subscribe { rooms -> - val shortcuts = rooms - .take(n = 4) // Android only allows us to create 4 shortcuts - .map { shortcutCreator.create(it) } + // Remove dead shortcuts (i.e. deleted rooms) + val roomIds = rooms.map { it.roomId } + val deadShortcutIds = ShortcutManagerCompat.getShortcuts(context, ShortcutManagerCompat.FLAG_MATCH_DYNAMIC) + .map { it.id } + .filter { !roomIds.contains(it) } + ShortcutManagerCompat.removeLongLivedShortcuts(context, deadShortcutIds) - ShortcutManagerCompat.removeAllDynamicShortcuts(context) - ShortcutManagerCompat.addDynamicShortcuts(context, shortcuts) + val shortcuts = rooms.mapIndexed { index, room -> + shortcutCreator.create(room, index) + } + + shortcuts.forEach { shortcut -> + ShortcutManagerCompat.pushDynamicShortcut(context, shortcut) + } } ?: Disposables.empty() } diff --git a/vector/src/main/java/im/vector/app/features/home/UnreadMessagesSharedViewModel.kt b/vector/src/main/java/im/vector/app/features/home/UnreadMessagesSharedViewModel.kt index e9e2447b39..ac983a9f0c 100644 --- a/vector/src/main/java/im/vector/app/features/home/UnreadMessagesSharedViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/UnreadMessagesSharedViewModel.kt @@ -38,6 +38,7 @@ import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.room.RoomSortOrder import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams +import org.matrix.android.sdk.api.session.room.spaceSummaryQueryParams import org.matrix.android.sdk.api.session.room.summary.RoomAggregateNotificationCount import org.matrix.android.sdk.rx.asObservable import java.util.concurrent.TimeUnit @@ -143,11 +144,22 @@ class UnreadMessagesSharedViewModel @AssistedInject constructor(@Assisted initia roomSummaryQueryParams { this.memberships = listOf(Membership.INVITE) } ).size } + + val spaceInviteCount = if (autoAcceptInvites.hideInvites) { + 0 + } else { + session.getRoomSummaries( + spaceSummaryQueryParams { + this.memberships = listOf(Membership.INVITE) + } + ).size + } + val totalCount = session.getNotificationCountForRooms( roomSummaryQueryParams { this.memberships = listOf(Membership.JOIN) this.activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(null).takeIf { - vectorPreferences.labsSpacesOnlyOrphansInHome() + !vectorPreferences.prefSpacesShowAllRoomInHome() } ?: ActiveSpaceFilter.None } ) @@ -161,15 +173,16 @@ class UnreadMessagesSharedViewModel @AssistedInject constructor(@Assisted initia // filter out current selection it.roomId != selectedSpace } + CountInfo( homeCount = counts, otherCount = RoomAggregateNotificationCount( - rootCounts.fold(0, { acc, rs -> - acc + rs.notificationCount - }) + (counts.notificationCount.takeIf { selectedSpace != null } ?: 0), - rootCounts.fold(0, { acc, rs -> - acc + rs.highlightCount - }) + (counts.highlightCount.takeIf { selectedSpace != null } ?: 0) + notificationCount = rootCounts.fold(0, { acc, rs -> acc + rs.notificationCount }) + + (counts.notificationCount.takeIf { selectedSpace != null } ?: 0) + + spaceInviteCount, + highlightCount = rootCounts.fold(0, { acc, rs -> acc + rs.highlightCount }) + + (counts.highlightCount.takeIf { selectedSpace != null } ?: 0) + + spaceInviteCount ) ) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt index 6b031159b8..9bb82cdc27 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt @@ -19,6 +19,7 @@ package im.vector.app.features.home.room.detail import android.net.Uri import android.view.View import im.vector.app.core.platform.VectorViewModelAction +import im.vector.app.features.call.conference.ConferenceEvent import org.matrix.android.sdk.api.session.content.ContentAttachmentData import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent @@ -89,9 +90,14 @@ sealed class RoomDetailAction : VectorViewModelAction { object ManageIntegrations : RoomDetailAction() data class AddJitsiWidget(val withVideo: Boolean) : RoomDetailAction() data class RemoveWidget(val widgetId: String) : RoomDetailAction() + + object JoinJitsiCall: RoomDetailAction() + object LeaveJitsiCall: RoomDetailAction() + data class EnsureNativeWidgetAllowed(val widget: Widget, val userJustAccepted: Boolean, val grantedEvents: RoomDetailViewEvents) : RoomDetailAction() + data class UpdateJoinJitsiCallStatus(val conferenceEvent: ConferenceEvent): RoomDetailAction() data class OpenOrCreateDm(val userId: String) : RoomDetailAction() data class JumpToReadReceipt(val userId: String) : RoomDetailAction() diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt index 057b4f2703..c6eda584ad 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt @@ -86,12 +86,13 @@ import im.vector.app.core.hardware.vibrate import im.vector.app.core.intent.getFilenameFromUri import im.vector.app.core.intent.getMimeTypeFromUri import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.core.platform.lifecycleAwareLazy import im.vector.app.core.platform.showOptimizedSnackbar import im.vector.app.core.resources.ColorProvider -import im.vector.app.core.ui.views.ActiveConferenceView import im.vector.app.core.ui.views.CurrentCallsView +import im.vector.app.core.ui.views.CurrentCallsViewPresenter import im.vector.app.core.ui.views.FailedMessagesWarningView -import im.vector.app.core.ui.views.KnownCallsViewHolder +import im.vector.app.core.ui.views.JoinConferenceView import im.vector.app.core.ui.views.NotificationAreaView import im.vector.app.core.utils.Debouncer import im.vector.app.core.utils.DimensionConverter @@ -123,6 +124,9 @@ import im.vector.app.features.attachments.preview.AttachmentsPreviewArgs import im.vector.app.features.attachments.toGroupedContentAttachmentData import im.vector.app.features.call.SharedKnownCallsViewModel import im.vector.app.features.call.VectorCallActivity +import im.vector.app.features.call.conference.ConferenceEvent +import im.vector.app.features.call.conference.ConferenceEventEmitter +import im.vector.app.features.call.conference.ConferenceEventObserver import im.vector.app.features.call.conference.JitsiCallViewModel import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.command.Command @@ -150,6 +154,7 @@ import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData import im.vector.app.features.home.room.detail.timeline.reactions.ViewReactionsBottomSheet import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever import im.vector.app.features.home.room.detail.upgrade.MigrateRoomBottomSheet +import im.vector.app.features.home.room.detail.views.RoomDetailLazyLoadedViews import im.vector.app.features.home.room.detail.widget.RoomWidgetsBottomSheet import im.vector.app.features.html.EventHtmlRenderer import im.vector.app.features.html.PillImageSpan @@ -307,9 +312,12 @@ class RoomDetailFragment @Inject constructor( private lateinit var attachmentTypeSelector: AttachmentTypeSelectorView private var lockSendButton = false - private val knownCallsViewHolder = KnownCallsViewHolder() + private val currentCallsViewPresenter = CurrentCallsViewPresenter() - private lateinit var emojiPopup: EmojiPopup + private val lazyLoadedViews = RoomDetailLazyLoadedViews() + private val emojiPopup: EmojiPopup by lifecycleAwareLazy { + createEmojiPopup() + } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -321,6 +329,7 @@ class RoomDetailFragment @Inject constructor( } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + lifecycle.addObserver(ConferenceEventObserver(vectorBaseActivity, this::onBroadcastJitsiEvent)) super.onViewCreated(view, savedInstanceState) sharedActionViewModel = activityViewModelProvider.get(MessageSharedActionViewModel::class.java) knownCallsViewModel = activityViewModelProvider.get(SharedKnownCallsViewModel::class.java) @@ -336,17 +345,16 @@ class RoomDetailFragment @Inject constructor( onTapToReturnToCall = ::onTapToReturnToCall ) keyboardStateUtils = KeyboardStateUtils(requireActivity()) + lazyLoadedViews.bind(views) setupToolbar(views.roomToolbar) setupRecyclerView() setupComposer() - setupInviteView() setupNotificationView() setupJumpToReadMarkerView() setupActiveCallView() setupJumpToBottomView() - setupConfBannerView() - setupEmojiPopup() - setupFailedMessagesWarningView() + setupEmojiButton() + setupRemoveJitsiWidgetView() setupVoiceMessageView() views.roomToolbarContentView.debouncedClicks { @@ -362,10 +370,10 @@ class RoomDetailFragment @Inject constructor( knownCallsViewModel .liveKnownCalls - .observe(viewLifecycleOwner, { - knownCallsViewHolder.updateCall(callManager.getCurrentCall(), it) + .observe(viewLifecycleOwner) { + currentCallsViewPresenter.updateCall(callManager.getCurrentCall(), it) invalidateOptionsMenu() - }) + } roomDetailViewModel.selectSubscribe(RoomDetailViewState::canShowJumpToReadMarker, RoomDetailViewState::unreadState) { _, _ -> updateJumpToReadMarkerViewVisibility() @@ -383,8 +391,17 @@ class RoomDetailFragment @Inject constructor( } } - roomDetailViewModel.selectSubscribe(RoomDetailViewState::syncState) { syncState -> - views.syncStateView.render(syncState) + roomDetailViewModel.selectSubscribe( + RoomDetailViewState::syncState, + RoomDetailViewState::incrementalSyncStatus, + RoomDetailViewState::pushCounter + ) { syncState, incrementalSyncStatus, pushCounter -> + views.syncStateView.render( + syncState, + incrementalSyncStatus, + pushCounter, + vectorPreferences.developerShowDebugInfo() + ) } roomDetailViewModel.observeViewEvents { @@ -412,6 +429,7 @@ class RoomDetailFragment @Inject constructor( RoomDetailViewEvents.OpenActiveWidgetBottomSheet -> onViewWidgetsClicked() is RoomDetailViewEvents.ShowInfoOkDialog -> showDialogWithMessage(it.message) is RoomDetailViewEvents.JoinJitsiConference -> joinJitsiRoom(it.widget, it.withVideo) + RoomDetailViewEvents.LeaveJitsiConference -> leaveJitsiConference() RoomDetailViewEvents.ShowWaitingView -> vectorBaseActivity.showWaitingView() RoomDetailViewEvents.HideWaitingView -> vectorBaseActivity.hideWaitingView() is RoomDetailViewEvents.RequestNativeWidgetPermission -> requestNativeWidgetPermission(it) @@ -436,6 +454,26 @@ class RoomDetailFragment @Inject constructor( } } + private fun setupRemoveJitsiWidgetView() { + views.removeJitsiWidgetView.onCompleteSliding = { + withState(roomDetailViewModel) { + val jitsiWidgetId = it.jitsiState.widgetId ?: return@withState + if (it.jitsiState.hasJoined) { + leaveJitsiConference() + } + roomDetailViewModel.handle(RoomDetailAction.RemoveWidget(jitsiWidgetId)) + } + } + } + + private fun leaveJitsiConference() { + ConferenceEventEmitter(vectorBaseActivity).emitConferenceEnded() + } + + private fun onBroadcastJitsiEvent(conferenceEvent: ConferenceEvent) { + roomDetailViewModel.handle(RoomDetailAction.UpdateJoinJitsiCallStatus(conferenceEvent)) + } + private fun onCannotRecord() { // Update the UI, cancel the animation views.voiceMessageRecorderView.initVoiceRecordingViews() @@ -559,33 +597,14 @@ class RoomDetailFragment @Inject constructor( ) } - private fun setupConfBannerView() { - views.activeConferenceView.callback = object : ActiveConferenceView.Callback { - override fun onTapJoinAudio(jitsiWidget: Widget) { - // need to check if allowed first - roomDetailViewModel.handle(RoomDetailAction.EnsureNativeWidgetAllowed( - widget = jitsiWidget, - userJustAccepted = false, - grantedEvents = RoomDetailViewEvents.JoinJitsiConference(jitsiWidget, false)) - ) - } - - override fun onTapJoinVideo(jitsiWidget: Widget) { - roomDetailViewModel.handle(RoomDetailAction.EnsureNativeWidgetAllowed( - widget = jitsiWidget, - userJustAccepted = false, - grantedEvents = RoomDetailViewEvents.JoinJitsiConference(jitsiWidget, true)) - ) - } - - override fun onDelete(jitsiWidget: Widget) { - roomDetailViewModel.handle(RoomDetailAction.RemoveWidget(jitsiWidget.widgetId)) - } + private fun setupEmojiButton() { + views.composerLayout.views.composerEmojiButton.debouncedClicks { + emojiPopup.toggle() } } - private fun setupEmojiPopup() { - emojiPopup = EmojiPopup + private fun createEmojiPopup(): EmojiPopup { + return EmojiPopup .Builder .fromRootView(views.rootConstraintLayout) .setKeyboardAnimationStyle(R.style.emoji_fade_animation_style) @@ -602,14 +621,18 @@ class RoomDetailFragment @Inject constructor( } } .build(views.composerLayout.views.composerEditText) + } - views.composerLayout.views.composerEmojiButton.debouncedClicks { - emojiPopup.toggle() + private val permissionVoiceMessageLauncher = registerForPermissionsResult { allGranted, deniedPermanently -> + if (allGranted) { + // In this case, let the user start again the gesture + } else if (deniedPermanently) { + vectorBaseActivity.onPermissionDeniedSnackbar(R.string.denied_permission_voice_message) } } - private fun setupFailedMessagesWarningView() { - views.failedMessagesWarningView.callback = object : FailedMessagesWarningView.Callback { + private fun createFailedMessagesWarningCallback(): FailedMessagesWarningView.Callback { + return object : FailedMessagesWarningView.Callback { override fun onDeleteAllClicked() { MaterialAlertDialogBuilder(requireContext()) .setTitle(R.string.event_status_delete_all_failed_dialog_title) @@ -627,14 +650,6 @@ class RoomDetailFragment @Inject constructor( } } - private val permissionVoiceMessageLauncher = registerForPermissionsResult { allGranted, deniedPermanently -> - if (allGranted) { - // In this case, let the user start again the gesture - } else if (deniedPermanently) { - vectorBaseActivity.onPermissionDeniedSnackbar(R.string.denied_permission_voice_message) - } - } - private fun setupVoiceMessageView() { views.voiceMessageRecorderView.voiceMessagePlaybackTracker = voiceMessagePlaybackTracker @@ -767,20 +782,18 @@ class RoomDetailFragment @Inject constructor( } override fun onDestroyView() { + lazyLoadedViews.unBind() timelineEventController.callback = null timelineEventController.removeModelBuildListener(modelBuildListener) - views.activeCallView.callback = null + currentCallsViewPresenter.unBind() modelBuildListener = null autoCompleter.clear() debouncer.cancelAll() views.timelineRecyclerView.cleanup() - emojiPopup.dismiss() - super.onDestroyView() } override fun onDestroy() { - knownCallsViewHolder.unBind() roomDetailViewModel.handle(RoomDetailAction.ExitTrackingUnreadMessagesState) super.onDestroy() } @@ -816,12 +829,7 @@ class RoomDetailFragment @Inject constructor( } private fun setupActiveCallView() { - knownCallsViewHolder.bind( - views.activeCallPiP, - views.activeCallView, - views.activeCallPiPWrap, - this - ) + currentCallsViewPresenter.bind(views.currentCallsView, this) } private fun navigateToEvent(action: RoomDetailViewEvents.NavigateToEvent) { @@ -872,6 +880,10 @@ class RoomDetailFragment @Inject constructor( onOptionsItemSelected(menuItem) } } + val joinConfItem = menu.findItem(R.id.join_conference) + (joinConfItem.actionView as? JoinConferenceView)?.onJoinClicked = { + roomDetailViewModel.handle(RoomDetailAction.JoinJitsiCall) + } } override fun onPrepareOptionsMenu(menu: Menu) { @@ -880,7 +892,8 @@ class RoomDetailFragment @Inject constructor( } withState(roomDetailViewModel) { state -> // Set the visual state of the call buttons (voice/video) to enabled/disabled according to user permissions - val callButtonsEnabled = when (state.asyncRoomSummary.invoke()?.joinedMembersCount) { + val hasCallInRoom = callManager.getCallsByRoomId(state.roomId).isNotEmpty() || state.jitsiState.hasJoined + val callButtonsEnabled = !hasCallInRoom && when (state.asyncRoomSummary.invoke()?.joinedMembersCount) { 1 -> false 2 -> state.isAllowedToStartWebRTCCall else -> state.isAllowedToManageWidgets @@ -891,14 +904,8 @@ class RoomDetailFragment @Inject constructor( val matrixAppsMenuItem = menu.findItem(R.id.open_matrix_apps) val widgetsCount = state.activeRoomWidgets.invoke()?.size ?: 0 - if (widgetsCount > 0) { - val actionView = matrixAppsMenuItem.actionView - actionView - .findViewById(R.id.action_view_icon_image) - .setColorFilter(colorProvider.getColorFromAttribute(R.attr.colorPrimary)) - actionView.findViewById(R.id.cart_badge).setTextOrHide("$widgetsCount") - matrixAppsMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS) - } else { + val hasOnlyJitsiWidget = widgetsCount == 1 && state.hasActiveJitsiWidget() + if (widgetsCount == 0 || hasOnlyJitsiWidget) { // icon should be default color no badge val actionView = matrixAppsMenuItem.actionView actionView @@ -906,6 +913,13 @@ class RoomDetailFragment @Inject constructor( .setColorFilter(ThemeUtils.getColor(requireContext(), R.attr.vctr_content_secondary)) actionView.findViewById(R.id.cart_badge).isVisible = false matrixAppsMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER) + } else { + val actionView = matrixAppsMenuItem.actionView + actionView + .findViewById(R.id.action_view_icon_image) + .setColorFilter(colorProvider.getColorFromAttribute(R.attr.colorPrimary)) + actionView.findViewById(R.id.cart_badge).setTextOrHide("$widgetsCount") + matrixAppsMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS) } } } @@ -932,10 +946,6 @@ class RoomDetailFragment @Inject constructor( callActionsHandler.onVideoCallClicked() true } - R.id.hangup_call -> { - roomDetailViewModel.handle(RoomDetailAction.EndCall) - true - } R.id.search -> { handleSearchAction() true @@ -1354,22 +1364,22 @@ class RoomDetailFragment @Inject constructor( return isHandled } - private fun setupInviteView() { - views.inviteView.callback = this - } - override fun invalidate() = withState(roomDetailViewModel) { state -> invalidateOptionsMenu() val summary = state.asyncRoomSummary() renderToolbar(summary, state.typingMessage) - views.activeConferenceView.render(state) - views.failedMessagesWarningView.render(state.hasFailedSending) + views.removeJitsiWidgetView.render(state) + if (state.hasFailedSending) { + lazyLoadedViews.failedMessagesWarningView(inflateIfNeeded = true, createFailedMessagesWarningCallback())?.isVisible = true + } else { + lazyLoadedViews.failedMessagesWarningView(inflateIfNeeded = false)?.isVisible = false + } val inviter = state.asyncInviter() if (summary?.membership == Membership.JOIN) { views.jumpToBottomView.count = summary.notificationCount views.jumpToBottomView.drawBadge = summary.hasUnreadMessages timelineEventController.update(state) - views.inviteView.isVisible = false + lazyLoadedViews.inviteView(false)?.isVisible = false if (state.tombstoneEvent == null) { if (state.canSendMessage) { if (!views.voiceMessageRecorderView.isActive()) { @@ -1390,10 +1400,15 @@ class RoomDetailFragment @Inject constructor( views.notificationAreaView.render(NotificationAreaView.State.Tombstone(state.tombstoneEvent)) } } else if (summary?.membership == Membership.INVITE && inviter != null) { - views.inviteView.isVisible = true - views.inviteView.render(inviter, VectorInviteView.Mode.LARGE, state.changeMembershipState) - // Intercept click event - views.inviteView.setOnClickListener { } + views.composerLayout.isVisible = false + views.voiceMessageRecorderView.isVisible = false + lazyLoadedViews.inviteView(true)?.apply { + callback = this@RoomDetailFragment + isVisible = true + render(inviter, VectorInviteView.Mode.LARGE, state.changeMembershipState) + setOnClickListener { } + } + Unit } else if (state.asyncInviter.complete) { vectorBaseActivity.finish() } @@ -1717,7 +1732,7 @@ class RoomDetailFragment @Inject constructor( override fun onEventLongClicked(informationData: MessageInformationData, messageContent: Any?, view: View): Boolean { view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) - val roomId = roomDetailViewModel.timeline.getTimelineEventWithId(informationData.eventId)?.roomId ?: return false + val roomId = roomDetailArgs.roomId this.view?.hideKeyboard() MessageActionsBottomSheet diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt index d62c5f6003..2802ee2f83 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt @@ -45,6 +45,7 @@ sealed class RoomDetailViewEvents : VectorViewEvents { data class NavigateToEvent(val eventId: String) : RoomDetailViewEvents() data class JoinJitsiConference(val widget: Widget, val withVideo: Boolean) : RoomDetailViewEvents() + object LeaveJitsiConference : RoomDetailViewEvents() object OpenInvitePeople : RoomDetailViewEvents() object OpenSetRoomAvatarDialog : RoomDetailViewEvents() diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt index 0ac638034d..cacf9b8902 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt @@ -39,6 +39,8 @@ import im.vector.app.core.mvrx.runCatchingToAsync import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider import im.vector.app.features.attachments.toContentAttachmentData +import im.vector.app.features.call.conference.ConferenceEvent +import im.vector.app.features.call.conference.JitsiActiveConferenceHolder import im.vector.app.features.call.conference.JitsiService import im.vector.app.features.call.lookup.CallProtocolsChecker import im.vector.app.features.call.webrtc.WebRtcCallManager @@ -51,17 +53,18 @@ import im.vector.app.features.home.room.detail.composer.VoiceMessageHelper import im.vector.app.features.home.room.detail.composer.rainbow.RainbowGenerator import im.vector.app.features.home.room.detail.sticker.StickerPickerActionHandler import im.vector.app.features.home.room.detail.timeline.factory.TimelineFactory -import im.vector.app.features.home.room.detail.timeline.helper.RoomSummariesHolder import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever import im.vector.app.features.home.room.typing.TypingHelper import im.vector.app.features.powerlevel.PowerLevelsObservableFactory import im.vector.app.features.session.coroutineScope +import im.vector.app.features.settings.VectorDataStore import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.voice.VoicePlayerHelper import io.reactivex.Observable import io.reactivex.rxkotlin.subscribeBy import io.reactivex.schedulers.Schedulers import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.commonmark.parser.Parser @@ -79,6 +82,7 @@ import org.matrix.android.sdk.api.session.events.model.isTextMessage import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.file.FileService +import org.matrix.android.sdk.api.session.initsync.SyncStatusService import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState import org.matrix.android.sdk.api.session.room.members.roomMemberQueryParams import org.matrix.android.sdk.api.session.room.model.Membership @@ -101,6 +105,7 @@ import org.matrix.android.sdk.api.session.space.CreateSpaceParams import org.matrix.android.sdk.api.session.widgets.model.WidgetType import org.matrix.android.sdk.api.util.toOptional import org.matrix.android.sdk.internal.crypto.model.event.WithHeldCode +import org.matrix.android.sdk.rx.asObservable import org.matrix.android.sdk.rx.rx import org.matrix.android.sdk.rx.unwrap import timber.log.Timber @@ -110,17 +115,18 @@ import java.util.concurrent.atomic.AtomicBoolean class RoomDetailViewModel @AssistedInject constructor( @Assisted private val initialState: RoomDetailViewState, private val vectorPreferences: VectorPreferences, + private val vectorDataStore: VectorDataStore, private val stringProvider: StringProvider, private val rainbowGenerator: RainbowGenerator, private val session: Session, private val supportedVerificationMethodsProvider: SupportedVerificationMethodsProvider, private val stickerPickerActionHandler: StickerPickerActionHandler, - private val roomSummariesHolder: RoomSummariesHolder, private val typingHelper: TypingHelper, private val callManager: WebRtcCallManager, private val chatEffectManager: ChatEffectManager, private val directRoomHelper: DirectRoomHelper, private val jitsiService: JitsiService, + private val activeConferenceHolder: JitsiActiveConferenceHolder, private val voiceMessageHelper: VoiceMessageHelper, private val voicePlayerHelper: VoicePlayerHelper, timelineFactory: TimelineFactory @@ -173,6 +179,7 @@ class RoomDetailViewModel @AssistedInject constructor( observeSummaryState() getUnreadState() observeSyncState() + observeDataStore() observeEventDisplayedActions() loadDraftIfAny() observeUnreadState() @@ -197,6 +204,18 @@ class RoomDetailViewModel @AssistedInject constructor( } } + private fun observeDataStore() { + viewModelScope.launch { + vectorDataStore.pushCounterFlow.collect { nbOfPush -> + setState { + copy( + pushCounter = nbOfPush + ) + } + } + } + } + private fun prepareForEncryption() { // check if there is not already a call made, or if there has been an error if (prepareToEncrypt.shouldLoad) { @@ -241,9 +260,25 @@ class RoomDetailViewModel @AssistedInject constructor( .map { widgets -> widgets.filter { it.isActive } } - .execute { - copy(activeRoomWidgets = it) + .execute { widgets -> + copy(activeRoomWidgets = widgets) } + + asyncSubscribe(RoomDetailViewState::activeRoomWidgets) { widgets -> + setState { + val jitsiWidget = widgets.firstOrNull { it.type == WidgetType.Jitsi } + val jitsiConfId = jitsiWidget?.let { + jitsiService.extractJitsiWidgetData(it)?.confId + } + copy( + jitsiState = jitsiState.copy( + confId = jitsiConfId, + widgetId = jitsiWidget?.widgetId, + hasJoined = activeConferenceHolder.isJoined(jitsiConfId) + ) + ) + } + } } private fun observeMyRoomMember() { @@ -308,6 +343,9 @@ class RoomDetailViewModel @AssistedInject constructor( is RoomDetailAction.EndCall -> handleEndCall() is RoomDetailAction.ManageIntegrations -> handleManageIntegrations() is RoomDetailAction.AddJitsiWidget -> handleAddJitsiConference(action) + is RoomDetailAction.UpdateJoinJitsiCallStatus -> handleJitsiCallJoinStatus(action) + is RoomDetailAction.JoinJitsiCall -> handleJoinJitsiCall() + is RoomDetailAction.LeaveJitsiCall -> handleLeaveJitsiCall() is RoomDetailAction.RemoveWidget -> handleDeleteWidget(action.widgetId) is RoomDetailAction.EnsureNativeWidgetAllowed -> handleCheckWidgetAllowed(action) is RoomDetailAction.CancelSend -> handleCancel(action) @@ -340,6 +378,33 @@ class RoomDetailViewModel @AssistedInject constructor( }.exhaustive } + private fun handleJitsiCallJoinStatus(action: RoomDetailAction.UpdateJoinJitsiCallStatus) = withState { state -> + if (state.jitsiState.confId == null) { + // If jitsi widget is removed while on the call + if (state.jitsiState.hasJoined) { + setState { copy(jitsiState = jitsiState.copy(hasJoined = false)) } + } + return@withState + } + when (action.conferenceEvent) { + is ConferenceEvent.Joined, + is ConferenceEvent.Terminated -> { + setState { copy(jitsiState = jitsiState.copy(hasJoined = activeConferenceHolder.isJoined(jitsiState.confId))) } + } + else -> Unit + } + } + + private fun handleLeaveJitsiCall() { + _viewEvents.post(RoomDetailViewEvents.LeaveJitsiConference) + } + + private fun handleJoinJitsiCall() = withState { state -> + val jitsiWidget = state.activeRoomWidgets()?.firstOrNull { it.widgetId == state.jitsiState.widgetId } ?: return@withState + val action = RoomDetailAction.EnsureNativeWidgetAllowed(jitsiWidget, false, RoomDetailViewEvents.JoinJitsiConference(jitsiWidget, true)) + handleCheckWidgetAllowed(action) + } + private fun handleAcceptCall(action: RoomDetailAction.AcceptCall) { callManager.getCallById(action.callId)?.also { _viewEvents.post(RoomDetailViewEvents.DisplayAndAcceptCall(it)) @@ -448,10 +513,15 @@ class RoomDetailViewModel @AssistedInject constructor( } } - private fun handleDeleteWidget(widgetId: String) { - _viewEvents.post(RoomDetailViewEvents.ShowWaitingView) + private fun handleDeleteWidget(widgetId: String) = withState { state -> + val isJitsiWidget = state.jitsiState.widgetId == widgetId viewModelScope.launch(Dispatchers.IO) { try { + if (isJitsiWidget) { + setState { copy(jitsiState = jitsiState.copy(deleteWidgetInProgress = true)) } + } else { + _viewEvents.post(RoomDetailViewEvents.ShowWaitingView) + } session.widgetService().destroyRoomWidget(room.roomId, widgetId) // local echo setState { @@ -467,7 +537,11 @@ class RoomDetailViewModel @AssistedInject constructor( } catch (failure: Throwable) { _viewEvents.post(RoomDetailViewEvents.ShowMessage(stringProvider.getString(R.string.failed_to_remove_widget))) } finally { - _viewEvents.post(RoomDetailViewEvents.HideWaitingView) + if (isJitsiWidget) { + setState { copy(jitsiState = jitsiState.copy(deleteWidgetInProgress = false)) } + } else { + _viewEvents.post(RoomDetailViewEvents.HideWaitingView) + } } } } @@ -682,9 +756,10 @@ class RoomDetailViewModel @AssistedInject constructor( R.id.timeline_setting -> true R.id.invite -> state.canInvite R.id.open_matrix_apps -> true - R.id.voice_call, - R.id.video_call -> callManager.getCallsByRoomId(state.roomId).isEmpty() - R.id.hangup_call -> callManager.getCallsByRoomId(state.roomId).isNotEmpty() + R.id.voice_call -> state.isWebRTCCallOptionAvailable() + R.id.video_call -> state.isWebRTCCallOptionAvailable() || state.jitsiState.confId == null || state.jitsiState.hasJoined + // Show Join conference button only if there is an active conf id not joined. Otherwise fallback to default video disabled. ^ + R.id.join_conference -> !state.isWebRTCCallOptionAvailable() && state.jitsiState.confId != null && !state.jitsiState.hasJoined R.id.search -> true R.id.dev_tools -> vectorPreferences.developerMode() else -> false @@ -1436,6 +1511,17 @@ class RoomDetailViewModel @AssistedInject constructor( } } .disposeOnClear() + + session.getSyncStatusLive() + .asObservable() + .subscribe { it -> + if (it is SyncStatusService.Status.IncrementalSyncStatus) { + setState { + copy(incrementalSyncStatus = it) + } + } + } + .disposeOnClear() } private fun observeRoomSummary() { @@ -1515,7 +1601,6 @@ class RoomDetailViewModel @AssistedInject constructor( private fun observeSummaryState() { asyncSubscribe(RoomDetailViewState::asyncRoomSummary) { summary -> - roomSummariesHolder.set(summary) setState { val typingMessage = typingHelper.getTypingMessage(summary.typingUsers) copy( @@ -1563,7 +1648,6 @@ class RoomDetailViewModel @AssistedInject constructor( } override fun onCleared() { - roomSummariesHolder.remove(room.roomId) timeline.dispose() timeline.removeAllListeners() if (vectorPreferences.sendTypingNotifs()) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt index d10456b7c2..8f4ad97b72 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt @@ -19,13 +19,16 @@ package im.vector.app.features.home.room.detail import com.airbnb.mvrx.Async import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.Uninitialized +import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.initsync.SyncStatusService import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.sync.SyncState import org.matrix.android.sdk.api.session.widgets.model.Widget +import org.matrix.android.sdk.api.session.widgets.model.WidgetType /** * Describes the current send mode: @@ -55,6 +58,14 @@ sealed class UnreadState { data class HasUnread(val firstUnreadEventId: String) : UnreadState() } +data class JitsiState( + val hasJoined: Boolean = false, + // Not null if we have an active jitsi widget on the room + val confId: String? = null, + val widgetId: String? = null, + val deleteWidgetInProgress: Boolean = false +) + data class RoomDetailViewState( val roomId: String, val eventId: String?, @@ -67,6 +78,8 @@ data class RoomDetailViewState( val tombstoneEvent: Event? = null, val joinUpgradedRoomAsync: Async = Uninitialized, val syncState: SyncState = SyncState.Idle, + val incrementalSyncStatus: SyncStatusService.Status.IncrementalSyncStatus = SyncStatusService.Status.IncrementalSyncIdle, + val pushCounter: Int = 0, val highlightedEventId: String? = null, val unreadState: UnreadState = UnreadState.Unknown, val canShowJumpToReadMarker: Boolean = true, @@ -75,7 +88,8 @@ data class RoomDetailViewState( val canInvite: Boolean = true, val isAllowedToManageWidgets: Boolean = false, val isAllowedToStartWebRTCCall: Boolean = true, - val hasFailedSending: Boolean = false + val hasFailedSending: Boolean = false, + val jitsiState: JitsiState = JitsiState() ) : MvRxState { constructor(args: RoomDetailArgs) : this( @@ -85,5 +99,11 @@ data class RoomDetailViewState( highlightedEventId = args.eventId ) + fun isWebRTCCallOptionAvailable() = (asyncRoomSummary.invoke()?.joinedMembersCount ?: 0) <= 2 + + // This checks directly on the active room widgets. + // It can differs for a short period of time on the JitsiState as its computed async. + fun hasActiveJitsiWidget() = activeRoomWidgets()?.any { it.type == WidgetType.Jitsi && it.isActive }.orFalse() + fun isDm() = asyncRoomSummary()?.isDirect == true } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/StartCallActionsHandler.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/StartCallActionsHandler.kt index b71b90ace3..92a75b449a 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/StartCallActionsHandler.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/StartCallActionsHandler.kt @@ -26,7 +26,6 @@ import im.vector.app.core.utils.PERMISSIONS_FOR_VIDEO_IP_CALL import im.vector.app.core.utils.checkPermissions import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.settings.VectorPreferences -import org.matrix.android.sdk.api.session.widgets.model.WidgetType class StartCallActionsHandler( private val roomId: String, @@ -36,7 +35,7 @@ class StartCallActionsHandler( private val roomDetailViewModel: RoomDetailViewModel, private val startCallActivityResultLauncher: ActivityResultLauncher>, private val showDialogWithMessage: (String) -> Unit, - private val onTapToReturnToCall: () -> Unit) { + private val onTapToReturnToCall: () -> Unit) { fun onVideoCallClicked() { handleCallRequest(true) @@ -61,16 +60,8 @@ class StartCallActionsHandler( } 2 -> { val currentCall = callManager.getCurrentCall() - if (currentCall != null) { - // resume existing if same room, if not prompt to kill and then restart new call? - if (currentCall.signalingRoomId == roomId) { - onTapToReturnToCall() - } - // else { - // TODO might not work well, and should prompt - // webRtcPeerConnectionManager.endCall() - // safeStartCall(it, isVideoCall) - // } + if (currentCall?.signalingRoomId == roomId) { + onTapToReturnToCall() } else if (!state.isAllowedToStartWebRTCCall) { showDialogWithMessage(fragment.getString( if (state.isDm()) { @@ -96,9 +87,8 @@ class StartCallActionsHandler( } )) } else { - if (state.activeRoomWidgets()?.filter { it.type == WidgetType.Jitsi }?.any() == true) { - // A conference is already in progress! - showDialogWithMessage(fragment.getString(R.string.conference_call_in_progress)) + if (state.hasActiveJitsiWidget()) { + // A conference is already in progress, return } else { MaterialAlertDialogBuilder(fragment.requireContext()) .setTitle(if (isVideoCall) R.string.video_meeting else R.string.audio_meeting) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageRecorderView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageRecorderView.kt index 47e72b46f7..1749c955c9 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageRecorderView.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageRecorderView.kt @@ -27,6 +27,9 @@ import androidx.core.view.isVisible import androidx.core.view.updateLayoutParams import im.vector.app.BuildConfig import im.vector.app.R +import im.vector.app.core.extensions.setAttributeBackground +import im.vector.app.core.extensions.setAttributeTintedBackground +import im.vector.app.core.extensions.setAttributeTintedImageResource import im.vector.app.core.hardware.vibrate import im.vector.app.core.utils.CountUpTimer import im.vector.app.core.utils.DimensionConverter @@ -40,11 +43,7 @@ import kotlin.math.floor /** * Encapsulates the voice message recording view and animations. */ -class VoiceMessageRecorderView @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0 -) : ConstraintLayout(context, attrs, defStyleAttr), VoiceMessagePlaybackTracker.Listener { +class VoiceMessageRecorderView: ConstraintLayout, VoiceMessagePlaybackTracker.Listener { interface Callback { // Return true if the recording is started @@ -54,7 +53,7 @@ class VoiceMessageRecorderView @JvmOverloads constructor( fun onVoicePlaybackButtonClicked() } - private val views: ViewVoiceMessageRecorderBinding + private lateinit var views: ViewVoiceMessageRecorderBinding var callback: Callback? = null var voiceMessagePlaybackTracker: VoiceMessagePlaybackTracker? = null @@ -80,7 +79,17 @@ class VoiceMessageRecorderView @JvmOverloads constructor( private val distanceToCancel = dimensionConverter.dpToPx(120).toFloat() private val rtlXMultiplier = context.resources.getInteger(R.integer.rtl_x_multiplier) - init { + // Don't convert to primary constructor. + // We need to define views as lateinit var to be able to check if initialized for the bug fix on api 21 and 22. + @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 + ) : super(context, attrs, defStyleAttr) { + initialize() + } + + fun initialize() { inflate(context, R.layout.view_voice_message_recorder, this) views = ViewVoiceMessageRecorderBinding.bind(this) @@ -90,6 +99,9 @@ class VoiceMessageRecorderView @JvmOverloads constructor( override fun onVisibilityChanged(changedView: View, visibility: Int) { super.onVisibilityChanged(changedView, visibility) + // onVisibilityChanged is called by constructor on api 21 and 22. + if (!this::views.isInitialized) return + if (changedView == this && visibility == VISIBLE) { views.voiceMessageMicButton.contentDescription = context.getString(R.string.a11y_start_voice_message) } else { @@ -208,7 +220,7 @@ class VoiceMessageRecorderView @JvmOverloads constructor( views.voiceMessageLockArrow.translationY = 0F } RecordingState.LOCKING -> { - views.voiceMessageLockImage.setImageResource(R.drawable.ic_voice_message_locked) + views.voiceMessageLockImage.setAttributeTintedImageResource(R.drawable.ic_voice_message_locked, R.attr.colorPrimary) val translationAmount = -distanceY.coerceIn(0F, distanceToLock) views.voiceMessageMicButton.translationY = translationAmount views.voiceMessageLockArrow.translationY = translationAmount @@ -357,6 +369,7 @@ class VoiceMessageRecorderView @JvmOverloads constructor( private fun showRecordingViews() { views.voiceMessageMicButton.setImageResource(R.drawable.ic_voice_mic_recording) + views.voiceMessageMicButton.setAttributeTintedBackground(R.drawable.circle_with_halo, R.attr.colorPrimary) views.voiceMessageMicButton.updateLayoutParams { setMargins(0, 0, 0, 0) } @@ -434,6 +447,7 @@ class VoiceMessageRecorderView @JvmOverloads constructor( private fun resetMicButtonUi() { views.voiceMessageMicButton.isVisible = true views.voiceMessageMicButton.setImageResource(R.drawable.ic_voice_mic) + views.voiceMessageMicButton.setAttributeBackground(android.R.attr.selectableItemBackgroundBorderless) views.voiceMessageMicButton.updateLayoutParams { if (rtlXMultiplier == -1) { // RTL diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt index e1dae11c1c..39b3cd5061 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt @@ -31,8 +31,7 @@ import im.vector.app.core.epoxy.LoadingItem_ import im.vector.app.core.extensions.localDateTime import im.vector.app.core.extensions.nextOrNull import im.vector.app.core.extensions.prevOrNull -import im.vector.app.core.resources.UserPreferencesProvider -import im.vector.app.features.call.webrtc.WebRtcCallManager +import im.vector.app.features.home.room.detail.JitsiState import im.vector.app.features.home.room.detail.RoomDetailAction import im.vector.app.features.home.room.detail.RoomDetailViewState import im.vector.app.features.home.room.detail.UnreadState @@ -46,15 +45,15 @@ import im.vector.app.features.home.room.detail.timeline.helper.TimelineControlle import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventDiffUtilCallback import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventVisibilityHelper import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventVisibilityStateChangedListener +import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventsGroups import im.vector.app.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider -import im.vector.app.features.home.room.detail.timeline.item.AbsMessageItem import im.vector.app.features.home.room.detail.timeline.item.BasedMergedItem import im.vector.app.features.home.room.detail.timeline.item.DaySeparatorItem import im.vector.app.features.home.room.detail.timeline.item.DaySeparatorItem_ +import im.vector.app.features.home.room.detail.timeline.item.ItemWithEvents import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptsItem -import im.vector.app.features.home.room.detail.timeline.item.SendStateDecoration import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever import im.vector.app.features.media.ImageContentRenderer import im.vector.app.features.media.VideoContentRenderer @@ -65,6 +64,7 @@ import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.ReadReceipt import org.matrix.android.sdk.api.session.room.model.RoomMemberContent +import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent import org.matrix.android.sdk.api.session.room.model.message.MessageImageInfoContent import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent @@ -80,14 +80,30 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec private val timelineMediaSizeProvider: TimelineMediaSizeProvider, private val mergedHeaderItemFactory: MergedHeaderItemFactory, private val session: Session, - private val callManager: WebRtcCallManager, @TimelineEventControllerHandler private val backgroundHandler: Handler, - private val userPreferencesProvider: UserPreferencesProvider, private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper, private val readReceiptsItemFactory: ReadReceiptsItemFactory ) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener, EpoxyController.Interceptor { + /** + * This is a partial state of the RoomDetailViewState + */ + data class PartialState( + val unreadState: UnreadState = UnreadState.Unknown, + val highlightedEventId: String? = null, + val jitsiState: JitsiState = JitsiState(), + val roomSummary: RoomSummary? = null + ) { + + constructor(state: RoomDetailViewState) : this( + unreadState = state.unreadState, + highlightedEventId = state.highlightedEventId, + jitsiState = state.jitsiState, + roomSummary = state.asyncRoomSummary() + ) + } + interface Callback : BaseCallback, ReactionPillCallback, @@ -149,14 +165,15 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec // Map eventId to adapter position private val adapterPositionMapping = HashMap() + private val timelineEventsGroups = TimelineEventsGroups() + private val receiptsByEvent = HashMap>() private val modelCache = arrayListOf() private var currentSnapshot: List = emptyList() private var inSubmitList: Boolean = false private var hasReachedInvite: Boolean = false private var hasUTD: Boolean = false - private var unreadState: UnreadState = UnreadState.Unknown private var positionOfReadMarker: Int? = null - private var eventIdToHighlight: String? = null + private var partialState: PartialState = PartialState() var callback: Callback? = null var timeline: Timeline? = null @@ -174,7 +191,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec // it's sent by the same user so we are sure we have up to date information. val invalidatedSenderId: String? = currentSnapshot.getOrNull(position)?.senderInfo?.userId val prevDisplayableEventIndex = currentSnapshot.subList(0, position).indexOfLast { - timelineEventVisibilityHelper.shouldShowEvent(it, eventIdToHighlight) + timelineEventVisibilityHelper.shouldShowEvent(it, partialState.highlightedEventId) } if (prevDisplayableEventIndex != -1 && currentSnapshot[prevDisplayableEventIndex].senderInfo.userId == invalidatedSenderId) { modelCache[prevDisplayableEventIndex] = null @@ -215,9 +232,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec private val interceptorHelper = TimelineControllerInterceptorHelper( ::positionOfReadMarker, - adapterPositionMapping, - userPreferencesProvider, - callManager + adapterPositionMapping ) init { @@ -226,29 +241,22 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec } override fun intercept(models: MutableList>) = synchronized(modelCache) { - interceptorHelper.intercept(models, unreadState, timeline, callback) + interceptorHelper.intercept(models, partialState.unreadState, timeline, callback) } - fun update(viewState: RoomDetailViewState) { - var requestModelBuild = false - if (eventIdToHighlight != viewState.highlightedEventId) { + fun update(viewState: RoomDetailViewState) = synchronized(modelCache) { + val newPartialState = PartialState(viewState) + if (partialState.highlightedEventId != newPartialState.highlightedEventId) { // Clear cache to force a refresh - synchronized(modelCache) { - for (i in 0 until modelCache.size) { - if (modelCache[i]?.eventId == viewState.highlightedEventId - || modelCache[i]?.eventId == eventIdToHighlight) { - modelCache[i] = null - } + for (i in 0 until modelCache.size) { + if (modelCache[i]?.eventId == viewState.highlightedEventId + || modelCache[i]?.eventId == partialState.highlightedEventId) { + modelCache[i] = null } } - eventIdToHighlight = viewState.highlightedEventId - requestModelBuild = true } - if (this.unreadState != viewState.unreadState) { - this.unreadState = viewState.unreadState - requestModelBuild = true - } - if (requestModelBuild) { + if (newPartialState != partialState) { + partialState = newPartialState requestModelBuild() } } @@ -268,6 +276,10 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec } override fun buildModels() { + // Don't build anything if membership is not joined + if (partialState.roomSummary?.membership != Membership.JOIN) { + return + } val timestamp = System.currentTimeMillis() val showingForwardLoader = LoadingItem_() @@ -346,31 +358,33 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec if (modelCache.isEmpty()) { return } - val receiptsByEvents = getReadReceiptsByShownEvent() - val lastSentEventWithoutReadReceipts = searchLastSentEventWithoutReadReceipts(receiptsByEvents) + preprocessReverseEvents() + val lastSentEventWithoutReadReceipts = searchLastSentEventWithoutReadReceipts(receiptsByEvent) (0 until modelCache.size).forEach { position -> val event = currentSnapshot[position] val nextEvent = currentSnapshot.nextOrNull(position) val prevEvent = currentSnapshot.prevOrNull(position) val nextDisplayableEvent = currentSnapshot.subList(position + 1, currentSnapshot.size).firstOrNull { - timelineEventVisibilityHelper.shouldShowEvent(it, eventIdToHighlight) + timelineEventVisibilityHelper.shouldShowEvent(it, partialState.highlightedEventId) } - val params = TimelineItemFactoryParams( - event = event, - prevEvent = prevEvent, - nextEvent = nextEvent, - nextDisplayableEvent = nextDisplayableEvent, - highlightedEventId = eventIdToHighlight, - lastSentEventIdWithoutReadReceipts = lastSentEventWithoutReadReceipts, - callback = callback - ) // Should be build if not cached or if model should be refreshed - if (modelCache[position] == null || modelCache[position]?.shouldTriggerBuild == true) { + if (modelCache[position] == null || modelCache[position]?.isCacheable == false) { + val timelineEventsGroup = timelineEventsGroups.getOrNull(event) + val params = TimelineItemFactoryParams( + event = event, + prevEvent = prevEvent, + nextEvent = nextEvent, + nextDisplayableEvent = nextDisplayableEvent, + partialState = partialState, + lastSentEventIdWithoutReadReceipts = lastSentEventWithoutReadReceipts, + callback = callback, + eventsGroup = timelineEventsGroup + ) modelCache[position] = buildCacheItem(params) } val itemCachedData = modelCache[position] ?: return@forEach // Then update with additional models if needed - modelCache[position] = itemCachedData.enrichWithModels(event, nextEvent, position, receiptsByEvents) + modelCache[position] = itemCachedData.enrichWithModels(event, nextEvent, position, receiptsByEvent) } } @@ -384,12 +398,13 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec it.id(event.localId) it.setOnVisibilityStateChanged(TimelineEventVisibilityStateChangedListener(callback, event)) } - val shouldTriggerBuild = eventModel is AbsMessageItem && eventModel.attributes.informationData.sendStateDecoration == SendStateDecoration.SENT + val isCacheable = eventModel is ItemWithEvents && eventModel.isCacheable() return CacheItemData( localId = event.localId, eventId = event.root.eventId, eventModel = eventModel, - shouldTriggerBuild = shouldTriggerBuild) + isCacheable = isCacheable + ) } private fun CacheItemData.enrichWithModels(event: TimelineEvent, @@ -399,10 +414,11 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec val wantsDateSeparator = wantsDateSeparator(event, nextEvent) val mergedHeaderModel = mergedHeaderItemFactory.create(event, nextEvent = nextEvent, + partialState = partialState, items = this@TimelineEventController.currentSnapshot, addDaySeparator = wantsDateSeparator, currentPosition = position, - eventIdToHighlight = eventIdToHighlight, + eventIdToHighlight = partialState.highlightedEventId, callback = callback ) { requestModelBuild() @@ -431,7 +447,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec return null } // If the event is not shown, we go to the next one - if (!timelineEventVisibilityHelper.shouldShowEvent(event, eventIdToHighlight)) { + if (!timelineEventVisibilityHelper.shouldShowEvent(event, partialState.highlightedEventId)) { continue } // If the event is sent by us, we update the holder with the eventId and stop the search @@ -442,19 +458,18 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec return null } - private fun getReadReceiptsByShownEvent(): Map> { - val receiptsByEvent = HashMap>() - if (!userPreferencesProvider.shouldShowReadReceipts()) { - return receiptsByEvent - } - var lastShownEventId: String? = null + private fun preprocessReverseEvents() { + receiptsByEvent.clear() + timelineEventsGroups.clear() val itr = currentSnapshot.listIterator(currentSnapshot.size) + var lastShownEventId: String? = null while (itr.hasPrevious()) { val event = itr.previous() + timelineEventsGroups.addOrIgnore(event) val currentReadReceipts = ArrayList(event.readReceipts).filter { it.user.userId != session.myUserId } - if (timelineEventVisibilityHelper.shouldShowEvent(event, eventIdToHighlight)) { + if (timelineEventVisibilityHelper.shouldShowEvent(event, partialState.highlightedEventId)) { lastShownEventId = event.eventId } if (lastShownEventId == null) { @@ -463,7 +478,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec val existingReceipts = receiptsByEvent.getOrPut(lastShownEventId) { ArrayList() } existingReceipts.addAll(currentReadReceipts) } - return receiptsByEvent } private fun buildDaySeparatorItem(originServerTs: Long?): DaySeparatorItem { @@ -536,6 +550,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec val eventModel: EpoxyModel<*>? = null, val mergedHeaderModel: BasedMergedItem<*>? = null, val formattedDayModel: DaySeparatorItem? = null, - val shouldTriggerBuild: Boolean = false + val isCacheable: Boolean = true ) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt index b9c368ebdc..6e6c7c1dbe 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt @@ -36,6 +36,7 @@ import im.vector.app.features.html.VectorHtmlCompressor import im.vector.app.features.powerlevel.PowerLevelsObservableFactory import im.vector.app.features.reactions.data.EmojiDataSource import im.vector.app.features.settings.VectorPreferences +import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupState import org.matrix.android.sdk.api.session.events.model.EventType @@ -207,7 +208,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted EventType.CALL_CANDIDATES, EventType.CALL_HANGUP, EventType.CALL_ANSWER -> { - noticeEventFormatter.format(timelineEvent) + noticeEventFormatter.format(timelineEvent, room?.roomSummary()?.isDirect.orFalse()) } else -> null } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/CallItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/CallItemFactory.kt index 9697fb6672..97f2618fe6 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/CallItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/CallItemFactory.kt @@ -16,127 +16,122 @@ package im.vector.app.features.home.room.detail.timeline.factory import im.vector.app.core.epoxy.VectorEpoxyModel -import im.vector.app.features.call.vectorCallService -import im.vector.app.features.call.webrtc.WebRtcCallManager +import im.vector.app.core.resources.UserPreferencesProvider import im.vector.app.features.home.room.detail.timeline.MessageColorProvider import im.vector.app.features.home.room.detail.timeline.TimelineEventController import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider +import im.vector.app.features.home.room.detail.timeline.helper.CallSignalingEventsGroup import im.vector.app.features.home.room.detail.timeline.helper.MessageInformationDataFactory import im.vector.app.features.home.room.detail.timeline.helper.MessageItemAttributesFactory -import im.vector.app.features.home.room.detail.timeline.helper.RoomSummariesHolder import im.vector.app.features.home.room.detail.timeline.item.CallTileTimelineItem import im.vector.app.features.home.room.detail.timeline.item.CallTileTimelineItem_ import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.toModel -import org.matrix.android.sdk.api.session.room.model.call.CallAnswerContent -import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent -import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent -import org.matrix.android.sdk.api.session.room.model.call.CallRejectContent -import org.matrix.android.sdk.api.session.room.model.call.CallSignalingContent -import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.util.toMatrixItem import javax.inject.Inject class CallItemFactory @Inject constructor( private val session: Session, + private val userPreferencesProvider: UserPreferencesProvider, private val messageColorProvider: MessageColorProvider, private val messageInformationDataFactory: MessageInformationDataFactory, private val messageItemAttributesFactory: MessageItemAttributesFactory, private val avatarSizeProvider: AvatarSizeProvider, - private val roomSummariesHolder: RoomSummariesHolder, - private val callManager: WebRtcCallManager -) { + private val noticeItemFactory: NoticeItemFactory) { fun create(params: TimelineItemFactoryParams): VectorEpoxyModel<*>? { val event = params.event if (event.root.eventId == null) return null - val roomId = event.roomId + val showHiddenEvents = userPreferencesProvider.shouldShowHiddenEvents() + val callEventGrouper = params.eventsGroup?.let { CallSignalingEventsGroup(it) } ?: return null + val roomSummary = params.partialState.roomSummary ?: return null val informationData = messageInformationDataFactory.create(params) - val callSignalingContent = event.getCallSignalingContent() ?: return null - val callId = callSignalingContent.callId ?: return null - val call = callManager.getCallById(callId) - val callKind = when { - call == null -> CallTileTimelineItem.CallKind.UNKNOWN - call.mxCall.isVideoCall -> CallTileTimelineItem.CallKind.VIDEO - else -> CallTileTimelineItem.CallKind.AUDIO - } - return when (event.root.getClearType()) { + val callKind = if (callEventGrouper.isVideo()) CallTileTimelineItem.CallKind.VIDEO else CallTileTimelineItem.CallKind.AUDIO + val callItem = when (event.root.getClearType()) { EventType.CALL_ANSWER -> { - createCallTileTimelineItem( - roomId = roomId, - callId = callId, - callStatus = CallTileTimelineItem.CallStatus.IN_CALL, - callKind = callKind, - callback = params.callback, - highlight = params.isHighlighted, - informationData = informationData, - isStillActive = call != null - ) + if (callEventGrouper.isInCall()) { + createCallTileTimelineItem( + roomSummary = roomSummary, + callId = callEventGrouper.callId, + callStatus = CallTileTimelineItem.CallStatus.IN_CALL, + callKind = callKind, + callback = params.callback, + highlight = params.isHighlighted, + informationData = informationData, + isStillActive = callEventGrouper.isInCall(), + formattedDuration = callEventGrouper.formattedDuration() + ) + } else { + null + } } EventType.CALL_INVITE -> { - createCallTileTimelineItem( - roomId = roomId, - callId = callId, - callStatus = CallTileTimelineItem.CallStatus.INVITED, - callKind = callKind, - callback = params.callback, - highlight = params.isHighlighted, - informationData = informationData, - isStillActive = call != null - ) + if (callEventGrouper.isRinging()) { + createCallTileTimelineItem( + roomSummary = roomSummary, + callId = callEventGrouper.callId, + callStatus = CallTileTimelineItem.CallStatus.INVITED, + callKind = callKind, + callback = params.callback, + highlight = params.isHighlighted, + informationData = informationData, + isStillActive = callEventGrouper.isRinging(), + formattedDuration = callEventGrouper.formattedDuration() + ) + } else { + null + } } EventType.CALL_REJECT -> { createCallTileTimelineItem( - roomId = roomId, - callId = callId, + roomSummary = roomSummary, + callId = callEventGrouper.callId, callStatus = CallTileTimelineItem.CallStatus.REJECTED, callKind = callKind, callback = params.callback, highlight = params.isHighlighted, informationData = informationData, - isStillActive = false + isStillActive = false, + formattedDuration = callEventGrouper.formattedDuration() ) } EventType.CALL_HANGUP -> { createCallTileTimelineItem( - roomId = roomId, - callId = callId, - callStatus = CallTileTimelineItem.CallStatus.ENDED, + roomSummary = roomSummary, + callId = callEventGrouper.callId, + callStatus = if (callEventGrouper.callWasMissed()) CallTileTimelineItem.CallStatus.MISSED else CallTileTimelineItem.CallStatus.ENDED, callKind = callKind, callback = params.callback, highlight = params.isHighlighted, informationData = informationData, - isStillActive = false + isStillActive = false, + formattedDuration = callEventGrouper.formattedDuration() ) } else -> null } - } - - private fun TimelineEvent.getCallSignalingContent(): CallSignalingContent? { - return when (root.getClearType()) { - EventType.CALL_INVITE -> root.getClearContent().toModel() - EventType.CALL_HANGUP -> root.getClearContent().toModel() - EventType.CALL_REJECT -> root.getClearContent().toModel() - EventType.CALL_ANSWER -> root.getClearContent().toModel() - else -> null + return if (callItem == null && showHiddenEvents) { + // Fallback to notice item for showing hidden events + noticeItemFactory.create(params) + } else { + callItem } } private fun createCallTileTimelineItem( - roomId: String, + roomSummary: RoomSummary, callId: String, callKind: CallTileTimelineItem.CallKind, callStatus: CallTileTimelineItem.CallStatus, informationData: MessageInformationData, highlight: Boolean, isStillActive: Boolean, + formattedDuration: String, callback: TimelineEventController.Callback? ): CallTileTimelineItem? { - val correctedRoomId = session.vectorCallService.userMapper.nativeRoomForVirtualRoom(roomId) ?: roomId - val userOfInterest = roomSummariesHolder.get(correctedRoomId)?.toMatrixItem() ?: return null + val userOfInterest = roomSummary.toMatrixItem() val attributes = messageItemAttributesFactory.create(null, informationData, callback).let { CallTileTimelineItem.Attributes( callId = callId, @@ -144,6 +139,7 @@ class CallItemFactory @Inject constructor( callStatus = callStatus, informationData = informationData, avatarRenderer = it.avatarRenderer, + formattedDuration = formattedDuration, messageColorProvider = messageColorProvider, itemClickListener = it.itemClickListener, itemLongClickListener = it.itemLongClickListener, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt index cb2a067540..25b5dc34d6 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt @@ -22,7 +22,6 @@ import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.detail.timeline.TimelineEventController import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider import im.vector.app.features.home.room.detail.timeline.helper.MergedTimelineEventVisibilityStateChangedListener -import im.vector.app.features.home.room.detail.timeline.helper.RoomSummariesHolder import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventVisibilityHelper import im.vector.app.features.home.room.detail.timeline.helper.canBeMerged import im.vector.app.features.home.room.detail.timeline.helper.isRoomConfiguration @@ -47,8 +46,7 @@ import javax.inject.Inject class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolder: ActiveSessionHolder, private val avatarRenderer: AvatarRenderer, private val avatarSizeProvider: AvatarSizeProvider, - private val roomSummariesHolder: RoomSummariesHolder, -private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper) { + private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper) { private val collapsedEventIds = linkedSetOf() private val mergeItemCollapseStates = HashMap() @@ -60,6 +58,7 @@ private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper) { fun create(event: TimelineEvent, nextEvent: TimelineEvent?, items: List, + partialState: TimelineEventController.PartialState, addDaySeparator: Boolean, currentPosition: Int, eventIdToHighlight: String?, @@ -70,18 +69,17 @@ private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper) { && event.isRoomConfiguration(nextEvent.root.getClearContent()?.toModel()?.creator)) { // It's the first item before room.create // Collapse all room configuration events - buildRoomCreationMergedSummary(currentPosition, items, event, eventIdToHighlight, requestModelBuild, callback) + buildRoomCreationMergedSummary(currentPosition, items, partialState, event, eventIdToHighlight, requestModelBuild, callback) } else if (!event.canBeMerged() || (nextEvent?.root?.getClearType() == event.root.getClearType() && !addDaySeparator)) { null } else { - buildMembershipEventsMergedSummary(currentPosition, items, event, eventIdToHighlight, requestModelBuild, callback) + buildMembershipEventsMergedSummary(currentPosition, items, partialState, event, eventIdToHighlight, requestModelBuild, callback) } } - private fun isDirectRoom(roomId: String) = roomSummariesHolder.get(roomId)?.isDirect.orFalse() - private fun buildMembershipEventsMergedSummary(currentPosition: Int, items: List, + partialState: TimelineEventController.PartialState, event: TimelineEvent, eventIdToHighlight: String?, requestModelBuild: () -> Unit, @@ -102,7 +100,7 @@ private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper) { memberName = mergedEvent.senderInfo.disambiguatedDisplayName, localId = mergedEvent.localId, eventId = mergedEvent.root.eventId ?: "", - isDirectRoom = isDirectRoom(event.roomId) + isDirectRoom = partialState.isDirectRoom() ) mergedData.add(data) } @@ -141,6 +139,7 @@ private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper) { private fun buildRoomCreationMergedSummary(currentPosition: Int, items: List, + partialState: TimelineEventController.PartialState, event: TimelineEvent, eventIdToHighlight: String?, requestModelBuild: () -> Unit, @@ -173,7 +172,7 @@ private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper) { memberName = mergedEvent.senderInfo.disambiguatedDisplayName, localId = mergedEvent.localId, eventId = mergedEvent.root.eventId ?: "", - isDirectRoom = isDirectRoom(event.roomId) + isDirectRoom = partialState.isDirectRoom() ) mergedData.add(data) } @@ -206,7 +205,8 @@ private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper) { isEncryptionAlgorithmSecure = encryptionAlgorithm == MXCRYPTO_ALGORITHM_MEGOLM, callback = callback, currentUserId = currentUserId, - roomSummary = roomSummariesHolder.get(event.roomId), + roomSummary = partialState.roomSummary, + canInvite = powerLevelsHelper?.isUserAbleToInvite(currentUserId) ?: false, canChangeAvatar = powerLevelsHelper?.isUserAllowedToSend(currentUserId, true, EventType.STATE_ROOM_AVATAR) ?: false, canChangeTopic = powerLevelsHelper?.isUserAllowedToSend(currentUserId, true, EventType.STATE_ROOM_TOPIC) ?: false, canChangeName = powerLevelsHelper?.isUserAllowedToSend(currentUserId, true, EventType.STATE_ROOM_NAME) ?: false @@ -223,6 +223,10 @@ private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper) { } else null } + private fun TimelineEventController.PartialState.isDirectRoom(): Boolean { + return roomSummary?.isDirect.orFalse() + } + fun isCollapsed(localId: Long): Boolean { return collapsedEventIds.contains(localId) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt index e67fa7cca0..287cd014e9 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -622,11 +622,13 @@ class MessageItemFactory @Inject constructor( .highlighted(highlight) } - private fun List?.toFft(): List? { - return this?.map { - // Value comes from AudioRecordView.maxReportableAmp, and 1024 is the max value in the Matrix spec - it * 22760 / 1024 - } + private fun List?.toFft(): List? { + return this + ?.filterNotNull() + ?.map { + // Value comes from AudioRecordView.maxReportableAmp, and 1024 is the max value in the Matrix spec + it * 22760 / 1024 + } } companion object { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/NoticeItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/NoticeItemFactory.kt index e757b6b47b..ed6620dcd4 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/NoticeItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/NoticeItemFactory.kt @@ -22,6 +22,7 @@ import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvide import im.vector.app.features.home.room.detail.timeline.helper.MessageInformationDataFactory import im.vector.app.features.home.room.detail.timeline.item.NoticeItem import im.vector.app.features.home.room.detail.timeline.item.NoticeItem_ +import org.matrix.android.sdk.api.extensions.orFalse import javax.inject.Inject class NoticeItemFactory @Inject constructor(private val eventFormatter: NoticeEventFormatter, @@ -31,7 +32,7 @@ class NoticeItemFactory @Inject constructor(private val eventFormatter: NoticeEv fun create(params: TimelineItemFactoryParams): NoticeItem? { val event = params.event - val formattedText = eventFormatter.format(event) ?: return null + val formattedText = eventFormatter.format(event, isDm = params.partialState.roomSummary?.isDirect.orFalse()) ?: return null val informationData = informationDataFactory.create(params) val attributes = NoticeItem.Attributes( avatarRenderer = avatarRenderer, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactoryParams.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactoryParams.kt index 0e595ba30e..cdfedb2925 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactoryParams.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactoryParams.kt @@ -17,6 +17,7 @@ package im.vector.app.features.home.room.detail.timeline.factory import im.vector.app.features.home.room.detail.timeline.TimelineEventController +import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventsGroup import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent data class TimelineItemFactoryParams( @@ -24,9 +25,14 @@ data class TimelineItemFactoryParams( val prevEvent: TimelineEvent? = null, val nextEvent: TimelineEvent? = null, val nextDisplayableEvent: TimelineEvent? = null, - val highlightedEventId: String? = null, + val partialState: TimelineEventController.PartialState = TimelineEventController.PartialState(), val lastSentEventIdWithoutReadReceipts: String? = null, - val callback: TimelineEventController.Callback? = null + val callback: TimelineEventController.Callback? = null, + val eventsGroup: TimelineEventsGroup? = null ) { + + val highlightedEventId: String? + get() = partialState.highlightedEventId + val isHighlighted = highlightedEventId == event.eventId } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/WidgetItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/WidgetItemFactory.kt index 1fc57489a5..52f72810c9 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/WidgetItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/WidgetItemFactory.kt @@ -16,34 +16,28 @@ package im.vector.app.features.home.room.detail.timeline.factory -import im.vector.app.ActiveSessionDataSource -import im.vector.app.R import im.vector.app.core.epoxy.VectorEpoxyModel -import im.vector.app.core.resources.StringProvider +import im.vector.app.core.resources.UserPreferencesProvider +import im.vector.app.features.home.AvatarRenderer +import im.vector.app.features.home.room.detail.timeline.MessageColorProvider import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider +import im.vector.app.features.home.room.detail.timeline.helper.JitsiWidgetEventsGroup import im.vector.app.features.home.room.detail.timeline.helper.MessageInformationDataFactory -import im.vector.app.features.home.room.detail.timeline.helper.MessageItemAttributesFactory -import im.vector.app.features.home.room.detail.timeline.item.WidgetTileTimelineItem -import im.vector.app.features.home.room.detail.timeline.item.WidgetTileTimelineItem_ -import org.matrix.android.sdk.api.extensions.orFalse -import org.matrix.android.sdk.api.session.events.model.Event +import im.vector.app.features.home.room.detail.timeline.item.CallTileTimelineItem +import im.vector.app.features.home.room.detail.timeline.item.CallTileTimelineItem_ import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.widgets.model.WidgetContent import org.matrix.android.sdk.api.session.widgets.model.WidgetType +import org.matrix.android.sdk.api.util.toMatrixItem import javax.inject.Inject class WidgetItemFactory @Inject constructor( - private val sp: StringProvider, - private val messageItemAttributesFactory: MessageItemAttributesFactory, private val informationDataFactory: MessageInformationDataFactory, private val noticeItemFactory: NoticeItemFactory, private val avatarSizeProvider: AvatarSizeProvider, - private val activeSessionDataSource: ActiveSessionDataSource -) { - private val currentUserId: String? - get() = activeSessionDataSource.currentValue?.orNull()?.myUserId - - private fun Event.isSentByCurrentUser() = senderId != null && senderId == currentUserId + private val messageColorProvider: MessageColorProvider, + private val avatarRenderer: AvatarRenderer, + private val userPreferencesProvider: UserPreferencesProvider) { fun create(params: TimelineItemFactoryParams): VectorEpoxyModel<*>? { val event = params.event @@ -51,62 +45,54 @@ class WidgetItemFactory @Inject constructor( val previousWidgetContent: WidgetContent? = event.root.resolvedPrevContent().toModel() return when (WidgetType.fromString(widgetContent.type ?: previousWidgetContent?.type ?: "")) { - WidgetType.Jitsi -> createJitsiItem(params, widgetContent, previousWidgetContent) + WidgetType.Jitsi -> createJitsiItem(params, widgetContent) // There is lot of other widget types we could improve here else -> noticeItemFactory.create(params) } } - private fun createJitsiItem(params: TimelineItemFactoryParams, - widgetContent: WidgetContent, - previousWidgetContent: WidgetContent?): VectorEpoxyModel<*> { - val timelineEvent = params.event + private fun createJitsiItem(params: TimelineItemFactoryParams, widgetContent: WidgetContent): VectorEpoxyModel<*>? { val informationData = informationDataFactory.create(params) - val attributes = messageItemAttributesFactory.create(null, informationData, params.callback) - - val disambiguatedDisplayName = timelineEvent.senderInfo.disambiguatedDisplayName - val message = if (widgetContent.isActive()) { - val widgetName = widgetContent.getHumanName() - if (previousWidgetContent?.isActive().orFalse()) { - // Widget has been modified - if (timelineEvent.root.isSentByCurrentUser()) { - sp.getString(R.string.notice_widget_jitsi_modified_by_you, widgetName) - } else { - sp.getString(R.string.notice_widget_jitsi_modified, disambiguatedDisplayName, widgetName) - } + val userOfInterest = params.partialState.roomSummary?.toMatrixItem() ?: return null + val isActiveTile = widgetContent.isActive() + val jitsiWidgetEventsGroup = params.eventsGroup?.let { JitsiWidgetEventsGroup(it) } ?: return null + val isCallStillActive = jitsiWidgetEventsGroup.isStillActive() + val showHiddenEvents = userPreferencesProvider.shouldShowHiddenEvents() + if (isActiveTile && !isCallStillActive) { + return if (showHiddenEvents) { + noticeItemFactory.create(params) } else { - // Widget has been added - if (timelineEvent.root.isSentByCurrentUser()) { - sp.getString(R.string.notice_widget_jitsi_added_by_you, widgetName) - } else { - sp.getString(R.string.notice_widget_jitsi_added, disambiguatedDisplayName, widgetName) - } - } - } else { - // Widget has been removed - val widgetName = previousWidgetContent?.getHumanName() - if (timelineEvent.root.isSentByCurrentUser()) { - sp.getString(R.string.notice_widget_jitsi_removed_by_you, widgetName) - } else { - sp.getString(R.string.notice_widget_jitsi_removed, disambiguatedDisplayName, widgetName) + null } } - - return WidgetTileTimelineItem_() - .attributes( - WidgetTileTimelineItem.Attributes( - title = message, - drawableStart = R.drawable.ic_video, - informationData = informationData, - avatarRenderer = attributes.avatarRenderer, - messageColorProvider = attributes.messageColorProvider, - itemLongClickListener = attributes.itemLongClickListener, - itemClickListener = attributes.itemClickListener, - reactionPillCallback = attributes.reactionPillCallback, - readReceiptsCallback = attributes.readReceiptsCallback, - emojiTypeFace = attributes.emojiTypeFace - ) - ) + val callStatus = if (isActiveTile && params.event.root.stateKey == params.partialState.jitsiState.widgetId) { + if (params.partialState.jitsiState.hasJoined) { + CallTileTimelineItem.CallStatus.IN_CALL + } else { + CallTileTimelineItem.CallStatus.INVITED + } + } else { + CallTileTimelineItem.CallStatus.ENDED + } + val attributes = CallTileTimelineItem.Attributes( + callId = jitsiWidgetEventsGroup.callId, + callKind = CallTileTimelineItem.CallKind.CONFERENCE, + callStatus = callStatus, + informationData = informationData, + avatarRenderer = avatarRenderer, + messageColorProvider = messageColorProvider, + itemClickListener = null, + itemLongClickListener = null, + reactionPillCallback = params.callback, + readReceiptsCallback = params.callback, + userOfInterest = userOfInterest, + callback = params.callback, + isStillActive = isCallStillActive, + formattedDuration = "" + ) + return CallTileTimelineItem_() + .attributes(attributes) + .highlighted(params.isHighlighted) .leftGuideline(avatarSizeProvider.leftGuideline) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt index e6fbc5294b..5a9af975ed 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt @@ -41,7 +41,7 @@ class DisplayableEventFormatter @Inject constructor( private val noticeEventFormatter: NoticeEventFormatter ) { - fun format(timelineEvent: TimelineEvent, appendAuthor: Boolean): CharSequence { + fun format(timelineEvent: TimelineEvent, isDm: Boolean, appendAuthor: Boolean): CharSequence { if (timelineEvent.root.isRedacted()) { return noticeEventFormatter.formatRedactedEvent(timelineEvent.root) } @@ -135,7 +135,7 @@ class DisplayableEventFormatter @Inject constructor( } else -> { return span { - text = noticeEventFormatter.format(timelineEvent) ?: "" + text = noticeEventFormatter.format(timelineEvent, isDm) ?: "" textStyle = "italic" } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt index b1b96df9ea..c80a92d568 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt @@ -19,7 +19,6 @@ package im.vector.app.features.home.room.detail.timeline.format import im.vector.app.ActiveSessionDataSource import im.vector.app.R import im.vector.app.core.resources.StringProvider -import im.vector.app.features.home.room.detail.timeline.helper.RoomSummariesHolder import im.vector.app.features.roomprofile.permissions.RoleFormatter import im.vector.app.features.settings.VectorPreferences import org.matrix.android.sdk.api.extensions.appendNl @@ -40,7 +39,6 @@ import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesContent import org.matrix.android.sdk.api.session.room.model.RoomMemberContent import org.matrix.android.sdk.api.session.room.model.RoomNameContent import org.matrix.android.sdk.api.session.room.model.RoomServerAclContent -import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.RoomThirdPartyInviteContent import org.matrix.android.sdk.api.session.room.model.RoomTopicContent import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent @@ -58,7 +56,6 @@ class NoticeEventFormatter @Inject constructor( private val roomHistoryVisibilityFormatter: RoomHistoryVisibilityFormatter, private val roleFormatter: RoleFormatter, private val vectorPreferences: VectorPreferences, - private val roomSummariesHolder: RoomSummariesHolder, private val sp: StringProvider ) { @@ -67,28 +64,25 @@ class NoticeEventFormatter @Inject constructor( private fun Event.isSentByCurrentUser() = senderId != null && senderId == currentUserId - private fun RoomSummary?.isDm() = this?.isDirect.orFalse() - - fun format(timelineEvent: TimelineEvent): CharSequence? { - val rs = roomSummariesHolder.get(timelineEvent.roomId) + fun format(timelineEvent: TimelineEvent, isDm: Boolean): CharSequence? { return when (val type = timelineEvent.root.getClearType()) { - EventType.STATE_ROOM_JOIN_RULES -> formatJoinRulesEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName, rs) - EventType.STATE_ROOM_CREATE -> formatRoomCreateEvent(timelineEvent.root, rs) + EventType.STATE_ROOM_JOIN_RULES -> formatJoinRulesEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName, isDm) + EventType.STATE_ROOM_CREATE -> formatRoomCreateEvent(timelineEvent.root, isDm) EventType.STATE_ROOM_NAME -> formatRoomNameEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName) EventType.STATE_ROOM_TOPIC -> formatRoomTopicEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName) EventType.STATE_ROOM_AVATAR -> formatRoomAvatarEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName) - EventType.STATE_ROOM_MEMBER -> formatRoomMemberEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName, rs) - EventType.STATE_ROOM_THIRD_PARTY_INVITE -> formatRoomThirdPartyInvite(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName, rs) + EventType.STATE_ROOM_MEMBER -> formatRoomMemberEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName, isDm) + EventType.STATE_ROOM_THIRD_PARTY_INVITE -> formatRoomThirdPartyInvite(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName, isDm) EventType.STATE_ROOM_ALIASES -> formatRoomAliasesEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName) EventType.STATE_ROOM_CANONICAL_ALIAS -> formatRoomCanonicalAliasEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName) EventType.STATE_ROOM_HISTORY_VISIBILITY -> - formatRoomHistoryVisibilityEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName, rs) + formatRoomHistoryVisibilityEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName, isDm) EventType.STATE_ROOM_SERVER_ACL -> formatRoomServerAclEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName) - EventType.STATE_ROOM_GUEST_ACCESS -> formatRoomGuestAccessEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName, rs) + EventType.STATE_ROOM_GUEST_ACCESS -> formatRoomGuestAccessEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName, isDm) EventType.STATE_ROOM_ENCRYPTION -> formatRoomEncryptionEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName) EventType.STATE_ROOM_WIDGET, EventType.STATE_ROOM_WIDGET_LEGACY -> formatWidgetEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName) - EventType.STATE_ROOM_TOMBSTONE -> formatRoomTombstoneEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName, rs) + EventType.STATE_ROOM_TOMBSTONE -> formatRoomTombstoneEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName, isDm) EventType.STATE_ROOM_POWER_LEVELS -> formatRoomPowerLevels(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName) EventType.CALL_INVITE, EventType.CALL_CANDIDATES, @@ -176,20 +170,20 @@ class NoticeEventFormatter @Inject constructor( } } - fun format(event: Event, senderName: String?, rs: RoomSummary?): CharSequence? { + fun format(event: Event, senderName: String?, isDm: Boolean): CharSequence? { return when (val type = event.getClearType()) { - EventType.STATE_ROOM_JOIN_RULES -> formatJoinRulesEvent(event, senderName, rs) + EventType.STATE_ROOM_JOIN_RULES -> formatJoinRulesEvent(event, senderName, isDm) EventType.STATE_ROOM_NAME -> formatRoomNameEvent(event, senderName) EventType.STATE_ROOM_TOPIC -> formatRoomTopicEvent(event, senderName) EventType.STATE_ROOM_AVATAR -> formatRoomAvatarEvent(event, senderName) - EventType.STATE_ROOM_MEMBER -> formatRoomMemberEvent(event, senderName, rs) - EventType.STATE_ROOM_THIRD_PARTY_INVITE -> formatRoomThirdPartyInvite(event, senderName, rs) - EventType.STATE_ROOM_HISTORY_VISIBILITY -> formatRoomHistoryVisibilityEvent(event, senderName, rs) + EventType.STATE_ROOM_MEMBER -> formatRoomMemberEvent(event, senderName, isDm) + EventType.STATE_ROOM_THIRD_PARTY_INVITE -> formatRoomThirdPartyInvite(event, senderName, isDm) + EventType.STATE_ROOM_HISTORY_VISIBILITY -> formatRoomHistoryVisibilityEvent(event, senderName, isDm) EventType.CALL_INVITE, EventType.CALL_HANGUP, EventType.CALL_REJECT, EventType.CALL_ANSWER -> formatCallEvent(type, event, senderName) - EventType.STATE_ROOM_TOMBSTONE -> formatRoomTombstoneEvent(event, senderName, rs) + EventType.STATE_ROOM_TOMBSTONE -> formatRoomTombstoneEvent(event, senderName, isDm) else -> { Timber.v("Type $type not handled by this formatter") null @@ -201,14 +195,14 @@ class NoticeEventFormatter @Inject constructor( return "Debug: event type \"${event.getClearType()}\"" } - private fun formatRoomCreateEvent(event: Event, rs: RoomSummary?): CharSequence? { + private fun formatRoomCreateEvent(event: Event, isDm: Boolean): CharSequence? { return event.getClearContent().toModel() ?.takeIf { it.creator.isNullOrBlank().not() } ?.let { if (event.isSentByCurrentUser()) { - sp.getString(if (rs.isDm()) R.string.notice_direct_room_created_by_you else R.string.notice_room_created_by_you) + sp.getString(if (isDm) R.string.notice_direct_room_created_by_you else R.string.notice_room_created_by_you) } else { - sp.getString(if (rs.isDm()) R.string.notice_direct_room_created else R.string.notice_room_created, it.creator) + sp.getString(if (isDm) R.string.notice_direct_room_created else R.string.notice_room_created, it.creator) } } } @@ -230,11 +224,11 @@ class NoticeEventFormatter @Inject constructor( } } - private fun formatRoomTombstoneEvent(event: Event, senderName: String?, rs: RoomSummary?): CharSequence? { + private fun formatRoomTombstoneEvent(event: Event, senderName: String?, isDm: Boolean): CharSequence? { return if (event.isSentByCurrentUser()) { - sp.getString(if (rs.isDm()) R.string.notice_direct_room_update_by_you else R.string.notice_room_update_by_you) + sp.getString(if (isDm) R.string.notice_direct_room_update_by_you else R.string.notice_room_update_by_you) } else { - sp.getString(if (rs.isDm()) R.string.notice_direct_room_update else R.string.notice_room_update, senderName) + sp.getString(if (isDm) R.string.notice_direct_room_update else R.string.notice_room_update, senderName) } } @@ -272,20 +266,20 @@ class NoticeEventFormatter @Inject constructor( } } - private fun formatRoomHistoryVisibilityEvent(event: Event, senderName: String?, rs: RoomSummary?): CharSequence? { + private fun formatRoomHistoryVisibilityEvent(event: Event, senderName: String?, isDm: Boolean): CharSequence? { val historyVisibility = event.getClearContent().toModel()?.historyVisibility ?: return null val historyVisibilitySuffix = roomHistoryVisibilityFormatter.getNoticeSuffix(historyVisibility) return if (event.isSentByCurrentUser()) { - sp.getString(if (rs.isDm()) R.string.notice_made_future_direct_room_visibility_by_you else R.string.notice_made_future_room_visibility_by_you, + sp.getString(if (isDm) R.string.notice_made_future_direct_room_visibility_by_you else R.string.notice_made_future_room_visibility_by_you, historyVisibilitySuffix) } else { - sp.getString(if (rs.isDm()) R.string.notice_made_future_direct_room_visibility else R.string.notice_made_future_room_visibility, + sp.getString(if (isDm) R.string.notice_made_future_direct_room_visibility else R.string.notice_made_future_room_visibility, senderName, historyVisibilitySuffix) } } - private fun formatRoomThirdPartyInvite(event: Event, senderName: String?, rs: RoomSummary?): CharSequence? { + private fun formatRoomThirdPartyInvite(event: Event, senderName: String?, isDm: Boolean): CharSequence? { val content = event.getClearContent().toModel() val prevContent = event.resolvedPrevContent()?.toModel() @@ -294,24 +288,24 @@ class NoticeEventFormatter @Inject constructor( // Revoke case if (event.isSentByCurrentUser()) { sp.getString( - if (rs.isDm()) { + if (isDm) { R.string.notice_direct_room_third_party_revoked_invite_by_you } else { R.string.notice_room_third_party_revoked_invite_by_you }, prevContent.displayName) } else { - sp.getString(if (rs.isDm()) R.string.notice_direct_room_third_party_revoked_invite else R.string.notice_room_third_party_revoked_invite, + sp.getString(if (isDm) R.string.notice_direct_room_third_party_revoked_invite else R.string.notice_room_third_party_revoked_invite, senderName, prevContent.displayName) } } content != null -> { // Invitation case if (event.isSentByCurrentUser()) { - sp.getString(if (rs.isDm()) R.string.notice_direct_room_third_party_invite_by_you else R.string.notice_room_third_party_invite_by_you, + sp.getString(if (isDm) R.string.notice_direct_room_third_party_invite_by_you else R.string.notice_room_third_party_invite_by_you, content.displayName) } else { - sp.getString(if (rs.isDm()) R.string.notice_direct_room_third_party_invite else R.string.notice_room_third_party_invite, + sp.getString(if (isDm) R.string.notice_direct_room_third_party_invite else R.string.notice_room_third_party_invite, senderName, content.displayName) } } @@ -358,7 +352,7 @@ class NoticeEventFormatter @Inject constructor( } EventType.CALL_REJECT -> if (event.isSentByCurrentUser()) { - sp.getString(R.string.call_tile_you_declined, "") + sp.getString(R.string.call_tile_you_declined_this_call) } else { sp.getString(R.string.call_tile_other_declined, senderName) } @@ -366,13 +360,13 @@ class NoticeEventFormatter @Inject constructor( } } - private fun formatRoomMemberEvent(event: Event, senderName: String?, rs: RoomSummary?): String? { + private fun formatRoomMemberEvent(event: Event, senderName: String?, isDm: Boolean): String? { val eventContent: RoomMemberContent? = event.getClearContent().toModel() val prevEventContent: RoomMemberContent? = event.resolvedPrevContent().toModel() val isMembershipEvent = prevEventContent?.membership != eventContent?.membership || eventContent?.membership == Membership.LEAVE return if (isMembershipEvent) { - buildMembershipNotice(event, senderName, eventContent, prevEventContent, rs) + buildMembershipNotice(event, senderName, eventContent, prevEventContent, isDm) } else { buildProfileNotice(event, senderName, eventContent, prevEventContent) } @@ -554,25 +548,25 @@ class NoticeEventFormatter @Inject constructor( } } - private fun formatRoomGuestAccessEvent(event: Event, senderName: String?, rs: RoomSummary?): String? { + private fun formatRoomGuestAccessEvent(event: Event, senderName: String?, isDm: Boolean): String? { val eventContent: RoomGuestAccessContent? = event.getClearContent().toModel() return when (eventContent?.guestAccess) { GuestAccess.CanJoin -> if (event.isSentByCurrentUser()) { sp.getString( - if (rs.isDm()) R.string.notice_direct_room_guest_access_can_join_by_you else R.string.notice_room_guest_access_can_join_by_you + if (isDm) R.string.notice_direct_room_guest_access_can_join_by_you else R.string.notice_room_guest_access_can_join_by_you ) } else { - sp.getString(if (rs.isDm()) R.string.notice_direct_room_guest_access_can_join else R.string.notice_room_guest_access_can_join, + sp.getString(if (isDm) R.string.notice_direct_room_guest_access_can_join else R.string.notice_room_guest_access_can_join, senderName) } GuestAccess.Forbidden -> if (event.isSentByCurrentUser()) { sp.getString( - if (rs.isDm()) R.string.notice_direct_room_guest_access_forbidden_by_you else R.string.notice_room_guest_access_forbidden_by_you + if (isDm) R.string.notice_direct_room_guest_access_forbidden_by_you else R.string.notice_room_guest_access_forbidden_by_you ) } else { - sp.getString(if (rs.isDm()) R.string.notice_direct_room_guest_access_forbidden else R.string.notice_room_guest_access_forbidden, + sp.getString(if (isDm) R.string.notice_direct_room_guest_access_forbidden else R.string.notice_room_guest_access_forbidden, senderName) } else -> null @@ -656,7 +650,7 @@ class NoticeEventFormatter @Inject constructor( senderName: String?, eventContent: RoomMemberContent?, prevEventContent: RoomMemberContent?, - rs: RoomSummary?): String? { + isDm: Boolean): String? { val senderDisplayName = senderName ?: event.senderId ?: "" val targetDisplayName = eventContent?.displayName ?: prevEventContent?.displayName ?: event.stateKey ?: "" return when (eventContent?.membership) { @@ -706,17 +700,17 @@ class NoticeEventFormatter @Inject constructor( Membership.JOIN -> eventContent.safeReason?.let { reason -> if (event.isSentByCurrentUser()) { - sp.getString(if (rs.isDm()) R.string.notice_direct_room_join_with_reason_by_you else R.string.notice_room_join_with_reason_by_you, + sp.getString(if (isDm) R.string.notice_direct_room_join_with_reason_by_you else R.string.notice_room_join_with_reason_by_you, reason) } else { - sp.getString(if (rs.isDm()) R.string.notice_direct_room_join_with_reason else R.string.notice_room_join_with_reason, + sp.getString(if (isDm) R.string.notice_direct_room_join_with_reason else R.string.notice_room_join_with_reason, senderDisplayName, reason) } } ?: run { if (event.isSentByCurrentUser()) { - sp.getString(if (rs.isDm()) R.string.notice_direct_room_join_by_you else R.string.notice_room_join_by_you) + sp.getString(if (isDm) R.string.notice_direct_room_join_by_you else R.string.notice_room_join_by_you) } else { - sp.getString(if (rs.isDm()) R.string.notice_direct_room_join else R.string.notice_room_join, + sp.getString(if (isDm) R.string.notice_direct_room_join else R.string.notice_room_join, senderDisplayName) } } @@ -738,7 +732,7 @@ class NoticeEventFormatter @Inject constructor( eventContent.safeReason?.let { reason -> if (event.isSentByCurrentUser()) { sp.getString( - if (rs.isDm()) { + if (isDm) { R.string.notice_direct_room_leave_with_reason_by_you } else { R.string.notice_room_leave_with_reason_by_you @@ -746,14 +740,14 @@ class NoticeEventFormatter @Inject constructor( reason ) } else { - sp.getString(if (rs.isDm()) R.string.notice_direct_room_leave_with_reason else R.string.notice_room_leave_with_reason, + sp.getString(if (isDm) R.string.notice_direct_room_leave_with_reason else R.string.notice_room_leave_with_reason, senderDisplayName, reason) } } ?: run { if (event.isSentByCurrentUser()) { - sp.getString(if (rs.isDm()) R.string.notice_direct_room_leave_by_you else R.string.notice_room_leave_by_you) + sp.getString(if (isDm) R.string.notice_direct_room_leave_by_you else R.string.notice_room_leave_by_you) } else { - sp.getString(if (rs.isDm()) R.string.notice_direct_room_leave else R.string.notice_room_leave, + sp.getString(if (isDm) R.string.notice_direct_room_leave else R.string.notice_room_leave, senderDisplayName) } } @@ -818,14 +812,14 @@ class NoticeEventFormatter @Inject constructor( } } - private fun formatJoinRulesEvent(event: Event, senderName: String?, rs: RoomSummary?): CharSequence? { + private fun formatJoinRulesEvent(event: Event, senderName: String?, isDm: Boolean): CharSequence? { val content = event.getClearContent().toModel() ?: return null return when (content.joinRules) { RoomJoinRules.INVITE -> if (event.isSentByCurrentUser()) { - sp.getString(if (rs.isDm()) R.string.direct_room_join_rules_invite_by_you else R.string.room_join_rules_invite_by_you) + sp.getString(if (isDm) R.string.direct_room_join_rules_invite_by_you else R.string.room_join_rules_invite_by_you) } else { - sp.getString(if (rs.isDm()) R.string.direct_room_join_rules_invite else R.string.room_join_rules_invite, + sp.getString(if (isDm) R.string.direct_room_join_rules_invite else R.string.room_join_rules_invite, senderName) } RoomJoinRules.PUBLIC -> diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt index 221149aced..da75a808d8 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt @@ -34,6 +34,7 @@ import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.isAttachmentMessage import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.model.ReferencesAggregatedContent +import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationRequestContent import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent @@ -48,7 +49,6 @@ import javax.inject.Inject * This class compute if data of an event (such has avatar, display name, ...) should be displayed, depending on the previous event in the timeline */ class MessageInformationDataFactory @Inject constructor(private val session: Session, - private val roomSummariesHolder: RoomSummariesHolder, private val dateFormatter: VectorDateFormatter, private val visibilityHelper: TimelineEventVisibilityHelper, private val vectorPreferences: VectorPreferences) { @@ -74,7 +74,8 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses || nextDisplayableEvent.isEdition() val time = dateFormatter.format(event.root.originServerTs, DateFormatKind.MESSAGE_SIMPLE) - val e2eDecoration = getE2EDecoration(event) + val roomSummary = params.partialState.roomSummary + val e2eDecoration = getE2EDecoration(roomSummary, event) // SendState Decoration val isSentByMe = event.root.senderId == session.myUserId @@ -140,8 +141,7 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses } } - private fun getE2EDecoration(event: TimelineEvent): E2EDecoration { - val roomSummary = roomSummariesHolder.get(event.roomId) + private fun getE2EDecoration(roomSummary: RoomSummary?, event: TimelineEvent): E2EDecoration { return if ( event.root.sendState == SendState.SYNCED && roomSummary?.isEncrypted.orFalse() diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/RoomSummariesHolder.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/RoomSummariesHolder.kt deleted file mode 100644 index ac953f91f7..0000000000 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/RoomSummariesHolder.kt +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright (c) 2020 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package im.vector.app.features.home.room.detail.timeline.helper - -import org.matrix.android.sdk.api.session.room.model.RoomSummary -import javax.inject.Inject -import javax.inject.Singleton - -/* - You can use this to share room summary instances within the app. - You should probably use this only in the context of the timeline - */ -@Singleton -class RoomSummariesHolder @Inject constructor() { - - private var roomSummaries = HashMap() - - fun set(roomSummary: RoomSummary) { - roomSummaries[roomSummary.roomId] = roomSummary - } - - fun get(roomId: String) = roomSummaries[roomId] - - fun remove(roomId: String) = roomSummaries.remove(roomId) - - fun clear() { - roomSummaries.clear() - } -} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineControllerInterceptorHelper.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineControllerInterceptorHelper.kt index 3121f031e2..736da63ee2 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineControllerInterceptorHelper.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineControllerInterceptorHelper.kt @@ -19,12 +19,8 @@ package im.vector.app.features.home.room.detail.timeline.helper import com.airbnb.epoxy.EpoxyModel import com.airbnb.epoxy.VisibilityState import im.vector.app.core.epoxy.LoadingItem_ -import im.vector.app.core.epoxy.TimelineEmptyItem_ -import im.vector.app.core.resources.UserPreferencesProvider -import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.home.room.detail.UnreadState import im.vector.app.features.home.room.detail.timeline.TimelineEventController -import im.vector.app.features.home.room.detail.timeline.item.CallTileTimelineItem import im.vector.app.features.home.room.detail.timeline.item.DaySeparatorItem import im.vector.app.features.home.room.detail.timeline.item.ItemWithEvents import im.vector.app.features.home.room.detail.timeline.item.TimelineReadMarkerItem_ @@ -34,9 +30,7 @@ import kotlin.reflect.KMutableProperty0 private const val DEFAULT_PREFETCH_THRESHOLD = 30 class TimelineControllerInterceptorHelper(private val positionOfReadMarker: KMutableProperty0, - private val adapterPositionMapping: MutableMap, - private val userPreferencesProvider: UserPreferencesProvider, - private val callManager: WebRtcCallManager + private val adapterPositionMapping: MutableMap ) { private var previousModelsSize = 0 @@ -50,14 +44,12 @@ class TimelineControllerInterceptorHelper(private val positionOfReadMarker: KMut ) { positionOfReadMarker.set(null) adapterPositionMapping.clear() - val callIds = mutableSetOf() // Add some prefetch loader if needed models.addBackwardPrefetchIfNeeded(timeline, callback) models.addForwardPrefetchIfNeeded(timeline, callback) val modelsIterator = models.listIterator() - val showHiddenEvents = userPreferencesProvider.shouldShowHiddenEvents() var index = 0 val firstUnreadEventId = (unreadState as? UnreadState.HasUnread)?.firstUnreadEventId var atLeastOneVisibleItemSinceLastDaySeparator = false @@ -83,11 +75,6 @@ class TimelineControllerInterceptorHelper(private val positionOfReadMarker: KMut return@forEach } atLeastOneVisibleItemSinceLastDaySeparator = false - } else if (epoxyModel is CallTileTimelineItem) { - val hasBeenRemoved = modelsIterator.removeCallItemIfNeeded(epoxyModel, callIds, showHiddenEvents) - if (!hasBeenRemoved) { - atLeastOneVisibleItemSinceLastDaySeparator = true - } } if (appendReadMarker) { modelsIterator.addReadMarkerItem(callback) @@ -109,29 +96,6 @@ class TimelineControllerInterceptorHelper(private val positionOfReadMarker: KMut add(readMarker) } - private fun MutableListIterator>.removeCallItemIfNeeded( - epoxyModel: CallTileTimelineItem, - callIds: MutableSet, - showHiddenEvents: Boolean - ): Boolean { - val callId = epoxyModel.attributes.callId - // We should remove the call tile if we already have one for this call or - // if this is an active call tile without an actual call (which can happen with permalink) - val shouldRemoveCallItem = callIds.contains(callId) - || (!callManager.getAdvertisedCalls().contains(callId) && epoxyModel.attributes.callStatus.isActive()) - val removed = shouldRemoveCallItem && !showHiddenEvents - if (removed) { - remove() - val emptyItem = TimelineEmptyItem_() - .id(epoxyModel.id()) - .eventId(epoxyModel.attributes.informationData.eventId) - .notBlank(false) - add(emptyItem) - } - callIds.add(callId) - return removed - } - private fun MutableList>.addBackwardPrefetchIfNeeded(timeline: Timeline?, callback: TimelineEventController.Callback?) { val shouldAddBackwardPrefetch = timeline?.hasMoreToLoad(Timeline.Direction.BACKWARDS) ?: false if (shouldAddBackwardPrefetch) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventsGroups.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventsGroups.kt new file mode 100644 index 0000000000..3910204293 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventsGroups.kt @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.home.room.detail.timeline.helper + +import im.vector.app.core.utils.TextUtils +import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.matrix.android.sdk.api.session.widgets.model.WidgetContent +import org.threeten.bp.Duration + +class TimelineEventsGroup(val groupId: String) { + + val events: Set + get() = _events + + private val _events = HashSet() + + fun add(timelineEvent: TimelineEvent) { + _events.add(timelineEvent) + } +} + +class TimelineEventsGroups { + + private val groups = HashMap() + + fun addOrIgnore(event: TimelineEvent) { + val groupId = event.getGroupIdOrNull() ?: return + groups.getOrPut(groupId) { TimelineEventsGroup(groupId) }.add(event) + } + + fun getOrNull(event: TimelineEvent): TimelineEventsGroup? { + val groupId = event.getGroupIdOrNull() ?: return null + return groups[groupId] + } + + private fun TimelineEvent.getGroupIdOrNull(): String? { + val type = root.getClearType() + val content = root.getClearContent() + return if (EventType.isCallEvent(type)) { + (content?.get("call_id") as? String) + } else if (type == EventType.STATE_ROOM_WIDGET || type == EventType.STATE_ROOM_WIDGET_LEGACY) { + root.stateKey + } else { + null + } + } + + fun clear() { + groups.clear() + } +} + +class JitsiWidgetEventsGroup(private val group: TimelineEventsGroup) { + + val callId: String = group.groupId + + fun isStillActive(): Boolean { + return group.events.none { + it.root.getClearContent().toModel()?.isActive() == false + } + } +} + +class CallSignalingEventsGroup(private val group: TimelineEventsGroup) { + + val callId: String = group.groupId + + fun isVideo(): Boolean { + val invite = getInvite() ?: return false + return invite.root.getClearContent().toModel()?.isVideo().orFalse() + } + + fun isRinging(): Boolean { + return getAnswer() == null && getHangup() == null && getReject() == null + } + + fun isInCall(): Boolean { + return getHangup() == null && getReject() == null + } + + fun formattedDuration(): String { + val start = getAnswer()?.root?.originServerTs + val end = getHangup()?.root?.originServerTs + return if (start == null || end == null) { + "" + } else { + val durationInMillis = (end - start).coerceAtLeast(0L) + val duration = Duration.ofMillis(durationInMillis) + TextUtils.formatDuration(duration) + } + } + + /** + * Returns true if there are only events from one side. + */ + fun callWasMissed(): Boolean { + return group.events.distinctBy { it.senderInfo.userId }.size == 1 + } + + private fun getAnswer(): TimelineEvent? { + return group.events.firstOrNull { it.root.getClearType() == EventType.CALL_ANSWER } + } + + private fun getInvite(): TimelineEvent? { + return group.events.firstOrNull { it.root.getClearType() == EventType.CALL_INVITE } + } + + private fun getHangup(): TimelineEvent? { + return group.events.firstOrNull { it.root.getClearType() == EventType.CALL_HANGUP } + } + + private fun getReject(): TimelineEvent? { + return group.events.firstOrNull { it.root.getClearType() == EventType.CALL_REJECT } + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageItem.kt index fd5eea1b49..b53495fdaf 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageItem.kt @@ -42,6 +42,10 @@ abstract class AbsMessageItem : AbsBaseMessageItem override val baseAttributes: AbsBaseMessageItem.Attributes get() = attributes + override fun isCacheable(): Boolean { + return attributes.informationData.sendStateDecoration != SendStateDecoration.SENT + } + @EpoxyAttribute lateinit var attributes: Attributes diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/CallTileTimelineItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/CallTileTimelineItem.kt index 1f12bdbd2c..46392a494f 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/CallTileTimelineItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/CallTileTimelineItem.kt @@ -15,6 +15,7 @@ */ package im.vector.app.features.home.room.detail.timeline.item +import android.content.res.Resources import android.view.View import android.view.ViewGroup import android.widget.Button @@ -31,13 +32,11 @@ import im.vector.app.R import im.vector.app.core.epoxy.ClickListener import im.vector.app.core.epoxy.onClick import im.vector.app.core.extensions.setLeftDrawable -import im.vector.app.core.extensions.setTextWithColoredPart import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.detail.RoomDetailAction import im.vector.app.features.home.room.detail.timeline.MessageColorProvider import im.vector.app.features.home.room.detail.timeline.TimelineEventController import org.matrix.android.sdk.api.util.MatrixItem -import timber.log.Timber @EpoxyModelClass(layout = R.layout.item_timeline_event_base_state) abstract class CallTileTimelineItem : AbsBaseMessageItem() { @@ -45,6 +44,8 @@ abstract class CallTileTimelineItem : AbsBaseMessageItem renderInvitedStatus(holder) + CallStatus.IN_CALL -> renderInCallStatus(holder) + CallStatus.REJECTED -> renderRejectedStatus(holder) + CallStatus.ENDED -> renderEndedStatus(holder) + CallStatus.MISSED -> renderMissedStatus(holder) } - if (attributes.callStatus == CallStatus.INVITED && !attributes.informationData.sentByMe && attributes.isStillActive) { - holder.acceptRejectViewGroup.isVisible = true - holder.acceptView.onClick { - attributes.callback?.onTimelineItemAction(RoomDetailAction.AcceptCall(callId = attributes.callId)) + renderSendState(holder.view, null, holder.failedToSendIndicator) + } + + private fun renderMissedStatus(holder: Holder) { + // Sent by me means I made the call and opponent missed it. + if (attributes.informationData.sentByMe) { + if (attributes.callKind.isVoiceCall) { + holder.statusView.setStatus(R.string.call_tile_no_answer, R.drawable.ic_voice_call_declined) + } else { + holder.statusView.setStatus(R.string.call_tile_no_answer, R.drawable.ic_video_call_declined) } - holder.rejectView.setLeftDrawable(R.drawable.ic_call_hangup, R.attr.colorOnPrimary) - holder.rejectView.onClick { - attributes.callback?.onTimelineItemAction(RoomDetailAction.EndCall) + } else { + if (attributes.callKind.isVoiceCall) { + holder.statusView.setStatus(R.string.call_tile_voice_missed, R.drawable.ic_missed_voice_call_small) + } else { + holder.statusView.setStatus(R.string.call_tile_video_missed, R.drawable.ic_missed_video_call_small) } - holder.statusView.isVisible = false - when (attributes.callKind) { - CallKind.CONFERENCE -> { - holder.rejectView.setText(R.string.ignore) - holder.acceptView.setText(R.string.join) - holder.acceptView.setLeftDrawable(R.drawable.ic_call_audio_small, R.attr.colorOnPrimary) + } + holder.acceptRejectViewGroup.isVisible = true + holder.acceptView.setText(R.string.call_tile_call_back) + holder.acceptView.setLeftDrawable(attributes.callKind.icon, R.attr.colorOnPrimary) + holder.acceptView.onClick { + val callbackAction = RoomDetailAction.StartCall(attributes.callKind == CallKind.VIDEO) + attributes.callback?.onTimelineItemAction(callbackAction) + } + holder.rejectView.isVisible = false + } + + private fun renderEndedStatus(holder: Holder) { + holder.acceptRejectViewGroup.isVisible = false + when (attributes.callKind) { + CallKind.VIDEO -> { + val endCallStatus = holder.resources.getString(R.string.call_tile_video_call_has_ended, attributes.formattedDuration) + holder.statusView.setStatus(endCallStatus) + } + CallKind.AUDIO -> { + val endCallStatus = holder.resources.getString(R.string.call_tile_voice_call_has_ended, attributes.formattedDuration) + holder.statusView.setStatus(endCallStatus) + } + CallKind.CONFERENCE -> { + holder.statusView.setStatus(R.string.call_tile_ended) + } + } + } + + private fun renderRejectedStatus(holder: Holder) { + holder.acceptRejectViewGroup.isVisible = true + holder.acceptView.setText(R.string.call_tile_call_back) + holder.acceptView.setLeftDrawable(attributes.callKind.icon, R.attr.colorOnPrimary) + holder.acceptView.onClick { + val callbackAction = RoomDetailAction.StartCall(attributes.callKind == CallKind.VIDEO) + attributes.callback?.onTimelineItemAction(callbackAction) + } + holder.rejectView.isVisible = false + // Sent by me means I rejected the call made by opponent. + if (attributes.informationData.sentByMe) { + if (attributes.callKind.isVoiceCall) { + holder.statusView.setStatus(R.string.call_tile_voice_declined, R.drawable.ic_voice_call_declined) + } else { + holder.statusView.setStatus(R.string.call_tile_video_declined, R.drawable.ic_video_call_declined) + } + } else { + if (attributes.callKind.isVoiceCall) { + holder.statusView.setStatus(R.string.call_tile_no_answer, R.drawable.ic_voice_call_declined) + } else { + holder.statusView.setStatus(R.string.call_tile_no_answer, R.drawable.ic_video_call_declined) + } + } + } + + private fun renderInCallStatus(holder: Holder) { + holder.acceptRejectViewGroup.isVisible = true + holder.acceptView.isVisible = false + when { + attributes.callKind == CallKind.CONFERENCE -> { + holder.rejectView.isVisible = true + holder.rejectView.setText(R.string.leave) + holder.rejectView.setLeftDrawable(R.drawable.ic_call_hangup, R.attr.colorOnPrimary) + holder.rejectView.onClick { + attributes.callback?.onTimelineItemAction(RoomDetailAction.LeaveJitsiCall) } - CallKind.AUDIO -> { + } + attributes.isStillActive -> { + holder.rejectView.isVisible = true + holder.rejectView.setText(R.string.call_notification_hangup) + holder.rejectView.setLeftDrawable(R.drawable.ic_call_hangup, R.attr.colorOnPrimary) + holder.rejectView.onClick { + attributes.callback?.onTimelineItemAction(RoomDetailAction.EndCall) + } + } + else -> { + holder.acceptRejectViewGroup.isVisible = false + } + } + if (attributes.callKind.isVoiceCall) { + holder.statusView.setStatus(R.string.call_tile_voice_active) + } else { + holder.statusView.setStatus(R.string.call_tile_video_active) + } + } + + private fun renderInvitedStatus(holder: Holder) { + when { + attributes.callKind == CallKind.CONFERENCE -> { + holder.acceptRejectViewGroup.isVisible = true + holder.acceptView.onClick { + attributes.callback?.onTimelineItemAction(RoomDetailAction.JoinJitsiCall) + } + holder.acceptView.isVisible = true + holder.rejectView.isVisible = false + holder.acceptView.setText(R.string.join) + holder.acceptView.setLeftDrawable(R.drawable.ic_call_video_small, R.attr.colorOnPrimary) + } + !attributes.informationData.sentByMe && attributes.isStillActive -> { + holder.acceptRejectViewGroup.isVisible = true + holder.acceptView.isVisible = true + holder.rejectView.isVisible = true + holder.acceptView.onClick { + attributes.callback?.onTimelineItemAction(RoomDetailAction.AcceptCall(callId = attributes.callId)) + } + holder.rejectView.setLeftDrawable(R.drawable.ic_call_hangup, R.attr.colorOnPrimary) + holder.rejectView.onClick { + attributes.callback?.onTimelineItemAction(RoomDetailAction.EndCall) + } + if (attributes.callKind == CallKind.AUDIO) { holder.rejectView.setText(R.string.call_notification_reject) holder.acceptView.setText(R.string.call_notification_answer) holder.acceptView.setLeftDrawable(R.drawable.ic_call_audio_small, R.attr.colorOnPrimary) - } - CallKind.VIDEO -> { + } else if (attributes.callKind == CallKind.VIDEO) { holder.rejectView.setText(R.string.call_notification_reject) holder.acceptView.setText(R.string.call_notification_answer) holder.acceptView.setLeftDrawable(R.drawable.ic_call_video_small, R.attr.colorOnPrimary) } - else -> { - Timber.w("Shouldn't be in that state") - } } - } else { - holder.acceptRejectViewGroup.isVisible = false - holder.statusView.isVisible = true + else -> { + holder.acceptRejectViewGroup.isVisible = false + } + } + when { + // Invite state for conference should show as InCallStatus + attributes.callKind == CallKind.CONFERENCE -> { + holder.statusView.setStatus(R.string.call_tile_video_active) + } + attributes.informationData.sentByMe -> { + holder.statusView.setStatus(R.string.call_ringing) + } + attributes.callKind.isVoiceCall -> { + holder.statusView.setStatus(R.string.call_tile_voice_incoming) + } + else -> { + holder.statusView.setStatus(R.string.call_tile_video_incoming) + } } - holder.statusView.setCallStatus(attributes) - renderSendState(holder.view, null, holder.failedToSendIndicator) } - private fun TextView.setCallStatus(attributes: Attributes) { - when (attributes.callStatus) { - CallStatus.INVITED -> if (attributes.informationData.sentByMe) { - setText(R.string.call_tile_you_started_call) - } else { - text = context.getString(R.string.call_tile_other_started_call, attributes.userOfInterest.getBestName()) - } - CallStatus.IN_CALL -> setText(R.string.call_tile_in_call) - CallStatus.REJECTED -> if (attributes.informationData.sentByMe) { - setTextWithColoredPart(R.string.call_tile_you_declined, R.string.call_tile_call_back) { - val callbackAction = RoomDetailAction.StartCall(attributes.callKind == CallKind.VIDEO) - attributes.callback?.onTimelineItemAction(callbackAction) - } - } else { - text = context.getString(R.string.call_tile_other_declined, attributes.userOfInterest.getBestName()) - } - CallStatus.ENDED -> setText(R.string.call_tile_ended) - } + private fun TextView.setStatus(@StringRes statusRes: Int, @DrawableRes drawableRes: Int? = null) { + val status = resources.getString(statusRes) + setStatus(status, drawableRes) + } + + private fun TextView.setStatus(status: String, @DrawableRes drawableRes: Int? = null) { + setLeftDrawable(drawableRes ?: attributes.callKind.icon) + text = status } class Holder : AbsBaseMessageItem.Holder(STUB_ID) { val acceptView by bind