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..51fe7a7bd1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/release.yml @@ -0,0 +1,89 @@ +name: Release checklist +description: Checklist for each release. This template is only for the core team. +title: "[Release] Element Android v" +labels: [\U0001F680 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` + - [ ] 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` + validations: + required: true diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index cb6f1b0e48..984ae0748e 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -6,12 +6,31 @@ on: branches: [ main, develop ] 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 --stacktrace -PallWarningsAsErrors=false + integration-tests: name: Integration Tests (Synapse) runs-on: ubuntu-latest strategy: + fail-fast: false matrix: - api-level: [21, 30] + api-level: [21, 28, 30] steps: - uses: actions/checkout@v2 - name: Set up Python 3.8 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..632dee8a58 --- /dev/null +++ b/.github/workflows/sanity_test.yml @@ -0,0 +1,50 @@ +name: Sanity Test + +on: + pull_request: { } + push: + branches: [ main, develop ] + +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 -PallWarningsAsErrors=false connectedGplayDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=im.vector.app.ui.UiAllScreensSanityTest + diff --git a/CHANGES.md b/CHANGES.md index 640d56a9fd..472b422a1c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,53 @@ +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..610a6227b7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -116,9 +116,34 @@ 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_`. diff --git a/attachment-viewer/build.gradle b/attachment-viewer/build.gradle index c393c5f483..d62062ae14 100644 --- a/attachment-viewer/build.gradle +++ b/attachment-viewer/build.gradle @@ -34,11 +34,11 @@ android { } } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_11 + targetCompatibility JavaVersion.VERSION_11 } kotlinOptions { - jvmTarget = '1.8' + jvmTarget = "11" } buildFeatures { 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..ef26530298 100644 --- a/build.gradle +++ b/build.gradle @@ -12,8 +12,10 @@ buildscript { } } dependencies { - classpath 'com.android.tools.build:gradle:4.2.2' - classpath 'com.google.gms:google-services:4.3.8' + // Release notes of Android Gradle Plugin (AGP): + // https://developer.android.com/studio/releases/gradle-plugin + classpath 'com.android.tools.build:gradle:7.0.2' + classpath 'com.google.gms:google-services:4.3.10' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.3' classpath 'com.google.android.gms:oss-licenses-plugin:0.10.4' @@ -44,6 +46,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/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/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/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/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..678ad05309 --- /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.1.16 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..c7e159bf2b --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/40102000.txt @@ -0,0 +1,2 @@ +تغییرهای اصلی در این نگارش: پیام صوتی به صورت پیش‌گزیده به کار افتاده. +گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases/tag/v1.1.16 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/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..1e708b1c7b --- /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.1.16 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..3ba7f8ceb3 --- /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.1.16 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..3c600baeed --- /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.1.16 diff --git a/fastlane/metadata/android/sq/title.txt b/fastlane/metadata/android/sq/title.txt new file mode 100644 index 0000000000..b46bbc02b1 --- /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..07defcbb57 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/40102000.txt @@ -0,0 +1,2 @@ +Основні зміни в цій версії: голосові повідомлення типово увімкнено. +Повний журнал змін: https://github.com/vector-im/element-android/releases/tag/v1.1.16 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..eedbe81bac --- /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.1.16 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/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..365e4b902c 100644 --- a/library/ui-styles/build.gradle +++ b/library/ui-styles/build.gradle @@ -39,13 +39,16 @@ android { proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } + compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_11 + targetCompatibility JavaVersion.VERSION_11 } + kotlinOptions { - jvmTarget = '1.8' + jvmTarget = "11" } + buildFeatures { viewBinding true } 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/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..d18e3b1d72 100644 --- a/matrix-sdk-android-rx/build.gradle +++ b/matrix-sdk-android-rx/build.gradle @@ -24,12 +24,12 @@ android { } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_11 + targetCompatibility JavaVersion.VERSION_11 } kotlinOptions { - jvmTarget = "1.8" + jvmTarget = "11" } } @@ -44,5 +44,5 @@ dependencies { implementation "androidx.paging:paging-runtime-ktx:2.1.2" // Logging - implementation 'com.jakewharton.timber:timber:4.7.1' + implementation 'com.jakewharton.timber:timber:5.0.1' } diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index 96c9fdb1e6..cbae6a05b3 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -9,7 +9,7 @@ buildscript { mavenCentral() } dependencies { - classpath "io.realm:realm-gradle-plugin:10.6.1" + classpath "io.realm:realm-gradle-plugin:10.8.0" } } @@ -67,17 +67,13 @@ android { installOptions "-g" } - lintOptions { - lintConfig file("lint.xml") - } - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_11 + targetCompatibility JavaVersion.VERSION_11 } kotlinOptions { - jvmTarget = "1.8" + jvmTarget = "11" } sourceSets { @@ -112,7 +108,7 @@ dependencies { def lifecycle_version = '2.2.0' def arch_version = '2.1.0' def markwon_version = '3.1.0' - def daggerVersion = '2.38' + def daggerVersion = '2.38.1' def work_version = '2.5.0' def retrofit_version = '2.9.0' @@ -141,7 +137,7 @@ dependencies { implementation "ru.noties.markwon:core:$markwon_version" // Image - implementation 'androidx.exifinterface:exifinterface:1.3.2' + implementation 'androidx.exifinterface:exifinterface:1.3.3' // Database implementation 'com.github.Zhuinden:realm-monarchy:0.7.1' @@ -162,14 +158,14 @@ dependencies { kapt "com.google.dagger:dagger-compiler:$daggerVersion" // Logging - implementation 'com.jakewharton.timber:timber:4.7.1' + implementation 'com.jakewharton.timber:timber:5.0.1' implementation 'com.facebook.stetho:stetho-okhttp3:1.6.0' // Video compression implementation 'com.otaliastudios:transcoder:0.10.3' // Phone number https://github.com/google/libphonenumber - implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.28' + implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.31' testImplementation 'junit:junit:4.13.2' testImplementation 'org.robolectric:robolectric:4.5.1' 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/common/CommonTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt index 6e07223ac7..7817351e53 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 @@ -91,6 +91,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 +328,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..301cdea461 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 @@ -47,6 +47,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 +172,7 @@ class SpaceHierarchyTest : InstrumentedTest { // } @Test + @Suppress("EXPERIMENTAL_API_USAGE") fun testFilteringBySpace() { val session = commonTestHelper.createAccount("John", SessionTestParams(true)) @@ -179,7 +181,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 +256,7 @@ class SpaceHierarchyTest : InstrumentedTest { } @Test + @Suppress("EXPERIMENTAL_API_USAGE") fun testBreakCycle() { val session = commonTestHelper.createAccount("John", SessionTestParams(true)) @@ -301,6 +304,7 @@ class SpaceHierarchyTest : InstrumentedTest { } @Test + @Suppress("EXPERIMENTAL_API_USAGE") fun testLiveFlatChildren() { val session = commonTestHelper.createAccount("John", SessionTestParams(true)) @@ -389,6 +393,7 @@ class SpaceHierarchyTest : InstrumentedTest { val roomIds: List ) + @Suppress("EXPERIMENTAL_API_USAGE") private fun createPublicSpace(session: Session, spaceName: String, childInfo: List> @@ -433,7 +438,7 @@ class SpaceHierarchyTest : InstrumentedTest { 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) )) 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 @@ - - - +
    +
  • + 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/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt index cce0c2a66f..6b42f1e428 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 @@ -717,6 +718,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) 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..68b212c830 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 @@ -31,6 +31,7 @@ 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 +40,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 @@ -165,7 +165,7 @@ interface VectorComponent { fun webRtcCallManager(): WebRtcCallManager - fun roomSummaryHolder(): RoomSummariesHolder + fun jitsiActiveConferenceHolder(): JitsiActiveConferenceHolder @Component.Factory interface Factory { diff --git a/vector/src/main/java/im/vector/app/core/epoxy/profiles/notifications/NotificationSettingsFooterItem.kt b/vector/src/main/java/im/vector/app/core/epoxy/profiles/notifications/NotificationSettingsFooterItem.kt new file mode 100644 index 0000000000..4608f2b1ce --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/epoxy/profiles/notifications/NotificationSettingsFooterItem.kt @@ -0,0 +1,62 @@ +/* + * 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 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.extensions.setTextWithColoredPart + +@EpoxyModelClass(layout = R.layout.item_notifications_footer) +abstract class NotificationSettingsFooterItem : 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..37d16ab6b1 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/epoxy/profiles/notifications/RadioButtonItem.kt @@ -0,0 +1,69 @@ +/* + * 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 + +@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.setImageDrawable(ContextCompat.getDrawable(holder.view.context, R.drawable.ic_radio_on)) + 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..1c424f7071 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 @@ -65,6 +65,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/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/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/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/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..5aee73ee69 --- /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 = currentCall != null + 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/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/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/member/AutocompleteMemberPresenter.kt b/vector/src/main/java/im/vector/app/features/autocomplete/member/AutocompleteMemberPresenter.kt index ecc607f08d..aa0c10e0a2 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 @@ -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..fb5e48af98 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,14 +32,12 @@ 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()) } } 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..63ba83bdbc 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 ) } } @@ -137,16 +137,14 @@ class VectorCallViewModel @AssistedInject constructor( private val currentCallListener = object : WebRtcCallManager.CurrentCallListener { 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() @@ -174,7 +172,12 @@ class VectorCallViewModel @AssistedInject constructor( } 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"))) @@ -184,17 +187,19 @@ class VectorCallViewModel @AssistedInject constructor( callManager.addCurrentCallListener(currentCallListener) 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(), @@ -227,6 +232,7 @@ class VectorCallViewModel @AssistedInject constructor( override fun onCleared() { callManager.removeCurrentCallListener(currentCallListener) call?.removeListener(callListener) + call = null proximityManager.stop() super.onCleared() } @@ -304,9 +310,13 @@ class VectorCallViewModel @AssistedInject constructor( VectorCallViewEvents.ShowCallTransferScreen ) } - VectorCallViewActions.TransferCall -> { + 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/JitsiActiveConferenceHolder.kt b/vector/src/main/java/im/vector/app/features/call/conference/JitsiActiveConferenceHolder.kt new file mode 100644 index 0000000000..1a9fc5ea10 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/call/conference/JitsiActiveConferenceHolder.kt @@ -0,0 +1,46 @@ +/* + * 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.jitsi.meet.sdk.BroadcastEvent +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(JitsiBroadcastEventObserver(context, this::onBroadcastEvent)) + } + + fun isJoined(confId: String?): Boolean { + return confId != null && activeConference?.endsWith(confId).orFalse() + } + + private fun onBroadcastEvent(broadcastEvent: BroadcastEvent) { + when (broadcastEvent.type) { + BroadcastEvent.Type.CONFERENCE_JOINED -> activeConference = broadcastEvent.extractConferenceUrl() + BroadcastEvent.Type.CONFERENCE_TERMINATED -> activeConference = null + else -> Unit + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/call/conference/JitsiBroadcastEvent.kt b/vector/src/main/java/im/vector/app/features/call/conference/JitsiBroadcastEvent.kt new file mode 100644 index 0000000000..00ad7c540e --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/call/conference/JitsiBroadcastEvent.kt @@ -0,0 +1,84 @@ +/* + * 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 org.matrix.android.sdk.api.extensions.tryOrNull + +private const val CONFERENCE_URL_DATA_KEY = "url" + +fun BroadcastEvent.extractConferenceUrl(): String? { + return when (type) { + BroadcastEvent.Type.CONFERENCE_TERMINATED, + BroadcastEvent.Type.CONFERENCE_WILL_JOIN, + BroadcastEvent.Type.CONFERENCE_JOINED -> data[CONFERENCE_URL_DATA_KEY] as? String + else -> null + } +} + +class JitsiBroadcastEmitter(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 JitsiBroadcastEventObserver(private val context: Context, + private val onBroadcastEvent: (BroadcastEvent) -> 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() { + tryOrNull("Unable to unregister receiver") { + LocalBroadcastManager.getInstance(context).unregisterReceiver(broadcastReceiver) + } + } + + @OnLifecycleEvent(Lifecycle.Event.ON_CREATE) + fun registerForBroadcastMessages() { + val intentFilter = IntentFilter() + for (type in BroadcastEvent.Type.values()) { + intentFilter.addAction(type.action) + } + tryOrNull("Unable to register receiver") { + LocalBroadcastManager.getInstance(context).registerReceiver(broadcastReceiver, intentFilter) + } + } + + private fun onBroadcastReceived(intent: Intent) { + val event = BroadcastEvent(intent) + onBroadcastEvent(event) + } +} 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..a7a6f99cfc 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 @@ -41,6 +40,7 @@ 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 @@ -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(JitsiBroadcastEventObserver(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) { + JitsiBroadcastEmitter(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,24 +223,7 @@ 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 unregisterForBroadcastMessages() { - tryOrNull("Unable to unregister receiver") { - LocalBroadcastManager.getInstance(this).unregisterReceiver(broadcastReceiver) - } - } - - private fun onBroadcastReceived(intent: Intent) { - val event = BroadcastEvent(intent) + private fun onBroadcastEvent(event: BroadcastEvent) { Timber.v("Broadcast received: ${event.type}") when (event.type) { BroadcastEvent.Type.CONFERENCE_TERMINATED -> onConferenceTerminated(event.data) diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt index 91d3ab7ddf..2d39fda2e3 100644 --- a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt @@ -39,7 +39,9 @@ import io.reactivex.disposables.Disposable import io.reactivex.subjects.PublishSubject import io.reactivex.subjects.ReplaySubject import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Deferred import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -91,6 +93,7 @@ private const val STREAM_ID = "userMedia" private const val AUDIO_TRACK_ID = "${STREAM_ID}a0" private const val VIDEO_TRACK_ID = "${STREAM_ID}v0" private val DEFAULT_AUDIO_CONSTRAINTS = MediaConstraints() +private const val INVITE_TIMEOUT_IN_MS = 60_000L private val loggerTag = LoggerTag("WebRtcCall", LoggerTag.VOIP) @@ -165,12 +168,14 @@ class WebRtcCall( } } + private var inviteTimeout: Deferred? = 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 @@ -807,7 +816,7 @@ class WebRtcCall( return@launch } val reject = mxCall.state is CallState.LocalRinging - terminate(EndCallReason.USER_HANGUP, reject) + terminate(reason, reject) if (reject) { mxCall.reject() } else { @@ -824,6 +833,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 +856,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/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/home/AvatarRenderer.kt b/vector/src/main/java/im/vector/app/features/home/AvatarRenderer.kt index 787027e0e2..d22707bda0 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 @@ -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..a302276f45 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 @@ -58,6 +58,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 @@ -89,6 +90,7 @@ class HomeActivity : UnknownDeviceDetectorSharedViewModel.Factory, ServerBackupStatusViewModel.Factory, UnreadMessagesSharedViewModel.Factory, + PromoteRestrictedViewModel.Factory, NavigationInterceptor, SpaceInviteBottomSheet.InteractionListener { @@ -99,6 +101,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 +147,8 @@ class HomeActivity : } } + override fun getCoordinatorLayout() = views.coordinatorLayout + override fun getBinding() = ActivityHomeBinding.inflate(layoutInflater) override fun injectWith(injector: ScreenComponent) { @@ -171,18 +177,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 +197,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 +213,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 @@ -232,9 +233,9 @@ class HomeActivity : 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 +243,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) } @@ -287,7 +303,7 @@ class HomeActivity : private fun renderState(state: HomeActivityViewState) { when (val status = state.initialSyncProgressServiceStatus) { - is InitialSyncProgressService.Status.Idle -> { + is InitialSyncProgressService.Status.Idle -> { views.waitingView.root.isVisible = false } is InitialSyncProgressService.Status.Progressing -> { @@ -451,15 +467,15 @@ class HomeActivity : 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 +489,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 } @@ -548,4 +564,6 @@ class HomeActivity : 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/HomeDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt index 13809bcef3..9b71d1c90c 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 @@ -39,8 +38,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 @@ -117,7 +116,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,11 +189,16 @@ 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 @@ -291,12 +295,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() { @@ -373,7 +372,7 @@ class HomeDetailFragment @Inject constructor( add(R.id.roomListContainer, RoomListFragment::class.java, params.toMvRxBundle(), fragmentTag) } is HomeTab.DialPad -> { - add(R.id.roomListContainer, createDialPadFragment()) + add(R.id.roomListContainer, createDialPadFragment(), fragmentTag) } } } else { 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..f02711690a 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 @@ -147,7 +147,7 @@ class UnreadMessagesSharedViewModel @AssistedInject constructor(@Assisted initia roomSummaryQueryParams { this.memberships = listOf(Membership.JOIN) this.activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(null).takeIf { - vectorPreferences.labsSpacesOnlyOrphansInHome() + !vectorPreferences.prefSpacesShowAllRoomInHome() } ?: ActiveSpaceFilter.None } ) 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..94388dcfeb 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 org.jitsi.meet.sdk.BroadcastEvent 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 jitsiEvent: BroadcastEvent): 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..d45aa69cf3 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 @@ -88,10 +88,10 @@ import im.vector.app.core.intent.getMimeTypeFromUri import im.vector.app.core.platform.VectorBaseFragment 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 +123,8 @@ 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.JitsiBroadcastEmitter +import im.vector.app.features.call.conference.JitsiBroadcastEventObserver import im.vector.app.features.call.conference.JitsiCallViewModel import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.command.Command @@ -182,6 +184,7 @@ import nl.dionsegijn.konfetti.models.Shape import nl.dionsegijn.konfetti.models.Size import org.billcarsonfr.jsonviewer.JSonViewerDialog import org.commonmark.parser.Parser +import org.jitsi.meet.sdk.BroadcastEvent import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.content.ContentAttachmentData @@ -307,7 +310,7 @@ 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 @@ -321,6 +324,7 @@ class RoomDetailFragment @Inject constructor( } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + lifecycle.addObserver(JitsiBroadcastEventObserver(vectorBaseActivity, this::onBroadcastJitsiEvent)) super.onViewCreated(view, savedInstanceState) sharedActionViewModel = activityViewModelProvider.get(MessageSharedActionViewModel::class.java) knownCallsViewModel = activityViewModelProvider.get(SharedKnownCallsViewModel::class.java) @@ -344,9 +348,9 @@ class RoomDetailFragment @Inject constructor( setupJumpToReadMarkerView() setupActiveCallView() setupJumpToBottomView() - setupConfBannerView() setupEmojiPopup() setupFailedMessagesWarningView() + setupRemoveJitsiWidgetView() setupVoiceMessageView() views.roomToolbarContentView.debouncedClicks { @@ -363,7 +367,7 @@ class RoomDetailFragment @Inject constructor( knownCallsViewModel .liveKnownCalls .observe(viewLifecycleOwner, { - knownCallsViewHolder.updateCall(callManager.getCurrentCall(), it) + currentCallsViewPresenter.updateCall(callManager.getCurrentCall(), it) invalidateOptionsMenu() }) @@ -412,6 +416,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 +441,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() { + JitsiBroadcastEmitter(vectorBaseActivity).emitConferenceEnded() + } + + private fun onBroadcastJitsiEvent(jitsiEvent: BroadcastEvent) { + roomDetailViewModel.handle(RoomDetailAction.UpdateJoinJitsiCallStatus(jitsiEvent)) + } + private fun onCannotRecord() { // Update the UI, cancel the animation views.voiceMessageRecorderView.initVoiceRecordingViews() @@ -559,31 +584,6 @@ 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 setupEmojiPopup() { emojiPopup = EmojiPopup .Builder @@ -769,7 +769,7 @@ class RoomDetailFragment @Inject constructor( override fun onDestroyView() { timelineEventController.callback = null timelineEventController.removeModelBuildListener(modelBuildListener) - views.activeCallView.callback = null + currentCallsViewPresenter.unBind() modelBuildListener = null autoCompleter.clear() debouncer.cancelAll() @@ -780,7 +780,6 @@ class RoomDetailFragment @Inject constructor( } override fun onDestroy() { - knownCallsViewHolder.unBind() roomDetailViewModel.handle(RoomDetailAction.ExitTrackingUnreadMessagesState) super.onDestroy() } @@ -816,12 +815,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 +866,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 +878,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 +890,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 +899,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 +932,6 @@ class RoomDetailFragment @Inject constructor( callActionsHandler.onVideoCallClicked() true } - R.id.hangup_call -> { - roomDetailViewModel.handle(RoomDetailAction.EndCall) - true - } R.id.search -> { handleSearchAction() true @@ -1362,7 +1358,7 @@ class RoomDetailFragment @Inject constructor( invalidateOptionsMenu() val summary = state.asyncRoomSummary() renderToolbar(summary, state.typingMessage) - views.activeConferenceView.render(state) + views.removeJitsiWidgetView.render(state) views.failedMessagesWarningView.render(state.hasFailedSending) val inviter = state.asyncInviter() if (summary?.membership == Membership.JOIN) { 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..3e902dc2ef 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 @@ -38,6 +38,7 @@ import im.vector.app.core.extensions.exhaustive 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.call.conference.JitsiActiveConferenceHolder import im.vector.app.features.attachments.toContentAttachmentData import im.vector.app.features.call.conference.JitsiService import im.vector.app.features.call.lookup.CallProtocolsChecker @@ -51,7 +52,6 @@ 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 @@ -66,6 +66,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.commonmark.parser.Parser import org.commonmark.renderer.html.HtmlRenderer +import org.jitsi.meet.sdk.BroadcastEvent import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixPatterns import org.matrix.android.sdk.api.extensions.tryOrNull @@ -115,12 +116,12 @@ class RoomDetailViewModel @AssistedInject constructor( 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 @@ -241,9 +242,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 +325,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 +360,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.jitsiEvent.type) { + BroadcastEvent.Type.CONFERENCE_JOINED, + BroadcastEvent.Type.CONFERENCE_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 +495,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 +519,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 +738,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 @@ -1515,7 +1572,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 +1619,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..1c75429d11 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,6 +19,7 @@ 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.room.members.ChangeMembershipState import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary @@ -26,6 +27,7 @@ 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 +57,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?, @@ -75,7 +85,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 +96,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..5f4f53cf1e 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 @@ -40,11 +40,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 +50,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 +76,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 +96,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 { 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..8be319f2a8 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 @@ -40,6 +39,7 @@ import im.vector.app.features.home.room.detail.timeline.factory.MergedHeaderItem import im.vector.app.features.home.room.detail.timeline.factory.ReadReceiptsItemFactory import im.vector.app.features.home.room.detail.timeline.factory.TimelineItemFactory import im.vector.app.features.home.room.detail.timeline.factory.TimelineItemFactoryParams +import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventsGroups import im.vector.app.features.home.room.detail.timeline.helper.ContentDownloadStateTrackerBinder import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder import im.vector.app.features.home.room.detail.timeline.helper.TimelineControllerInterceptorHelper @@ -47,14 +47,13 @@ import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventDiff 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.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() } } @@ -346,31 +354,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 +394,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 +410,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 +443,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 +454,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 +474,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 +546,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