diff --git a/.editorconfig b/.editorconfig index 0a49eadc0b..1966f91763 100644 --- a/.editorconfig +++ b/.editorconfig @@ -9,25 +9,6 @@ insert_final_newline=true # it's automatically set to 100 on `ktlint --android ...` (per Android Kotlin Style Guide) max_line_length=off -# Comma-separated list of rules to disable (Since 0.34.0) -# Note that rules in any ruleset other than the standard ruleset will need to be prefixed -# by the ruleset identifier. -disabled_rules=no-multi-spaces,colon-spacing,chain-wrapping,import-ordering,experimental:annotation - -# The following (so far identified) rules are kept: -# no-blank-line-before-rbrace -# final-newline -# no-consecutive-blank-lines -# comment-spacing -# filename -# comma-spacing -# paren-spacing -# op-spacing -# string-template -# no-unused-imports -# curly-spacing -# no-semi -# no-empty-class-body -# experimental:multiline-if-else -# experimental:no-empty-first-line-in-method-block -# no-wildcard-imports +# From https://github.com/pinterest/ktlint#custom-ktlint-specific-editorconfig-properties +# default IntelliJ IDEA style, same as alphabetical, but with "java", "javax", "kotlin" and alias imports in the end of the imports list +ij_kotlin_imports_layout=*,java.**,javax.**,kotlin.**,^ diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index dcb9f0a766..c1ab98e85d 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -23,12 +23,12 @@ body: - type: textarea id: result attributes: - label: What happened? + label: Intended result and actual result placeholder: Tell us what went wrong value: | - ### What did you expect? + #### What did you expect? - ### What happened? + #### What happened instead? validations: required: true - type: input @@ -64,9 +64,9 @@ body: - type: dropdown id: rageshake attributes: - label: Have you submitted a rageshake? + label: Will you send logs? 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. + 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 (it's helpful if you can include a link to the bug). Send the report to submit anonymous logs to the developers. options: - 'Yes' - 'No' diff --git a/.github/ISSUE_TEMPLATE/release.yml b/.github/ISSUE_TEMPLATE/release.yml index 44e66fe9cd..7ac55427a9 100644 --- a/.github/ISSUE_TEMPLATE/release.yml +++ b/.github/ISSUE_TEMPLATE/release.yml @@ -1,7 +1,7 @@ name: Release checklist description: Checklist for each release. This template is only for the core team. title: "[Release] Element Android v" -labels: [\U0001F680 Release] +labels: [🚀 Release] assignees: - bmarty @@ -23,8 +23,6 @@ body: ### 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()` @@ -71,14 +69,14 @@ body: https://github.com/matrix-org/matrix-android-sdk2 - [ ] Create a release with GitFlow + - [ ] Update the value of VERSION_NAME in the file gradle.properties - [ ] Update the files `./build.gradle` and `./gradle/gradle-wrapper.properties` manually, to use the latest version for the dependency. You can get inspired by the same files on Element Android project. - [ ] Run the script `./tools/import_from_element.sh` - - [ ] Update the version in `./matrix-sdk-android/build.gradle` - - [ ] Check the diff on this file and restore what may have been erased (in particular the line `apply plugin: "com.vanniktech.maven.publish"`) + - [ ] Check the diff in the file `./matrix-sdk-android/build.gradle` and restore what may have been erased (in particular the line `apply plugin: "com.vanniktech.maven.publish"` and the line about the version) - [ ] Let the script finish to build the library - [ ] Update the file `CHANGES.md` - - [ ] Update the value of VERSION_NAME in the file gradle.properties - [ ] Finish the release using GitFlow + - [ ] Push the branch `main`, the new tag and the branch `develop` to origin ##### Release on MavenCentral diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 8fbc5602fe..eb30c18fcf 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,10 +1,16 @@ ### Pull Request Checklist - + - [ ] Changes has been tested on an Android device or Android emulator with API 21 - [ ] UI change has been tested on both light and dark themes +- [ ] Accessibility has been taken into account. See https://github.com/vector-im/element-android/blob/develop/CONTRIBUTING.md#accessibility - [ ] Pull request is based on the develop branch - [ ] Pull request includes a new file under ./changelog.d. See https://github.com/vector-im/element-android/blob/develop/CONTRIBUTING.md#changelog - [ ] Pull request includes screenshots or videos if containing UI changes -- [ ] Pull request includes a [sign off](https://github.com/matrix-org/synapse/blob/master/CONTRIBUTING.md#sign-off) +- [ ] Pull request includes a [sign off](https://matrix-org.github.io/synapse/latest/development/contributing_guide.html#sign-off) +- [ ] You've made a self review of your PR +- [ ] If you have modified the screen flow, or added new screens to the application, you have updated the test [UiAllScreensSanityTest.allScreensTest()](https://github.com/vector-im/element-android/blob/main/vector/src/androidTest/java/im/vector/app/ui/UiAllScreensSanityTest.kt#L73) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index e552f5fd43..8c2f1041e0 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -18,6 +18,5 @@ updates: open-pull-requests-limit: 200 reviewers: - "bmarty" -### ignore: -### - dependency-name: com.squareup.okhttp3:logging-interceptor -### versions: "> 3.12.10" + ignore: + - dependency-name: com.google.zxing:core diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 0f11915258..5ccd00a02b 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -14,15 +14,19 @@ jobs: - name: Run code quality check suite run: ./tools/check/check_code_quality.sh - klint: + ktlint: name: Kotlin Linter runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - name: Run klint + - name: Run ktlint run: | - curl -sSLO https://github.com/pinterest/ktlint/releases/download/0.36.0/ktlint && chmod a+x ktlint - ./ktlint --android --experimental -v + ./gradlew ktlintCheck --continue + - name: Upload reports + uses: actions/upload-artifact@v2 + with: + name: ktlinting-report + path: vector/build/reports/ktlint/*.* # Lint for main module and all the other modules android-lint: diff --git a/.github/workflows/sync-from-external-sources.yml b/.github/workflows/sync-from-external-sources.yml new file mode 100644 index 0000000000..6a4f8ef147 --- /dev/null +++ b/.github/workflows/sync-from-external-sources.yml @@ -0,0 +1,69 @@ +name: Sync Data From External Sources +on: + schedule: + # At 00:00 on every Monday UTC + - cron: '0 0 * * 1' + +jobs: + sync-emojis: + runs-on: ubuntu-latest + 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 }}- + - name: Install Prerequisite dependencies + run: | + pip install BeautifulSoup4 + pip install requests + - name: Run Emoji script + run: ./tools/import_emojis.py + - name: Create Pull Request for Emojis + uses: peter-evans/create-pull-request@v3 + with: + commit-message: Sync Emojis + title: Sync Emojis + body: | + - Update Emojis from Unicode.org + branch: sync-emojis + base: develop + + sync-sas-strings: + runs-on: ubuntu-latest + 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 }}- + - name: Install Prerequisite dependencies + run: | + pip install requests + - name: Run SAS String script + run: ./tools/import_sas_strings.py + - name: Create Pull Request for SAS Strings + uses: peter-evans/create-pull-request@v3 + with: + commit-message: Sync SAS Strings + title: Sync SAS Strings + body: | + - Update SAS Strings from matrix-doc. + branch: sync-sas-strings + base: develop \ No newline at end of file diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1822bd7093..50195638de 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -29,6 +29,8 @@ jobs: run: ./gradlew clean test $CI_GRADLE_ARG_PROPERTIES -PallWarningsAsErrors=false --stacktrace - name: Publish Unit Test Results uses: EnricoMi/publish-unit-test-result-action@v1 - if: always() + if: always() && + github.event.sender.login != 'dependabot[bot]' && + ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository ) with: files: ./**/build/test-results/**/*.xml diff --git a/.github/workflows/triage-incoming.yml b/.github/workflows/triage-incoming.yml new file mode 100644 index 0000000000..40d5507415 --- /dev/null +++ b/.github/workflows/triage-incoming.yml @@ -0,0 +1,15 @@ +name: Move new issues onto Issue triage board + +on: + issues: + types: [opened] + +jobs: + automate-project-columns: + runs-on: ubuntu-latest + steps: + - uses: alex-page/github-project-automation-plus@v0.8.1 + with: + project: Issue triage + column: Incoming + repo-token: ${{ secrets.ELEMENT_BOT_TOKEN }} diff --git a/.github/workflows/triage-needs-info.yml b/.github/workflows/triage-needs-info.yml new file mode 100644 index 0000000000..64de7951c6 --- /dev/null +++ b/.github/workflows/triage-needs-info.yml @@ -0,0 +1,16 @@ +name: Move X-Needs-Info into Need info column in the Issue triage board + +on: + issues: + types: [labeled] + +jobs: + Move_Labeled_Issue_On_Project_Board: + runs-on: ubuntu-latest + steps: + - uses: konradpabjan/move-labeled-or-milestoned-issue@v2.0 + with: + action-token: ${{ secrets.GITHUB_TOKEN }} + project-url: "https://github.com/vector-im/element-android/projects/4" + column-name: "Need info" + label-name: "X-Needs-Info" diff --git a/.gitignore b/.gitignore index 04d1b6fe06..ff086d7723 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ .idea/*.xml .DS_Store /build +/benchmark-out /captures .externalNativeBuild @@ -15,4 +16,4 @@ /fastlane/private /fastlane/report.xml -ktlint +/library/build diff --git a/CHANGES.md b/CHANGES.md index 58d88cb89c..3824fead36 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,166 @@ +Changes in Element v1.3.6 (2021-10-26) +====================================== + +Bugfixes 🐛 +---------- + - Correctly handle url of type https://mobile.element.io/?hs_url=…&is_url=… + Skip the choose server screen when such URL are open when Element ([#2684](https://github.com/vector-im/element-android/issues/2684)) + + +Changes in Element v1.3.5 (2021-10-25) +====================================== + +Bugfixes 🐛 +---------- + - Fixing malformed link pop up when tapping on notifications ([#4267](https://github.com/vector-im/element-android/issues/4267)) + - Fix Broken EditText when using FromEditTextItem ([#4276](https://github.com/vector-im/element-android/issues/4276)) + - Fix crash when clicking on ViewEvent source actions ([#4279](https://github.com/vector-im/element-android/issues/4279)) + - Fix voice message record button wrong visibility ([#4283](https://github.com/vector-im/element-android/issues/4283)) + - Fix unread marker not showing ([#4313](https://github.com/vector-im/element-android/issues/4313)) + + +Changes in Element v1.3.4 (2021-10-20) +====================================== + +Features ✨ +---------- + - Implement /part command, with or without parameter ([#2909](https://github.com/vector-im/element-android/issues/2909)) + - Handle Presence support, for Direct Message room ([#4090](https://github.com/vector-im/element-android/issues/4090)) + - Priority conversations for Android 11+ ([#3313](https://github.com/vector-im/element-android/issues/3313)) + +Bugfixes 🐛 +---------- + - Issue #908 Adding trailing space " " or ": " if the user started a sentence by mentioning someone, ([#908](https://github.com/vector-im/element-android/issues/908)) + - Fixes reappearing notifications when dismissing notifications from slow homeservers or delayed /sync responses ([#3437](https://github.com/vector-im/element-android/issues/3437)) + - Catching event decryption crash and logging when attempting to markOlmSessionForUnwedging fails ([#3608](https://github.com/vector-im/element-android/issues/3608)) + - Fixing notification sounds being triggered for every message, now they only trigger for the first, consistent with the vibrations ([#3774](https://github.com/vector-im/element-android/issues/3774)) + - Voice Message not sendable if recorded while flight mode was on ([#4006](https://github.com/vector-im/element-android/issues/4006)) + - Fixes push notification emails list not refreshing the first time seeing the notifications page. + Also improves the error handling in the email notification toggling by using synchronous flows instead of the WorkManager ([#4106](https://github.com/vector-im/element-android/issues/4106)) + - Make MegolmBackupAuthData.signatures optional for robustness ([#4162](https://github.com/vector-im/element-android/issues/4162)) + - Fixing push notifications starting the looping background sync when the push notification causes the application to be created. ([#4167](https://github.com/vector-im/element-android/issues/4167)) + - Fix random crash when user logs out just after the log in. ([#4193](https://github.com/vector-im/element-android/issues/4193)) + - Make the font size selection dialog scrollable ([#4201](https://github.com/vector-im/element-android/issues/4201)) + - Fix conversation notification for sent messages ([#4221](https://github.com/vector-im/element-android/issues/4221)) + - Fixes the developer sync options being displayed in the home menu when developer mode is disabled ([#4234](https://github.com/vector-im/element-android/issues/4234)) + - Restore support for Android Auto as sent messages are no longer read aloud ([#4247](https://github.com/vector-im/element-android/issues/4247)) + - Fix crash on slash commands Exceptions ([#4261](https://github.com/vector-im/element-android/issues/4261)) + +Other changes +------------- + - Scrub user sensitive data like gps location from images when sending on original quality ([#465](https://github.com/vector-im/element-android/issues/465)) + - Migrate to MvRx2 (Mavericks) ([#3890](https://github.com/vector-im/element-android/issues/3890)) + - Implement a new github action workflow to generate two PRs for emoji and sas string sync ([#4216](https://github.com/vector-im/element-android/issues/4216)) + - Improve wording around rageshakes in the defect issue template. ([#4226](https://github.com/vector-im/element-android/issues/4226)) + - Add automation to move incoming issues and X-Needs-Info into the right places on the issue triage board. ([#4250](https://github.com/vector-im/element-android/issues/4250)) + - Uppon sharing image compression fails, return the original image ([#4264](https://github.com/vector-im/element-android/issues/4264)) + + +Changes in Element v1.3.3 (2021-10-11) +====================================== + +Bugfixes 🐛 +---------- + - Disable Android Auto supports ([#4205](https://github.com/vector-im/element-android/issues/4205)) + + +Changes in Element v1.3.2 (2021-10-08) +====================================== + +Features ✨ +---------- + - Android Auto notification support ([#240](https://github.com/vector-im/element-android/issues/240)) + - Add a fallback for user displayName when this one is null or empty ([#3732](https://github.com/vector-im/element-android/issues/3732)) + - Add client base url config to customize permalinks ([#4027](https://github.com/vector-im/element-android/issues/4027)) + - Check if DM exists before creating a new one ([#4157](https://github.com/vector-im/element-android/issues/4157)) + - Handle 8 new slash commands: `/ignore`, `/unignore`, `/roomname`, `/myroomnick`, `/roomavatar`, `/myroomavatar`, `/lenny`, `/whois`. ([#4158](https://github.com/vector-im/element-android/issues/4158)) + - Display identity server policies in the Discovery screen ([#4184](https://github.com/vector-im/element-android/issues/4184)) + +Bugfixes 🐛 +---------- + - Ensure initial sync progress dialog is hidden when the initial sync is over ([#983](https://github.com/vector-im/element-android/issues/983)) + - Avoid resending notifications that are already shown ([#1673](https://github.com/vector-im/element-android/issues/1673)) + - Room filter no results bad CTA in space mode when a space selected ([#3048](https://github.com/vector-im/element-android/issues/3048)) + - Fixes notifications not dismissing when reading messages on other devices ([#3347](https://github.com/vector-im/element-android/issues/3347)) + - Fixes the passphrase screen being incorrectly shown when pressing back on the key verification screen. + When the user doesn't have a passphrase set we don't show the passphrase screen. ([#3898](https://github.com/vector-im/element-android/issues/3898)) + - App doesn't take you to a Space after choosing to Join it ([#3933](https://github.com/vector-im/element-android/issues/3933)) + - Validate public space addresses and room aliases length ([#3934](https://github.com/vector-im/element-android/issues/3934)) + - Save button for adding rooms to a space is hidden when scrolling through list of rooms ([#3935](https://github.com/vector-im/element-android/issues/3935)) + - Align new room encryption default to Web ([#4045](https://github.com/vector-im/element-android/issues/4045)) + - Fix Reply/Edit mode animation is broken when sending ([#4077](https://github.com/vector-im/element-android/issues/4077)) + - Added changes that will make SearchView in search bar focused by default on opening reaction picker. + + When tapping close icon of SearchView, the SearchView did not collapse therefore added the on close listener + which will collapse the SearchView on close. ([#4092](https://github.com/vector-im/element-android/issues/4092)) + - Troubleshoot notification: Fix button not clickable ([#4109](https://github.com/vector-im/element-android/issues/4109)) + - Harmonize wording in the message bottom sheet and move up the View Reactions item ([#4155](https://github.com/vector-im/element-android/issues/4155)) + - Remove unused SendRelationWorker and related API call (3588) ([#4156](https://github.com/vector-im/element-android/issues/4156)) + - SIP user to native user mapping is wrong ([#4176](https://github.com/vector-im/element-android/issues/4176)) + +SDK API changes ⚠️ +------------------ + - Create extension `String.isMxcUrl()` ([#4158](https://github.com/vector-im/element-android/issues/4158)) + +Other changes +------------- + - Use ktlint plugin. See [the documentation](https://github.com/vector-im/element-android/blob/develop/CONTRIBUTING.md#ktlint) for more detail. ([#3957](https://github.com/vector-im/element-android/issues/3957)) + - Minimize the use of exported="true" in android Manifest (link: https://github.com/matrix-org/matrix-dinsic/issues/618) ([#4018](https://github.com/vector-im/element-android/issues/4018)) + - Fix redundancy in heading in the bug report issue form ([#4076](https://github.com/vector-im/element-android/issues/4076)) + - Fix release label in the release issue template ([#4113](https://github.com/vector-im/element-android/issues/4113)) + + +Changes in Element v1.3.1 (2021-09-29) +====================================== + +Bugfixes 🐛 +---------- + - Verifying exported E2E keys to provide user feedback when the output is malformed ([#4082](https://github.com/vector-im/element-android/issues/4082)) + - Fix settings crash when accelerometer not available ([#4103](https://github.com/vector-im/element-android/issues/4103)) + - Crash while rendering failed message warning ([#4110](https://github.com/vector-im/element-android/issues/4110)) + + +Changes in Element v1.3.0 (2021-09-27) +====================================== + +Features ✨ +---------- + - Spaces! + - Adds email notification registration to Settings ([#2243](https://github.com/vector-im/element-android/issues/2243)) + - Spaces | M3.23 Invite by email in create private space flow ([#3678](https://github.com/vector-im/element-android/issues/3678)) + - Improve space invite bottom sheet ([#4057](https://github.com/vector-im/element-android/issues/4057)) + - Allow to also leave rooms when leaving a space ([#3692](https://github.com/vector-im/element-android/issues/3692)) + - Better expose adding spaces as Subspaces ([#3752](https://github.com/vector-im/element-android/issues/3752)) + - Push and syncs: add debug info on room list and on room detail screen and improves the log format. ([#4046](https://github.com/vector-im/element-android/issues/4046)) + +Bugfixes 🐛 +---------- + - Remove the "Teammate spaces aren't quite ready" bottom sheet ([#3945](https://github.com/vector-im/element-android/issues/3945)) + - Restricted Room previews aren't working ([#3946](https://github.com/vector-im/element-android/issues/3946)) + - A removed room from a space can't be re-added as it won't be shown in add-room ([#3947](https://github.com/vector-im/element-android/issues/3947)) + - "Non-Admin" user able to invite others to Private Space (by default) ([#3951](https://github.com/vector-im/element-android/issues/3951)) + - Kick user dialog for spaces talks about rooms ([#3956](https://github.com/vector-im/element-android/issues/3956)) + - Messages are displayed as unable to decrypt then decrypted a few seconds later ([#4011](https://github.com/vector-im/element-android/issues/4011)) + - Fix DTMF not working ([#4015](https://github.com/vector-im/element-android/issues/4015)) + - Fix sticky end call notification ([#4019](https://github.com/vector-im/element-android/issues/4019)) + - Fix call screen stuck with some hanging up scenarios ([#4026](https://github.com/vector-im/element-android/issues/4026)) + - Fix other call not always refreshed when ended ([#4028](https://github.com/vector-im/element-android/issues/4028)) + - Private space invite bottomsheet only offering inviting by username not by email ([#4042](https://github.com/vector-im/element-android/issues/4042)) + - Spaces invitation system notifications don't take me to the join space toast ([#4043](https://github.com/vector-im/element-android/issues/4043)) + - Space Invites are not lighting up the drawer menu ([#4059](https://github.com/vector-im/element-android/issues/4059)) + - MessageActionsBottomSheet not being shown on local echos ([#4068](https://github.com/vector-im/element-android/issues/4068)) + +SDK API changes ⚠️ +------------------ + - InitialSyncProgressService has been renamed to SyncStatusService and its function getInitialSyncProgressStatus() has been renamed to getSyncStatusLive() ([#4046](https://github.com/vector-im/element-android/issues/4046)) + +Other changes +------------- + - Better support for Sdk2 version. Also slight change in the default user agent: `MatrixAndroidSDK_X` is replaced by `MatrixAndroidSdk2` ([#3994](https://github.com/vector-im/element-android/issues/3994)) + - Introduces ConferenceEvent to abstract usage of Jitsi BroadcastEvent class. ([#4014](https://github.com/vector-im/element-android/issues/4014)) + - Improve performances on RoomDetail screen ([#4065](https://github.com/vector-im/element-android/issues/4065)) + + Changes in Element v1.2.2 (2021-09-13) ====================================== diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 610a6227b7..dbc0ce9b72 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -9,7 +9,7 @@ Android support can be found in this [![Element Android Matrix room #element-and ## Android Studio settings Please set the "hard wrap" setting of Android Studio to 160 chars, this is the setting we use internally to format the source code (Menu `Settings/Editor/Code Style` then `Hard wrap at`). -Please ensure that your using the project formatting rules (which are in the project at .idea/codeStyles/), and format the file before committing them. +Please ensure that you're using the project formatting rules (which are in the project at .idea/codeStyles/), and format the file before committing them. ### Template @@ -80,14 +80,13 @@ Make sure the following commands execute without any error: #### ktlint
-curl -sSLO https://github.com/pinterest/ktlint/releases/download/0.34.2/ktlint && chmod a+x ktlint
-./ktlint --android --experimental -v
+./gradlew ktlintCheck --continue
 
Note that you can run
-./ktlint --android --experimental -v -F
+./gradlew ktlintFormat
 
For ktlint to fix some detected errors for you (you still have to check and commit the fix of course) @@ -148,6 +147,8 @@ The string will be removed during the next sync with Weblate. Please consider accessibility as an important point. As a minimum requirement, in layout XML files please use attributes such as `android:contentDescription` and `android:importantForAccessibility`, and test with a screen reader if it's working well. You can add new string resources, dedicated to accessibility, in this case, please prefix theirs id with `a11y_`. +For instance, when updating the image `src` of an ImageView, please also consider updating its `contentDescription`. A good example is a play pause button. + ### Layout When adding or editing layouts, make sure the layout will render correctly if device uses a RTL (Right To Left) language. diff --git a/attachment-viewer/build.gradle b/attachment-viewer/build.gradle index 064f497dc7..02fbfc794c 100644 --- a/attachment-viewer/build.gradle +++ b/attachment-viewer/build.gradle @@ -53,7 +53,6 @@ dependencies { implementation libs.rx.rxKotlin implementation libs.rx.rxAndroid - implementation libs.jetbrains.kotlinStdlib implementation libs.androidx.core implementation libs.androidx.appCompat implementation libs.androidx.recyclerview diff --git a/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/AttachmentViewerActivity.kt b/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/AttachmentViewerActivity.kt index f909418d6f..4ca6ced8fe 100644 --- a/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/AttachmentViewerActivity.kt +++ b/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/AttachmentViewerActivity.kt @@ -39,7 +39,6 @@ import androidx.core.view.updatePadding import androidx.transition.TransitionManager import androidx.viewpager2.widget.ViewPager2 import im.vector.lib.attachmentviewer.databinding.ActivityAttachmentViewerBinding - import java.lang.ref.WeakReference import kotlin.math.abs @@ -291,8 +290,8 @@ abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventLi private fun calculateTranslationAlpha(translationY: Float, translationLimit: Int): Float = 1.0f - 1.0f / translationLimit.toFloat() / 4f * abs(translationY) - private fun createSwipeToDismissHandler() - : SwipeToDismissHandler = SwipeToDismissHandler( + private fun createSwipeToDismissHandler(): SwipeToDismissHandler = + SwipeToDismissHandler( swipeView = views.dismissContainer, shouldAnimateDismiss = { shouldAnimateDismiss() }, onDismiss = { animateClose() }, diff --git a/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/ImageLoaderTarget.kt b/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/ImageLoaderTarget.kt index 531e8171e1..99686eaabb 100644 --- a/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/ImageLoaderTarget.kt +++ b/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/ImageLoaderTarget.kt @@ -36,8 +36,8 @@ interface ImageLoaderTarget { fun onResourceReady(uid: String, resource: Drawable) } -internal class DefaultImageLoaderTarget(val holder: AnimatedImageViewHolder, private val contextView: ImageView) - : ImageLoaderTarget { +internal class DefaultImageLoaderTarget(val holder: AnimatedImageViewHolder, private val contextView: ImageView) : + ImageLoaderTarget { override fun contextView(): ImageView { return contextView } diff --git a/build.gradle b/build.gradle index 49c3e07ece..e9045b99c7 100644 --- a/build.gradle +++ b/build.gradle @@ -17,6 +17,7 @@ buildscript { // https://developer.android.com/studio/releases/gradle-plugin classpath libs.gradle.gradlePlugin classpath libs.gradle.kotlinPlugin + classpath libs.gradle.hiltPlugin classpath 'com.google.gms:google-services:4.3.10' classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.3' classpath 'com.google.android.gms:oss-licenses-plugin:0.10.4' @@ -27,7 +28,14 @@ buildscript { } } +// ktlint Plugin +plugins { + id "org.jlleitschuh.gradle.ktlint" version "10.2.0" +} + allprojects { + apply plugin: "org.jlleitschuh.gradle.ktlint" + repositories { // For olm library. This has to be declared first, to ensure that Olm library is not downloaded from another repo maven { @@ -75,6 +83,26 @@ allprojects { // You can override by passing `-PallWarningsAsErrors=false` in the command line kotlinOptions.allWarningsAsErrors = project.getProperties().getOrDefault("allWarningsAsErrors", "true").toBoolean() } + + // Fix "Java heap space" issue + tasks.withType(org.jlleitschuh.gradle.ktlint.tasks.BaseKtLintCheckTask).configureEach { + it.workerMaxHeapSize.set("2G") + } + + // See https://github.com/JLLeitschuh/ktlint-gradle#configuration + ktlint { + android = true + ignoreFailures = false + enableExperimentalRules = true + // display the corresponding rule + verbose = true + disabledRules = [ + "spacing-between-declarations-with-comments", + "no-multi-spaces", + "experimental:spacing-between-declarations-with-annotations", + "experimental:annotation" + ] + } } task clean(type: Delete) { diff --git a/changelog.d/1491.bugfix b/changelog.d/1491.bugfix new file mode 100644 index 0000000000..0ff6bd2c11 --- /dev/null +++ b/changelog.d/1491.bugfix @@ -0,0 +1 @@ +Stops showing a dedicated redacted event notification, the message notifications will update accordingly \ No newline at end of file diff --git a/changelog.d/3395.bugfix b/changelog.d/3395.bugfix new file mode 100644 index 0000000000..9482e1bc7e --- /dev/null +++ b/changelog.d/3395.bugfix @@ -0,0 +1 @@ +Fixes marking individual notifications as read causing other notifications to be dismissed \ No newline at end of file diff --git a/changelog.d/3678.feature b/changelog.d/3678.feature deleted file mode 100644 index 7889cafd7d..0000000000 --- a/changelog.d/3678.feature +++ /dev/null @@ -1 +0,0 @@ -Spaces | M3.23 Invite by email in create private space flow \ No newline at end of file diff --git a/changelog.d/3692.feature b/changelog.d/3692.feature deleted file mode 100644 index 5e6178eeb5..0000000000 --- a/changelog.d/3692.feature +++ /dev/null @@ -1 +0,0 @@ -Allow to also leave rooms when leaving a space \ No newline at end of file diff --git a/changelog.d/3752.feature b/changelog.d/3752.feature deleted file mode 100644 index 742c015778..0000000000 --- a/changelog.d/3752.feature +++ /dev/null @@ -1 +0,0 @@ -Better expose adding spaces as Subspaces \ No newline at end of file diff --git a/changelog.d/3888.misc b/changelog.d/3888.misc new file mode 100644 index 0000000000..314e515631 --- /dev/null +++ b/changelog.d/3888.misc @@ -0,0 +1 @@ +Migrate app DI framework to Hilt \ No newline at end of file diff --git a/changelog.d/3945.bugfix b/changelog.d/3945.bugfix deleted file mode 100644 index caedcc9cba..0000000000 --- a/changelog.d/3945.bugfix +++ /dev/null @@ -1 +0,0 @@ -Remove the "Teammate spaces aren't quite ready" bottom sheet \ No newline at end of file diff --git a/changelog.d/3946.bugfix b/changelog.d/3946.bugfix deleted file mode 100644 index ba9603da24..0000000000 --- a/changelog.d/3946.bugfix +++ /dev/null @@ -1 +0,0 @@ - Restricted Room previews aren't working \ No newline at end of file diff --git a/changelog.d/3947.bugfix b/changelog.d/3947.bugfix deleted file mode 100644 index 66bca0b1ac..0000000000 --- a/changelog.d/3947.bugfix +++ /dev/null @@ -1 +0,0 @@ -A removed room from a space can't be re-added as it won't be shown in add-room \ No newline at end of file diff --git a/changelog.d/3951.bugfix b/changelog.d/3951.bugfix deleted file mode 100644 index cefcb996fa..0000000000 --- a/changelog.d/3951.bugfix +++ /dev/null @@ -1 +0,0 @@ -"Non-Admin" user able to invite others to Private Space (by default) \ No newline at end of file diff --git a/changelog.d/3956.bugfix b/changelog.d/3956.bugfix deleted file mode 100644 index adaeb0c684..0000000000 --- a/changelog.d/3956.bugfix +++ /dev/null @@ -1 +0,0 @@ - Kick user dialog for spaces talks about rooms \ No newline at end of file diff --git a/changelog.d/3968.bugfix b/changelog.d/3968.bugfix new file mode 100644 index 0000000000..dec0eaf2df --- /dev/null +++ b/changelog.d/3968.bugfix @@ -0,0 +1 @@ +Fixing room search needing exact casing for non latin-1 character named rooms \ No newline at end of file diff --git a/changelog.d/3994.misc b/changelog.d/3994.misc deleted file mode 100644 index 63fbe55387..0000000000 --- a/changelog.d/3994.misc +++ /dev/null @@ -1 +0,0 @@ -Better support for Sdk2 version. Also slight change in the default user agent: `MatrixAndroidSDK_X` is replaced by `MatrixAndroidSdk2` \ No newline at end of file diff --git a/changelog.d/4014.misc b/changelog.d/4014.misc deleted file mode 100644 index d2c69a4ac5..0000000000 --- a/changelog.d/4014.misc +++ /dev/null @@ -1 +0,0 @@ -Introduces ConferenceEvent to abstract usage of Jitsi BroadcastEvent class. \ No newline at end of file diff --git a/changelog.d/4015.bugfix b/changelog.d/4015.bugfix deleted file mode 100644 index 98ec646f44..0000000000 --- a/changelog.d/4015.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix DTMF not working \ No newline at end of file diff --git a/changelog.d/4047.bugfix b/changelog.d/4047.bugfix new file mode 100644 index 0000000000..a25e824900 --- /dev/null +++ b/changelog.d/4047.bugfix @@ -0,0 +1 @@ +Fixing call ringtones only playing once when the ringtone doesn't contain looping metadata (android 9.0 and above) \ No newline at end of file diff --git a/changelog.d/4152.bugfix b/changelog.d/4152.bugfix new file mode 100644 index 0000000000..1ff45609b5 --- /dev/null +++ b/changelog.d/4152.bugfix @@ -0,0 +1 @@ +Tentatively fixing the doubled notifications by updating the group summary at specific points in the notification rendering cycle \ No newline at end of file diff --git a/changelog.d/4170.bugfix b/changelog.d/4170.bugfix new file mode 100644 index 0000000000..3c1cc4361f --- /dev/null +++ b/changelog.d/4170.bugfix @@ -0,0 +1 @@ +Do not show shortcuts if a PIN code is set \ No newline at end of file diff --git a/changelog.d/4192.misc b/changelog.d/4192.misc new file mode 100644 index 0000000000..3587e3cb7c --- /dev/null +++ b/changelog.d/4192.misc @@ -0,0 +1 @@ +Limit supported TLS versions and cipher suites \ No newline at end of file diff --git a/changelog.d/4255.bugfix b/changelog.d/4255.bugfix new file mode 100644 index 0000000000..8fc820d70f --- /dev/null +++ b/changelog.d/4255.bugfix @@ -0,0 +1 @@ +Fixes being unable to join rooms by name \ No newline at end of file diff --git a/changelog.d/4266.removal b/changelog.d/4266.removal new file mode 100644 index 0000000000..5ac757bc8a --- /dev/null +++ b/changelog.d/4266.removal @@ -0,0 +1 @@ +Add API `LoginWizard.loginCustom(data: JsonDict): Session` to be able to login to a homeserver using arbitrary request content \ No newline at end of file diff --git a/changelog.d/4277.feature b/changelog.d/4277.feature new file mode 100644 index 0000000000..0be1114f22 --- /dev/null +++ b/changelog.d/4277.feature @@ -0,0 +1 @@ +Updating single sign on providers ordering to match priority/popularity \ No newline at end of file diff --git a/changelog.d/4334.removal b/changelog.d/4334.removal new file mode 100644 index 0000000000..1ed04d3cdf --- /dev/null +++ b/changelog.d/4334.removal @@ -0,0 +1 @@ +Add optional deviceId to the login API \ No newline at end of file diff --git a/changelog.d/4353.bugfix b/changelog.d/4353.bugfix new file mode 100644 index 0000000000..170dbed418 --- /dev/null +++ b/changelog.d/4353.bugfix @@ -0,0 +1 @@ +Fix video compression before upload diff --git a/changelog.d/4361.bugfix b/changelog.d/4361.bugfix new file mode 100644 index 0000000000..fe32783fd2 --- /dev/null +++ b/changelog.d/4361.bugfix @@ -0,0 +1 @@ +Fixing QR code crashes caused by a known issue in the zxing library for older versions of android by downgrading to 3.3.3 \ No newline at end of file diff --git a/changelog.d/4365.bugfix b/changelog.d/4365.bugfix new file mode 100644 index 0000000000..2322f022a9 --- /dev/null +++ b/changelog.d/4365.bugfix @@ -0,0 +1 @@ +Fixing timeline crash when rotating with the emoji window open \ No newline at end of file diff --git a/changelog.d/4369.bugfix b/changelog.d/4369.bugfix new file mode 100644 index 0000000000..43ce71cbb8 --- /dev/null +++ b/changelog.d/4369.bugfix @@ -0,0 +1 @@ +Fix handling of links coming from web instance reported as malformed by mistake \ No newline at end of file diff --git a/changelog.d/582.feature b/changelog.d/582.feature new file mode 100644 index 0000000000..5f82e1b82c --- /dev/null +++ b/changelog.d/582.feature @@ -0,0 +1 @@ +Adding the room name to the invitation notification (if the room summary is available) \ No newline at end of file diff --git a/dependencies.gradle b/dependencies.gradle index 13dc57d7fd..47090d4732 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -7,11 +7,11 @@ ext.versions = [ 'targetCompat' : JavaVersion.VERSION_11, ] +def gradle = "7.0.3" // Ref: https://kotlinlang.org/releases.html -def gradle = "7.0.2" -def kotlin = "1.5.30" -def kotlinCoroutines = "1.5.1" -def dagger = "2.38.1" +def kotlin = "1.5.31" +def kotlinCoroutines = "1.5.2" +def dagger = "2.40" def retrofit = "2.9.0" def arrow = "0.8.2" def markwon = "4.6.2" @@ -19,9 +19,11 @@ def moshi = "1.12.0" def lifecycle = "2.2.0" def rxBinding = "3.1.0" def epoxy = "4.6.2" +def mavericks = "2.4.0" def glide = "4.12.0" def bigImageViewer = "1.8.1" def jjwt = "0.11.2" +def vanniktechEmoji = "0.8.0" // Testing def mockk = "1.12.0" @@ -32,11 +34,11 @@ def androidxTest = "1.4.0" ext.libs = [ gradle : [ 'gradlePlugin' : "com.android.tools.build:gradle:$gradle", - 'kotlinPlugin' : "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin" + 'kotlinPlugin' : "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin", + 'hiltPlugin' : "com.google.dagger:hilt-android-gradle-plugin:$dagger" + ], jetbrains : [ - 'kotlinStdlibJdk7' : "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin", - 'kotlinStdlib' : "org.jetbrains.kotlin:kotlin-stdlib:$kotlin", 'coroutinesCore' : "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinCoroutines", 'coroutinesAndroid' : "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlinCoroutines", 'coroutinesRx2' : "org.jetbrains.kotlinx:kotlinx-coroutines-rx2:$kotlinCoroutines" @@ -47,17 +49,20 @@ ext.libs = [ 'recyclerview' : "androidx.recyclerview:recyclerview:1.2.1", 'exifinterface' : "androidx.exifinterface:exifinterface:1.3.3", 'fragmentKtx' : "androidx.fragment:fragment-ktx:1.3.6", - 'constraintLayout' : "androidx.constraintlayout:constraintlayout:2.1.0", - 'work' : "androidx.work:work-runtime-ktx:2.5.0", + 'constraintLayout' : "androidx.constraintlayout:constraintlayout:2.1.1", + 'work' : "androidx.work:work-runtime-ktx:2.6.0", 'autoFill' : "androidx.autofill:autofill:1.1.0", 'preferenceKtx' : "androidx.preference:preference-ktx:1.1.1", 'junit' : "androidx.test.ext:junit:1.1.3", 'lifecycleExtensions' : "androidx.lifecycle:lifecycle-extensions:$lifecycle", 'lifecycleJava8' : "androidx.lifecycle:lifecycle-common-java8:$lifecycle", 'lifecycleLivedata' : "androidx.lifecycle:lifecycle-livedata-ktx:2.3.1", + 'datastore' : "androidx.datastore:datastore:1.0.0", + 'datastorepreferences' : "androidx.datastore:datastore-preferences:1.0.0", 'pagingRuntimeKtx' : "androidx.paging:paging-runtime-ktx:2.1.2", 'coreTesting' : "androidx.arch.core:core-testing:2.1.0", 'testCore' : "androidx.test:core:$androidxTest", + 'orchestrator' : "androidx.test:orchestrator:$androidxTest", 'testRunner' : "androidx.test:runner:$androidxTest", 'testRules' : "androidx.test:rules:$androidxTest", 'espressoCore' : "androidx.test.espresso:espresso-core:$espresso", @@ -69,7 +74,9 @@ ext.libs = [ ], dagger : [ 'dagger' : "com.google.dagger:dagger:$dagger", - 'daggerCompiler' : "com.google.dagger:dagger-compiler:$dagger" + 'daggerCompiler' : "com.google.dagger:dagger-compiler:$dagger", + 'hilt' : "com.google.dagger:hilt-android:$dagger", + 'hiltCompiler' : "com.google.dagger:hilt-compiler:$dagger" ], squareup : [ 'moshi' : "com.squareup.moshi:moshi-adapters:$moshi", @@ -94,7 +101,9 @@ ext.libs = [ 'epoxyGlide' : "com.airbnb.android:epoxy-glide-preloading:$epoxy", 'epoxyProcessor' : "com.airbnb.android:epoxy-processor:$epoxy", 'epoxyPaging' : "com.airbnb.android:epoxy-paging:$epoxy", - 'mvrx' : "com.airbnb.android:mvrx:1.5.1" + 'mavericks' : "com.airbnb.android:mavericks:$mavericks", + 'mavericksRx' : "com.airbnb.android:mavericks-rxjava2:$mavericks", + 'mavericksTesting' : "com.airbnb.android:mavericks-testing:$mavericks" ], mockk : [ 'mockk' : "io.mockk:mockk:$mockk", @@ -119,6 +128,13 @@ ext.libs = [ 'jjwtImpl' : "io.jsonwebtoken:jjwt-impl:$jjwt", 'jjwtOrgjson' : "io.jsonwebtoken:jjwt-orgjson:$jjwt" ], + vanniktech : [ + 'emojiMaterial' : "com.vanniktech:emoji-material:$vanniktechEmoji", + 'emojiGoogle' : "com.vanniktech:emoji-google:$vanniktechEmoji" + ], + apache : [ + 'commonsImaging' : "org.apache.sanselan:sanselan:0.97-incubator" + ], tests : [ 'kluent' : "org.amshove.kluent:kluent-android:1.68", 'timberJunitRule' : "net.lachlanmckee:timber-junit-rule:1.0.1", diff --git a/docs/design.md b/docs/design.md new file mode 100644 index 0000000000..2e27f00ebf --- /dev/null +++ b/docs/design.md @@ -0,0 +1,104 @@ +# Element Android design + +## Introduction + +Design at element.io is done using Figma - https://www.figma.com + +## How to import from Figma to the Element Android project + +Integration should be done using the Android development best practice, and should follow the existing convention in the code. + +### Colors + +Element Android already contains all the colors which can be used by the designer, in the module `ui-style`. +Some of them depend on the theme, so ensure to use theme attributes and not colors directly. + +### Text + + - click on a text on Figma + - on the right panel, information about the style and colors are displayed + - in Element Android, text style are already defined, generally you should not create new style + - apply the style and the color to the layout + +### Dimension, position and margin + + - click on an item on Figma + - dimensions of the item will be displayed. + - move the mouse to other items to get relative positioning, margin, etc. + +### Icons + +#### Export drawable from Figma + + - click on the element to export + - ensure that the correct layer is selected. Sometimes the parent layer has to be selected on the left panel + - on the right panel, click on "export" + - select SVG + - you can check the preview of what will be exported + - click on "export" and save the file locally + - unzip the file if necessary + +It's also possible for any icon to go to the main component by right-clicking on the icon. + +#### Import in Android Studio + + - right click on the drawable folder where the drawable will be created + - click on "New"/"Vector Asset" + - select the exported file + - update the filename if necessary + - click on "Next" and click on "Finish" + - open the created vector drawable + - optionally update the color(s) to "#FF0000" (red) to ensure that the drawable is correctly tinted at runtime. + +## Figma links + +Figma links can be included in the layout, for future reference, but it is also OK to add a paragraph below here, to centralize the information + +Main entry point: https://www.figma.com/files/project/5612863/Element?fuid=779371459522484071 + +Note: all the Figma links are not publicly available. + +### Coumpound + +Coumpound contains the theme of the application, with all the components, in Light and Dark theme: palette (colors), typography, iconography, etc. + +https://www.figma.com/file/X4XTH9iS2KGJ2wFKDqkyed/Compound + +### Login + +TBD + +#### Login v2 + +https://www.figma.com/file/xdV4PuI3DlzA1EiBvbrggz/Login-Flow-v2 + +### Room list + +TBD + +### Timeline + +https://www.figma.com/file/x1HYYLYMmbYnhfoz2c2nGD/%5BRiotX%5D-Misc?node-id=0%3A1 + +### Voice message + +https://www.figma.com/file/uaWc62Ux2DkZC4OGtAGcNc/Voice-Messages?node-id=473%3A12 + +### Room settings + +TBD + +### VoIP + +https://www.figma.com/file/V6m2z0oAtUV1l8MdyIrAep/VoIP?node-id=4254%3A25767 + +### Presence + +https://www.figma.com/file/qmvEskET5JWva8jZJ4jX8o/Presence---User-Status?node-id=114%3A9174 +(Option B is chosen) + +### Spaces + +https://www.figma.com/file/m7L63aGPW7iHnIYStfdxCe/Spaces?node-id=192%3A30161 + +### List to be continued... diff --git a/docs/hilt_migration.md b/docs/hilt_migration.md new file mode 100644 index 0000000000..50021e9792 --- /dev/null +++ b/docs/hilt_migration.md @@ -0,0 +1,33 @@ +Useful links: +- https://dagger.dev/hilt/migration-guide +- https://dagger.dev/hilt/quick-start + +Hilt is built on top of Dagger 2 and simplify usage by removing needs to create components manually. + +When you create a new feature, you should have the following: + +Annotate your Activity with @AndroidEntryPoint +If you have a BottomSheetFragment => Annotate it with @AndroidEntryPoint +Otherwise => Add your Fragment to the FragmentModule +Add your ViewModel.Factory to the MavericksViewModelModule +Makes sure your ViewModel as the following code: + +``` + @AssistedFactory + interface Factory: MavericksAssistedViewModelFactory { + override fun create(initialState: MyViewState): MyViewModel + } + + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() +``` + +## Some remarks + +@MavericksViewModelScope dependencies can't be injected inside Fragments/Activities +You can only inject @Singleton, @MavericksViewModelScope or unscoped dependencies inside Maverick ViewModels +You can access some specific dependencies from Singleton component by using +``` +context.singletonEntryPoint() +``` +Be aware that only the app has been migrated to Hilt and not the SDK. + diff --git a/docs/mavericks_migration.md b/docs/mavericks_migration.md new file mode 100644 index 0000000000..a36ae8261a --- /dev/null +++ b/docs/mavericks_migration.md @@ -0,0 +1,11 @@ +Useful links: +- https://airbnb.io/mavericks/#/new-2x + +Mavericks 2 is replacing MvRx, by removing usage of Rx by Flow, both internally and in the API. +See the link ^ to have more intel, but basically, the changes are: + +session.rx() => session.flow() +room.rx() => room.flow() +subscribe { }.disposeOnClear() => onEach { }.launchIn(viewModelScope) + +Only using manually onEach requires to add launchIn,any other methods provided by Mavericks on viewModel and activity/fragment are already taking care of lifecycle. \ No newline at end of file diff --git a/fastlane/metadata/android/cs-CZ/changelogs/40102010.txt b/fastlane/metadata/android/cs-CZ/changelogs/40102010.txt new file mode 100644 index 0000000000..ca75c6b5d8 --- /dev/null +++ b/fastlane/metadata/android/cs-CZ/changelogs/40102010.txt @@ -0,0 +1,2 @@ +Hlavní změny v této verzi: Mnohá vylepšení VoIP a prostorů (stále v beta verzi). +Úplný seznam změn: https://github.com/vector-im/element-android/releases/tag/v1.2.1 diff --git a/fastlane/metadata/android/cs-CZ/changelogs/40103000.txt b/fastlane/metadata/android/cs-CZ/changelogs/40103000.txt new file mode 100644 index 0000000000..f97ff3ef3a --- /dev/null +++ b/fastlane/metadata/android/cs-CZ/changelogs/40103000.txt @@ -0,0 +1,2 @@ +Hlavní změny v této verzi: Uspořádejte si místnosti pomocí Prostorů! +Úplný seznam změn: https://github.com/vector-im/element-android/releases/tag/v1.3.0 diff --git a/fastlane/metadata/android/cs-CZ/changelogs/40103010.txt b/fastlane/metadata/android/cs-CZ/changelogs/40103010.txt new file mode 100644 index 0000000000..37f8aaa759 --- /dev/null +++ b/fastlane/metadata/android/cs-CZ/changelogs/40103010.txt @@ -0,0 +1,2 @@ +Hlavní změny v této verzi: Uspořádejte si místnosti pomocí Prostorů! Verze 1.3.1 opravuje pády, ke kterým může docházet ve verzi v1.3.0. +Úplný seznam změn: https://github.com/vector-im/element-android/releases/tag/v1.3.1 diff --git a/fastlane/metadata/android/cs-CZ/changelogs/40103020.txt b/fastlane/metadata/android/cs-CZ/changelogs/40103020.txt new file mode 100644 index 0000000000..2a9dd4bb10 --- /dev/null +++ b/fastlane/metadata/android/cs-CZ/changelogs/40103020.txt @@ -0,0 +1,2 @@ +Hlavní změny v této verzi: Přidání podpory pro Android Auto. Spousta oprav chyb! +Úplný seznam změn: https://github.com/vector-im/element-android/releases/tag/v1.3.2 diff --git a/fastlane/metadata/android/cs-CZ/changelogs/40103030.txt b/fastlane/metadata/android/cs-CZ/changelogs/40103030.txt new file mode 100644 index 0000000000..8faffd36ed --- /dev/null +++ b/fastlane/metadata/android/cs-CZ/changelogs/40103030.txt @@ -0,0 +1,2 @@ +Hlavní změny v této verzi: Umožňuje v nastavení zviditelnit zásady serveru identit. Dočasně odstraňuje podporu pro Android Auto. +Úplný seznam změn: https://github.com/vector-im/element-android/releases/tag/v1.3.3 diff --git a/fastlane/metadata/android/cs-CZ/full_description.txt b/fastlane/metadata/android/cs-CZ/full_description.txt index 6732b33fe3..a74c58b413 100644 --- a/fastlane/metadata/android/cs-CZ/full_description.txt +++ b/fastlane/metadata/android/cs-CZ/full_description.txt @@ -37,3 +37,6 @@ Zprávy, hlasové a videohovory, sdílení souborů, sdílení obrazovky a celá Navažte tam, kde jste skončili Zůstaňte v kontaktu, ať jste kdekoli, díky plně synchronizované historii zpráv ve všech zařízeních a na webu https://app.element.io + +Open source +Element Android je projekt s otevřeným zdrojovým kódem, který je hostován na GitHubu. Nahlaste prosím chyby a přispějte k jeho vývoji na adrese https://github.com/vector-im/element-android diff --git a/fastlane/metadata/android/de-DE/changelogs/40102000.txt b/fastlane/metadata/android/de-DE/changelogs/40102000.txt new file mode 100644 index 0000000000..cfa9f725f2 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/40102000.txt @@ -0,0 +1,2 @@ +Hauptänderungen: Sprachnachrichten standardmäßig aktiviert. +Ganze Änderungsliste: https://github.com/vector-im/element-android/releases/tag/v1.2.0 diff --git a/fastlane/metadata/android/de-DE/changelogs/40102010.txt b/fastlane/metadata/android/de-DE/changelogs/40102010.txt new file mode 100644 index 0000000000..2635704a81 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/40102010.txt @@ -0,0 +1,2 @@ +VoIP und Spaces verbessert +Vollständige Änderungsliste: https://github.com/vector-im/element-android/releases/tag/v1.2.1 diff --git a/fastlane/metadata/android/en-US/changelogs/40103000.txt b/fastlane/metadata/android/en-US/changelogs/40103000.txt new file mode 100644 index 0000000000..d4ef2f75a0 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/40103000.txt @@ -0,0 +1,2 @@ +Main changes in this version: Organize your rooms using Spaces! +Full changelog: https://github.com/vector-im/element-android/releases/tag/v1.3.0 \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/40103010.txt b/fastlane/metadata/android/en-US/changelogs/40103010.txt new file mode 100644 index 0000000000..e3760f1882 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/40103010.txt @@ -0,0 +1,2 @@ +Main changes in this version: Organize your rooms using Spaces! v1.3.1 is fixing a crash which can occurs in v1.3.0. +Full changelog: https://github.com/vector-im/element-android/releases/tag/v1.3.1 \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/40103020.txt b/fastlane/metadata/android/en-US/changelogs/40103020.txt new file mode 100644 index 0000000000..7ac48f4890 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/40103020.txt @@ -0,0 +1,2 @@ +Main changes in this version: Add support for Android Auto. Lot of bug fixes! +Full changelog: https://github.com/vector-im/element-android/releases/tag/v1.3.2 \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/40103030.txt b/fastlane/metadata/android/en-US/changelogs/40103030.txt new file mode 100644 index 0000000000..2068aeed95 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/40103030.txt @@ -0,0 +1,2 @@ +Main changes in this version: Make identity server policy(ies) visible in the settings. Temporarily remove Android Auto support. +Full changelog: https://github.com/vector-im/element-android/releases/tag/v1.3.3 \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/40103040.txt b/fastlane/metadata/android/en-US/changelogs/40103040.txt new file mode 100644 index 0000000000..a6af2efe00 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/40103040.txt @@ -0,0 +1,2 @@ +Main changes in this version: Add Presence support, for Direct Message room (note: presence is disabled on matrix.org). Add again Android Auto support. +Full changelog: https://github.com/vector-im/element-android/releases/tag/v1.3.4 \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/40103050.txt b/fastlane/metadata/android/en-US/changelogs/40103050.txt new file mode 100644 index 0000000000..93227f1a6d --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/40103050.txt @@ -0,0 +1,2 @@ +Main changes in this version: Add Presence support, for Direct Message room (note: presence is disabled on matrix.org). Add again Android Auto support. +Full changelog: https://github.com/vector-im/element-android/releases/tag/v1.3.5 \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/40103060.txt b/fastlane/metadata/android/en-US/changelogs/40103060.txt new file mode 100644 index 0000000000..7afd03a5c8 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/40103060.txt @@ -0,0 +1,2 @@ +Main changes in this version: Add Presence support, for Direct Message room (note: presence is disabled on matrix.org). Add again Android Auto support. +Full changelog: https://github.com/vector-im/element-android/releases/tag/v1.3.6 \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt index 853885944c..ef8d4e6a27 100644 --- a/fastlane/metadata/android/en-US/full_description.txt +++ b/fastlane/metadata/android/en-US/full_description.txt @@ -36,4 +36,7 @@ Real end-to-end encryption (only those in the conversation can decrypt messages) Messaging, voice and video calls, file sharing, screen sharing and a whole bunch of integrations, bots and widgets. Build rooms, communities, stay in touch and get things done. Pick up where you left off -Stay in touch wherever you are with fully synchronised message history across all your devices and on the web at https://app.element.io \ No newline at end of file +Stay in touch wherever you are with fully synchronised message history across all your devices and on the web at https://app.element.io + +Open source +Element Android is an open source project, hosted by GitHub. Please report bugs and/or contribute to its development at https://github.com/vector-im/element-android \ No newline at end of file diff --git a/fastlane/metadata/android/es-ES/changelogs/40100100.txt b/fastlane/metadata/android/es-ES/changelogs/40100100.txt index 70b786d12e..5cfcde2145 100644 --- a/fastlane/metadata/android/es-ES/changelogs/40100100.txt +++ b/fastlane/metadata/android/es-ES/changelogs/40100100.txt @@ -1 +1,2 @@ -// TODO +Esta nueva versión contiene principalmente correcciones de errores y mejoras. Enviar un mensaje ahora es mucho más rápido. +Registro de cambios completo: https://github.com/vector-im/element-android/releases/tag/v1.0.10 diff --git a/fastlane/metadata/android/es-ES/changelogs/40100110.txt b/fastlane/metadata/android/es-ES/changelogs/40100110.txt new file mode 100644 index 0000000000..5444087750 --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/40100110.txt @@ -0,0 +1,2 @@ +Esta nueva versión contiene principalmente mejoras en la interfaz de usuario y la experiencia del usuario. Ahora puedes invitar amigos y crear mensajes directos muy rápido escaneando códigos QR. +Registro de cambios completo: https://github.com/vector-im/element-android/releases/tag/v1.0.11 diff --git a/fastlane/metadata/android/es-ES/changelogs/40100120.txt b/fastlane/metadata/android/es-ES/changelogs/40100120.txt new file mode 100644 index 0000000000..3e17b0359b --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/40100120.txt @@ -0,0 +1,2 @@ +Principales cambios en esta versión: Vista previa de URL, nuevo teclado Emoji, nuevas capacidades de configuración de la habitación y ¡nieve para Navidad! +Registro de cambios completo: https://github.com/vector-im/element-android/releases/tag/v1.0.12 diff --git a/fastlane/metadata/android/es-ES/changelogs/40100130.txt b/fastlane/metadata/android/es-ES/changelogs/40100130.txt new file mode 100644 index 0000000000..c87cb0faf5 --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/40100130.txt @@ -0,0 +1,2 @@ +Principales cambios en esta versión: Vista previa de URL, nuevo teclado Emoji, nuevas capacidades de configuración de la habitación y ¡nieve para Navidad! +Registro de cambios completo: https://github.com/vector-im/element-android/releases/tag/v1.0.13 diff --git a/fastlane/metadata/android/es-ES/changelogs/40100140.txt b/fastlane/metadata/android/es-ES/changelogs/40100140.txt new file mode 100644 index 0000000000..9bd36b13db --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/40100140.txt @@ -0,0 +1,2 @@ +Principales cambios en esta versión: Editar permisos de sala, tema automático de luz / oscuridad y un montón de correcciones de errores. +Registro de cambios completo: https://github.com/vector-im/element-android/releases/tag/v1.0.14 diff --git a/fastlane/metadata/android/es-ES/changelogs/40100150.txt b/fastlane/metadata/android/es-ES/changelogs/40100150.txt new file mode 100644 index 0000000000..f1b7d303d1 --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/40100150.txt @@ -0,0 +1,2 @@ +Principales cambios en esta versión: Soporte de inicio de sesión social. +Registro de cambios completo: https://github.com/vector-im/element-android/releases/tag/v1.0.15 diff --git a/fastlane/metadata/android/es-ES/changelogs/40100160.txt b/fastlane/metadata/android/es-ES/changelogs/40100160.txt new file mode 100644 index 0000000000..707ec23519 --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/40100160.txt @@ -0,0 +1,2 @@ +Principales cambios en esta versión: Soporte de inicio de sesión social. +Registro de cambios completo: https://github.com/vector-im/element-android/releases/tag/v1.0.15 y https://github.com/vector-im/element-android/releases/tag/v1.0.16 diff --git a/fastlane/metadata/android/es-ES/changelogs/40100170.txt b/fastlane/metadata/android/es-ES/changelogs/40100170.txt new file mode 100644 index 0000000000..9c6d3d7f54 --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/40100170.txt @@ -0,0 +1,2 @@ +Principales cambios en esta versión: ¡Corrección de errores! +Registro de cambios completo: https://github.com/vector-im/element-android/releases/tag/v1.0.17 diff --git a/fastlane/metadata/android/es-ES/changelogs/40101000.txt b/fastlane/metadata/android/es-ES/changelogs/40101000.txt new file mode 100644 index 0000000000..996f9fdde8 --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/40101000.txt @@ -0,0 +1,2 @@ +Principales cambios en esta versión: ¡Mejora de VoIP (audio y videollamadas en DM) y corrección de errores! +Registro de cambios completo: https://github.com/vector-im/element-android/releases/tag/v1.1.0 diff --git a/fastlane/metadata/android/es-ES/changelogs/40101010.txt b/fastlane/metadata/android/es-ES/changelogs/40101010.txt new file mode 100644 index 0000000000..ea9662576c --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/40101010.txt @@ -0,0 +1,2 @@ +Principales cambios en esta versión: mejora del rendimiento y corrección de errores. +Registro de cambios completo: https://github.com/vector-im/element-android/releases/tag/v1.1.1 diff --git a/fastlane/metadata/android/es-ES/changelogs/40101020.txt b/fastlane/metadata/android/es-ES/changelogs/40101020.txt new file mode 100644 index 0000000000..87a92a96cd --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/40101020.txt @@ -0,0 +1,2 @@ +Principales cambios en esta versión: mejora del rendimiento y corrección de errores. +Registro de cambios completo: https://github.com/vector-im/element-android/releases/tag/v1.1.2 diff --git a/fastlane/metadata/android/es-ES/changelogs/40101030.txt b/fastlane/metadata/android/es-ES/changelogs/40101030.txt new file mode 100644 index 0000000000..ca82a2c59c --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/40101030.txt @@ -0,0 +1,2 @@ +Principales cambios en esta versión: mejora del rendimiento y corrección de errores. +Registro de cambios completo: https://github.com/vector-im/element-android/releases/tag/v1.1.3 diff --git a/fastlane/metadata/android/es-ES/changelogs/40101040.txt b/fastlane/metadata/android/es-ES/changelogs/40101040.txt new file mode 100644 index 0000000000..59acee78de --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/40101040.txt @@ -0,0 +1,2 @@ +Principales cambios en esta versión: mejora del rendimiento y corrección de errores. +Registro de cambios completo: https://github.com/vector-im/element-android/releases/tag/v1.1.4 diff --git a/fastlane/metadata/android/es-ES/changelogs/40101050.txt b/fastlane/metadata/android/es-ES/changelogs/40101050.txt new file mode 100644 index 0000000000..ccdd9fd8d6 --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/40101050.txt @@ -0,0 +1,2 @@ +Principales cambios en esta versión: correcciones urgentes para 1.1.4 +Registro de cambios completo: https://github.com/vector-im/element-android/releases/tag/v1.1.5 diff --git a/fastlane/metadata/android/es-ES/changelogs/40101060.txt b/fastlane/metadata/android/es-ES/changelogs/40101060.txt new file mode 100644 index 0000000000..9da3a09866 --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/40101060.txt @@ -0,0 +1,2 @@ +Principales cambios en esta versión: correcciones urgentes para 1.1.5 +Registro de cambios completo: https://github.com/vector-im/element-android/releases/tag/v1.1.6 diff --git a/fastlane/metadata/android/es-ES/changelogs/40101070.txt b/fastlane/metadata/android/es-ES/changelogs/40101070.txt new file mode 100644 index 0000000000..6abb774b93 --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/40101070.txt @@ -0,0 +1,2 @@ +Principales cambios en esta versión: soporte beta para Spaces. Comprima el video antes de enviarlo. +Registro de cambios completo: https://github.com/vector-im/element-android/releases/tag/v1.1.7 diff --git a/fastlane/metadata/android/es-ES/changelogs/40101080.txt b/fastlane/metadata/android/es-ES/changelogs/40101080.txt new file mode 100644 index 0000000000..776bc52a25 --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/40101080.txt @@ -0,0 +1,2 @@ +Principales cambios en esta versión: mejora de Spaces. +Registro de cambios completo: https://github.com/vector-im/element-android/releases/tag/v1.1.8 diff --git a/fastlane/metadata/android/es-ES/changelogs/40101090.txt b/fastlane/metadata/android/es-ES/changelogs/40101090.txt new file mode 100644 index 0000000000..eaeab1517a --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/40101090.txt @@ -0,0 +1,2 @@ +Principales cambios en esta versión: agregar soporte para la red gitter.im. +Registro de cambios completo: https://github.com/vector-im/element-android/releases/tag/v1.1.9 diff --git a/fastlane/metadata/android/es-ES/changelogs/40101100.txt b/fastlane/metadata/android/es-ES/changelogs/40101100.txt new file mode 100644 index 0000000000..d82529cf22 --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/40101100.txt @@ -0,0 +1,2 @@ +Principales cambios en esta versión: actualización de tema y estilo y nuevas funcionalidades para espacios. +Registro de cambios completo: https://github.com/vector-im/element-android/releases/tag/v1.1.10 diff --git a/fastlane/metadata/android/es-ES/changelogs/40101110.txt b/fastlane/metadata/android/es-ES/changelogs/40101110.txt new file mode 100644 index 0000000000..6432d2052a --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/40101110.txt @@ -0,0 +1,2 @@ +Principales cambios en esta versión: actualización de tema y estilo y nuevas funciones para espacios (corrección de errores para 1.1.10) +Registro de cambios completo: https://github.com/vector-im/element-android/releases/tag/v1.1.11 diff --git a/fastlane/metadata/android/es-ES/changelogs/40101120.txt b/fastlane/metadata/android/es-ES/changelogs/40101120.txt new file mode 100644 index 0000000000..a657fff51c --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/40101120.txt @@ -0,0 +1,2 @@ +Principales cambios en esta versión: actualización de tema y estilo y corrección de un bloqueo después de la videollamada +Registro de cambios completo: https://github.com/vector-im/element-android/releases/tag/v1.1.12 diff --git a/fastlane/metadata/android/es-ES/changelogs/40101130.txt b/fastlane/metadata/android/es-ES/changelogs/40101130.txt new file mode 100644 index 0000000000..c9fbf424ae --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/40101130.txt @@ -0,0 +1,2 @@ +Principales cambios en esta versión: principalmente actualización de estabilidad y corrección de errores. +Registro de cambios completo: https://github.com/vector-im/element-android/releases/tag/v1.1.13 diff --git a/fastlane/metadata/android/es-ES/changelogs/40102000.txt b/fastlane/metadata/android/es-ES/changelogs/40102000.txt new file mode 100644 index 0000000000..907019b6d6 --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/40102000.txt @@ -0,0 +1,2 @@ +Principales cambios en esta versión: Mensaje de voz está habilitado por defecto. +Registro de cambios completo: https://github.com/vector-im/element-android/releases/tag/v1.2.0 diff --git a/fastlane/metadata/android/es-ES/changelogs/40102010.txt b/fastlane/metadata/android/es-ES/changelogs/40102010.txt new file mode 100644 index 0000000000..909921ffd4 --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/40102010.txt @@ -0,0 +1,2 @@ +Principales cambios en esta versión: Muchas mejoras en VoIP y Spaces (aún en beta). +Registro de cambios completo: https://github.com/vector-im/element-android/releases/tag/v1.2.1 diff --git a/fastlane/metadata/android/es-ES/changelogs/40103000.txt b/fastlane/metadata/android/es-ES/changelogs/40103000.txt new file mode 100644 index 0000000000..054aa68541 --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/40103000.txt @@ -0,0 +1,2 @@ +Principales cambios en esta versión: ¡Organiza tus habitaciones usando Spaces! +Registro de cambios completo: https://github.com/vector-im/element-android/releases/tag/v1.3.0 diff --git a/fastlane/metadata/android/es-ES/changelogs/40103010.txt b/fastlane/metadata/android/es-ES/changelogs/40103010.txt new file mode 100644 index 0000000000..6ff7b0502e --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/40103010.txt @@ -0,0 +1,2 @@ +Principales cambios en esta versión: ¡Organiza tus habitaciones usando Spaces! v1.3.1 está arreglando un bloqueo que puede ocurrir en v1.3.0. +Registro de cambios completo: https://github.com/vector-im/element-android/releases/tag/v1.3.1 diff --git a/fastlane/metadata/android/es-ES/changelogs/40103020.txt b/fastlane/metadata/android/es-ES/changelogs/40103020.txt new file mode 100644 index 0000000000..22f7592aea --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/40103020.txt @@ -0,0 +1,2 @@ +Principales cambios en esta versión: agregar soporte para Android Auto. ¡Muchas correcciones de errores! +Registro de cambios completo: https://github.com/vector-im/element-android/releases/tag/v1.3.2 diff --git a/fastlane/metadata/android/es-ES/full_description.txt b/fastlane/metadata/android/es-ES/full_description.txt index 8c9915a735..fdba15e90e 100644 --- a/fastlane/metadata/android/es-ES/full_description.txt +++ b/fastlane/metadata/android/es-ES/full_description.txt @@ -1,30 +1,39 @@ -Element es un nuevo tipo de aplicación de mensajería y colaboración que: +Element es un mensajero seguro y una aplicación de colaboración en equipo de productividad que es ideal para chats grupales mientras se trabaja a distancia. Esta aplicación de chat utiliza encriptación de un extremo a otro para proporcionar poderosas videoconferencias, uso compartido de archivos y llamadas de voz. -1. Te da el control para preservar su privacidad -2. Te permite comunicarse con cualquier persona en la red Matrix e incluso más allá al integrarse con aplicaciones como Slack -3. Te protege de la publicidad, la minería de datos y los jardines vallados -4. Te protege a través de encriptación de Extremo-a-Extremo, con firma cruzada para verificar a otros +Las características de Element incluyen: +- Herramientas de comunicación online avanzadas +- Mensajes totalmente encriptados para permitir una comunicación corporativa más segura, incluso para trabajadores remotos +- Chat descentralizado basado en el marco de código abierto Matrix +- Uso compartido de archivos de forma segura con datos cifrados mientras gestiona proyectos +- Chats de video con voz sobre IP y pantalla compartida +- Fácil integración con sus herramientas de colaboración en línea favoritas, herramientas de gestión de proyectos, servicios VoIP y otras aplicaciones de mensajería para equipos -Element es completamente diferente de otras aplicaciones de mensajería y colaboración porque es descentralizado y de código abierto. +Element es completamente diferente de otras aplicaciones de mensajería y colaboración. Opera en Matrix, una red abierta para mensajería segura y comunicación descentralizada. Permite el autohospedaje para brindar a los usuarios la máxima propiedad y control de sus datos y mensajes. -Element te permite tener su propio servidor privado, o elegir uno público, para que tenga privacidad, posesión, y control de sus datos y conversaciones. Te da acceso a una red abierta; para que no se quede atrapado hablando solo con otros usuarios de Element. Y es muy seguro. +Privacidad y mensajería encriptada +Element lo protege de anuncios no deseados, minería de datos y jardines amurallados. También protege todos sus datos, video uno a uno y comunicación de voz a través del cifrado de extremo a extremo y la verificación de dispositivos con firma cruzada. -Element puede hacer todo esto porque opera en Matrix, el estándar para la comunicación abierta y descentralizada. +Element le brinda control sobre su privacidad al mismo tiempo que le permite comunicarse de manera segura con cualquier persona en la red Matrix u otras herramientas de colaboración empresarial al integrarse con aplicaciones como Slack. -Element te da el control permitiéndote elegir quién aloja tus conversaciones. Desde la aplicación Element, puedes elegir hospedar de diferentes maneras: +El elemento puede ser autohospedado +Para permitir un mayor control de sus conversaciones y datos confidenciales, Element puede ser autohospedado o puede elegir cualquier host basado en Matrix, el estándar para la comunicación descentralizada de código abierto. Element le brinda privacidad, cumplimiento de seguridad y flexibilidad de integración. -1. Obtén una cuenta gratuita en el servidor público de matrix.org alojado por los desarrolladores de Matrix, o elije entre miles de servidores públicos alojados por voluntarios -2. Autohospeda tu cuenta con un servidor en tu propio hardware -3. Regístrate para obtener una cuenta en un servidor personalizado simplemente suscribiéndote a la plataforma de alojamiento de Element Matrix Services +Sea dueño de sus datos +Tú decides dónde guardar tus datos y mensajes. Sin riesgo de minería de datos o acceso de terceros. -¿Por qué elegir Element? +Element te da el control de diferentes maneras: +1. Obtenga una cuenta gratuita en el servidor público de matrix.org alojado por los desarrolladores de Matrix, o elija entre miles de servidores públicos alojados por voluntarios +2. Autohospede su cuenta ejecutando un servidor en su propia infraestructura de TI +3. Regístrese para obtener una cuenta en un servidor personalizado simplemente suscribiéndose a la plataforma de alojamiento de Element Matrix Services -TOMA POSESIÓN DE TUS DATOS: Tú decides dónde guardar tus datos y mensajes. Tú eres el propietario y quien lo controla, no alguna MEGACORP que extrae tu datos o da acceso a terceros. +Colaboración y mensajería abierta +Puede chatear con cualquier persona en la red Matrix, ya sea que esté usando Element, otra aplicación Matrix o incluso si está usando una aplicación de mensajería diferente. -MENSAJERÍA ABIERTA Y COLABORACIÓN: Puede chatear con cualquier otra persona en la red de Matrix, tanto si usan Element u otra aplicación de Matrix, e incluso si están usando un sistema de mensajería diferente como Slack, IRC o XMPP. +Súper seguro +Cifrado real de extremo a extremo (solo aquellos en la conversación pueden descifrar mensajes) y verificación de dispositivos con firma cruzada. -SUPER SEGURO: Encriptación de Extremo-a-Extremo real (solo aquellos en la conversación pueden descifrar mensajes) y firma cruzada para verificar los dispositivos de los participantes de la conversación. +Completa comunicación e integración +Mensajería, llamadas de voz y video, uso compartido de archivos, uso compartido de pantalla y un montón de integraciones, bots y widgets. Construya salas, comunidades, manténgase en contacto y haga las cosas. -COMUNICACIÓN COMPLETA: Mensajería, llamadas de voz y video, uso compartido de archivos, uso compartido de pantalla y un montón de integraciones, bots y widgets. Crea salas, comunidades, mantente en contacto y organízate con eficacia. - -EN TODAS PARTES: Mantente en contacto donde quiera que estés con un historial de mensajes totalmente sincronizado en todos sus dispositivos y en la web en https://app.element.io. +Continúa donde lo dejaste +Manténgase en contacto donde quiera que esté con el historial de mensajes totalmente sincronizado en todos sus dispositivos y en la web en https://app.element.io diff --git a/fastlane/metadata/android/es-ES/title.txt b/fastlane/metadata/android/es-ES/title.txt index 971e5cf146..2e011d7ee7 100644 --- a/fastlane/metadata/android/es-ES/title.txt +++ b/fastlane/metadata/android/es-ES/title.txt @@ -1 +1 @@ -Element (previamente Riot.im) +Element - Mensajero seguro diff --git a/fastlane/metadata/android/et/changelogs/40102000.txt b/fastlane/metadata/android/et/changelogs/40102000.txt index 678ad05309..57f28039c5 100644 --- a/fastlane/metadata/android/et/changelogs/40102000.txt +++ b/fastlane/metadata/android/et/changelogs/40102000.txt @@ -1,2 +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 +Kogu ingliskeelne muudatuste logi: https://github.com/vector-im/element-android/releases/tag/v1.2.0 diff --git a/fastlane/metadata/android/et/changelogs/40102010.txt b/fastlane/metadata/android/et/changelogs/40102010.txt new file mode 100644 index 0000000000..0dc70c90af --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/40102010.txt @@ -0,0 +1,2 @@ +Põhilised muutused selles versioonis: palju täiendusi kõnede ja veel testjärgus olevas kogukonnakeskuste loogikas. +Kogu ingliskeelne muudatuste logi: https://github.com/vector-im/element-android/releases/tag/v1.2.1 diff --git a/fastlane/metadata/android/et/changelogs/40103000.txt b/fastlane/metadata/android/et/changelogs/40103000.txt new file mode 100644 index 0000000000..643ae1ce0e --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/40103000.txt @@ -0,0 +1,2 @@ +Põhilised muutused selles versioonis: halda oma jututubasid koondades neid uut tüüpi kogukondadesse! +Kogu ingliskeelne muudatuste logi: https://github.com/vector-im/element-android/releases/tag/v1.3.0 diff --git a/fastlane/metadata/android/et/changelogs/40103010.txt b/fastlane/metadata/android/et/changelogs/40103010.txt new file mode 100644 index 0000000000..e292f6db81 --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/40103010.txt @@ -0,0 +1,2 @@ +Põhilised muutused selles versioonis: halda oma jututubasid koondades neid uut tüüpi kogukondadesse! Lisaks parandasime versioonis 1.3.0 tekkinud olulise vea. +Kogu ingliskeelne muudatuste logi: https://github.com/vector-im/element-android/releases/tag/v1.3.1 diff --git a/fastlane/metadata/android/et/changelogs/40103020.txt b/fastlane/metadata/android/et/changelogs/40103020.txt new file mode 100644 index 0000000000..ca3c0d3ea5 --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/40103020.txt @@ -0,0 +1,2 @@ +Põhilised muutused selles versioonis: Android Auto tugi ning palju veaparandusi! +Kogu ingliskeelne muudatuste logi: https://github.com/vector-im/element-android/releases/tag/v1.3.2 diff --git a/fastlane/metadata/android/et/changelogs/40103030.txt b/fastlane/metadata/android/et/changelogs/40103030.txt new file mode 100644 index 0000000000..6293ccab85 --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/40103030.txt @@ -0,0 +1,2 @@ +Põhilised muutused selles versioonis: Isikutuvastusserveri kasutustingimused on leitavad seadistustest ja ajutiselt eemaldasime Android Auto toe. +Kogu ingliskeelne muudatuste logi: https://github.com/vector-im/element-android/releases/tag/v1.3.3 diff --git a/fastlane/metadata/android/et/full_description.txt b/fastlane/metadata/android/et/full_description.txt index ee0adef9ac..a3a3bc37e2 100644 --- a/fastlane/metadata/android/et/full_description.txt +++ b/fastlane/metadata/android/et/full_description.txt @@ -37,3 +37,6 @@ Sõnumid, hääl- ja videokõned, failide jagamine, ekraani jagamine ja terve hu Jätka sealt, kus pooleli jäid Saad suhelda kõigis oma seadmetes ja ka veebis aadressil https://app.element.io ning sealjuures täielikult sünkroonitud sõnumite ajalooga. + +Avatud lähtekoodiga tarkvara +Element Android on Github'is hallatud avatud lähtekoodiga tarkvaraprojekt. Palun teata vigadest ja/või osale arenduses https://github.com/vector-im/element-android lehel diff --git a/fastlane/metadata/android/fa/changelogs/40102000.txt b/fastlane/metadata/android/fa/changelogs/40102000.txt index c7e159bf2b..9c9a7c51d0 100644 --- a/fastlane/metadata/android/fa/changelogs/40102000.txt +++ b/fastlane/metadata/android/fa/changelogs/40102000.txt @@ -1,2 +1,2 @@ تغییرهای اصلی در این نگارش: پیام صوتی به صورت پیش‌گزیده به کار افتاده. -گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases/tag/v1.1.16 +گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases/tag/v1.2.0 diff --git a/fastlane/metadata/android/fa/changelogs/40102010.txt b/fastlane/metadata/android/fa/changelogs/40102010.txt new file mode 100644 index 0000000000..a2cc27d1b5 --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/40102010.txt @@ -0,0 +1,2 @@ +تغییرات اصلی در این نگارش: چندین بهبود در ویپ و فضاها (همچنان در حالت آزمایشی). +گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases/tag/v1.2.1 diff --git a/fastlane/metadata/android/fa/changelogs/40103000.txt b/fastlane/metadata/android/fa/changelogs/40103000.txt new file mode 100644 index 0000000000..ba43459c0a --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/40103000.txt @@ -0,0 +1,2 @@ +تغییرات عمده در این نگارش: سازمان‌دهی اتاق‌هایتان با استفاده از فضاها +گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases/tag/v1.3.0 diff --git a/fastlane/metadata/android/fa/changelogs/40103010.txt b/fastlane/metadata/android/fa/changelogs/40103010.txt new file mode 100644 index 0000000000..1a800ac505 --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/40103010.txt @@ -0,0 +1,2 @@ +تغییرات اصلی در این نگارش: سازمان‌دهی اتاق‌هایتان با فضاها! نگارش ۱٫۳٫۱ فروپاشی‌ای را که می‌توانست در نگارش ۱٫۳٫۰ رخ دهد، رفع می‌کند. +گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases/tag/v1.3.1 diff --git a/fastlane/metadata/android/fa/changelogs/40103020.txt b/fastlane/metadata/android/fa/changelogs/40103020.txt new file mode 100644 index 0000000000..be669d29a9 --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/40103020.txt @@ -0,0 +1,2 @@ +تغییرات اصلی در این نگارش: افزودن پشتیبانی از اندروید خودرو. کلّی رفع اشکال! +گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases/tag/v1.3.2 diff --git a/fastlane/metadata/android/fa/changelogs/40103030.txt b/fastlane/metadata/android/fa/changelogs/40103030.txt new file mode 100644 index 0000000000..49efae1951 --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/40103030.txt @@ -0,0 +1,2 @@ +تغییرات اصلی در این نگارش: نمایان کردن سیاست(های) کارساز هویت در تنظیمات. برداشتن موقّتی پشتیبانی اندروید خودرو. +گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases/tag/v1.3.3 diff --git a/fastlane/metadata/android/fa/full_description.txt b/fastlane/metadata/android/fa/full_description.txt index cd2d4eb4c1..98055857bb 100644 --- a/fastlane/metadata/android/fa/full_description.txt +++ b/fastlane/metadata/android/fa/full_description.txt @@ -37,3 +37,6 @@ ادامه از جایی که رها کرده‌اید هر کجا که هستید، با هم‌گام سازی کامل تاریخچهٔ پیام‌ها بین همهٔ افزاره‌هایتان و وب روی https://app.element.io در دسترس باشید + +نرم‌افزار آزاد +المنت اندروید، یک پروژهٔ نرم‌افزار آزاد میزبانی‌شده روی گیت‌هاب است. لطفاً گزارش مشکلات و مشارکت‌ها را به توسه‌اش به این نشانی بفرستید: https://github.com/vector-im/element-android diff --git a/fastlane/metadata/android/fr-FR/changelogs/40102000.txt b/fastlane/metadata/android/fr-FR/changelogs/40102000.txt new file mode 100644 index 0000000000..0bcf3551f6 --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/40102000.txt @@ -0,0 +1,2 @@ +Principaux changements pour cette version : messages vocaux activés par défaut. +Intégralité des changements : https://github.com/vector-im/element-android/releases/tag/v1.2.0 diff --git a/fastlane/metadata/android/fr-FR/changelogs/40102010.txt b/fastlane/metadata/android/fr-FR/changelogs/40102010.txt new file mode 100644 index 0000000000..910d4bd9c0 --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/40102010.txt @@ -0,0 +1,2 @@ +Principaux changements pour cette version : Beaucoup d’améliorations sur la VoIP et les Espaces (toujours en bêta). +Intégralité des changements : https://github.com/vector-im/element-android/releases/tag/v1.2.1 diff --git a/fastlane/metadata/android/fr-FR/changelogs/40103000.txt b/fastlane/metadata/android/fr-FR/changelogs/40103000.txt new file mode 100644 index 0000000000..66c2c3db86 --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/40103000.txt @@ -0,0 +1,2 @@ +Principaux changements pour cette version : Organisez vous salons à l’aide des Espaces ! +Intégralité des changements : https://github.com/vector-im/element-android/releases/tag/v1.3.0 diff --git a/fastlane/metadata/android/fr-FR/changelogs/40103010.txt b/fastlane/metadata/android/fr-FR/changelogs/40103010.txt new file mode 100644 index 0000000000..326319edb0 --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/40103010.txt @@ -0,0 +1,2 @@ +Principaux changements pour cette version : Organisez vos salons à l’aide des espaces ! La v1.3.1 corrige également un plantage dans la version v1.3.0 +Liste de tous les changements : https://github.com/vector-im/element-android/releases/tag/v1.3.1 diff --git a/fastlane/metadata/android/fr-FR/changelogs/40103020.txt b/fastlane/metadata/android/fr-FR/changelogs/40103020.txt new file mode 100644 index 0000000000..8a4868cee5 --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/40103020.txt @@ -0,0 +1,2 @@ +Principaux changements pour cette version : Ajout du support pour Android Auto. Beaucoup de corrections de bogues ! +Liste de tous les changements : https ://github.com/vector-im/element-android/releases/tag/v1.3.2 diff --git a/fastlane/metadata/android/fr-FR/changelogs/40103030.txt b/fastlane/metadata/android/fr-FR/changelogs/40103030.txt new file mode 100644 index 0000000000..0130080ca7 --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/40103030.txt @@ -0,0 +1,2 @@ +Principaux changements pour cette version : Affiche le(s) politique(s) des serveurs d’identité dans les réglages. Retrait temporaire du support d’Android Auto +Liste de tous les changements : https ://github.com/vector-im/element-android/releases/tag/v1.3.3 diff --git a/fastlane/metadata/android/fr-FR/full_description.txt b/fastlane/metadata/android/fr-FR/full_description.txt index 78fcdf5617..7ffdb6cb9d 100644 --- a/fastlane/metadata/android/fr-FR/full_description.txt +++ b/fastlane/metadata/android/fr-FR/full_description.txt @@ -37,3 +37,6 @@ Messagerie instantannée, appels audio et vidéo, partage de fichier, partage d Reprenez où vous vous êtes arrêté Restez en contact où que vous soyez grâce à l’historique des messages synchronisé entre tous vos appareils et sur le web sur https://app.element.io + +Open source +Element Adroid est un projet libre, hébergé par GitHub. Veuillez signaler tous les problèmes et / ou contribuer à son développement sur https://github.com/vector-im/element-android diff --git a/fastlane/metadata/android/hu-HU/changelogs/40102000.txt b/fastlane/metadata/android/hu-HU/changelogs/40102000.txt index 1e708b1c7b..87824693f7 100644 --- a/fastlane/metadata/android/hu-HU/changelogs/40102000.txt +++ b/fastlane/metadata/android/hu-HU/changelogs/40102000.txt @@ -1,2 +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 +Teljes változásnapló: https://github.com/vector-im/element-android/releases/tag/v1.2.0 diff --git a/fastlane/metadata/android/hu-HU/changelogs/40102010.txt b/fastlane/metadata/android/hu-HU/changelogs/40102010.txt new file mode 100644 index 0000000000..1ccd51aa8a --- /dev/null +++ b/fastlane/metadata/android/hu-HU/changelogs/40102010.txt @@ -0,0 +1,2 @@ +Fő változás ebben a verzióban: Sok fejlesztés a VoIP és Terek kapcsán (még béta) +Teljes változásnapló: https://github.com/vector-im/element-android/releases/tag/v1.2.1 diff --git a/fastlane/metadata/android/hu-HU/changelogs/40103000.txt b/fastlane/metadata/android/hu-HU/changelogs/40103000.txt new file mode 100644 index 0000000000..40673b30b1 --- /dev/null +++ b/fastlane/metadata/android/hu-HU/changelogs/40103000.txt @@ -0,0 +1,2 @@ +Fő változás ebben a verzióban: Szobák terekbe szervezése +Teljes változásnapló: https://github.com/vector-im/element-android/releases/tag/v1.3.0 diff --git a/fastlane/metadata/android/hu-HU/changelogs/40103010.txt b/fastlane/metadata/android/hu-HU/changelogs/40103010.txt new file mode 100644 index 0000000000..2cc0dab42d --- /dev/null +++ b/fastlane/metadata/android/hu-HU/changelogs/40103010.txt @@ -0,0 +1,2 @@ +Fő változás ebben a verzióban: Rendezd a szobáidat terekbe! v1.3.1 a v1.3.0 összeomlásait javítja. +Teljes változásnapló: https://github.com/vector-im/element-android/releases/tag/v1.3.1 diff --git a/fastlane/metadata/android/hu-HU/changelogs/40103020.txt b/fastlane/metadata/android/hu-HU/changelogs/40103020.txt new file mode 100644 index 0000000000..ff8af250e2 --- /dev/null +++ b/fastlane/metadata/android/hu-HU/changelogs/40103020.txt @@ -0,0 +1,2 @@ +Fő változás ebben a verzióban: Android Auto támogatás és sok hibajavítás! +Teljes változásnapló: https://github.com/vector-im/element-android/releases/tag/v1.3.2 diff --git a/fastlane/metadata/android/hu-HU/changelogs/40103030.txt b/fastlane/metadata/android/hu-HU/changelogs/40103030.txt new file mode 100644 index 0000000000..8795818607 --- /dev/null +++ b/fastlane/metadata/android/hu-HU/changelogs/40103030.txt @@ -0,0 +1,2 @@ +Fő változás ebben a verzióban: Azonosítási szerver feltételek megjelenítése a beállításoknál. Ideiglenesen az Android Auto támogatás eltávolítása. +Teljes változásnapló: https://github.com/vector-im/element-android/releases/tag/v1.3.3 diff --git a/fastlane/metadata/android/hu-HU/full_description.txt b/fastlane/metadata/android/hu-HU/full_description.txt index 032346ccb8..899b4cd978 100644 --- a/fastlane/metadata/android/hu-HU/full_description.txt +++ b/fastlane/metadata/android/hu-HU/full_description.txt @@ -38,3 +38,6 @@ Igazi végpontok között titkosítás (csak a beszélgetésben résztvevők tud Vedd fel a fonalat Maradj kapcsolatban bárhol minden eszközödön a szinkronizált üzenetekkel és a weben a https://app.element.io oldallal + +Nyílt forráskód +Element Android egy nyílt forráskódú projekt a GitHubon. Küldj hibajegyet és/vagy vegyél részt a fejlesztésében itt: https://github.com/vector-im/element-android diff --git a/fastlane/metadata/android/id/changelogs/40101160.txt b/fastlane/metadata/android/id/changelogs/40101160.txt new file mode 100644 index 0000000000..19209bacf2 --- /dev/null +++ b/fastlane/metadata/android/id/changelogs/40101160.txt @@ -0,0 +1,2 @@ +Perubahan utama dalam versi ini: Memperbaiki kesalahan saat mengirim pesan terenkripsi jika seseorang yang ada di ruangan keluar. +Changelog lanjutan: https://github.com/vector-im/element-android/releases/tag/v1.1.16 diff --git a/fastlane/metadata/android/id/changelogs/40102000.txt b/fastlane/metadata/android/id/changelogs/40102000.txt new file mode 100644 index 0000000000..f7d93e2e4f --- /dev/null +++ b/fastlane/metadata/android/id/changelogs/40102000.txt @@ -0,0 +1,2 @@ +Perubahan utama dalam versi ini: Pesan Suara diaktifkan secara default +Changelog lanjutan: https://github.com/vector-im/element-android/releases/tag/v1.2.0 diff --git a/fastlane/metadata/android/id/changelogs/40102010.txt b/fastlane/metadata/android/id/changelogs/40102010.txt new file mode 100644 index 0000000000..e77f0327b0 --- /dev/null +++ b/fastlane/metadata/android/id/changelogs/40102010.txt @@ -0,0 +1,2 @@ +Perubahan utama di versi ini: Banyak perbaikan di VoIP dan Space (masih beta). +Changelog lanjutan: https://github.com/vector-im/element-android/releases/tag/v1.2.1 diff --git a/fastlane/metadata/android/id/changelogs/40103000.txt b/fastlane/metadata/android/id/changelogs/40103000.txt new file mode 100644 index 0000000000..bf7b5d8d5d --- /dev/null +++ b/fastlane/metadata/android/id/changelogs/40103000.txt @@ -0,0 +1,2 @@ +Perubahan utama di versi ini: Organisir ruangan Anda menggunakan sebuah Space! +Changelog lanjutan: https://github.com/vector-im/element-android/releases/tag/v1.3.0 diff --git a/fastlane/metadata/android/id/changelogs/40103010.txt b/fastlane/metadata/android/id/changelogs/40103010.txt new file mode 100644 index 0000000000..7823017895 --- /dev/null +++ b/fastlane/metadata/android/id/changelogs/40103010.txt @@ -0,0 +1,2 @@ +Perubahan utama dalam versi ini: Organisir ruangan Anda dengan menggunakan sebuah Space! v1.3.1 memperbaiki crash yang dapat terjadi di v1.3.0. +Changelog lengkap: https://github.com/vector-im/element-android/releases/tag/v1.3.1 diff --git a/fastlane/metadata/android/id/changelogs/40103020.txt b/fastlane/metadata/android/id/changelogs/40103020.txt new file mode 100644 index 0000000000..4f46881d68 --- /dev/null +++ b/fastlane/metadata/android/id/changelogs/40103020.txt @@ -0,0 +1,2 @@ +Perubahan utama dalam versi ini: Penambahan dukungan untuk Android Auto. Banyak perbaikan bug! +Changelog lanjutan: https://github.com/vector-im/element-android/releases/tag/v1.3.2 diff --git a/fastlane/metadata/android/id/changelogs/40103030.txt b/fastlane/metadata/android/id/changelogs/40103030.txt new file mode 100644 index 0000000000..630593a107 --- /dev/null +++ b/fastlane/metadata/android/id/changelogs/40103030.txt @@ -0,0 +1,2 @@ +Perubahan utama dalam versi ini: Membuat kebijakan server identitas terlihat di pengaturan. Menghilangkan dukungan Android Auto untuk sementara. +Changelog lanjutan: https://github.com/vector-im/element-android/releases/tag/v1.3.3 diff --git a/fastlane/metadata/android/id/full_description.txt b/fastlane/metadata/android/id/full_description.txt index 0a18b8d64a..dfa9c8c826 100644 --- a/fastlane/metadata/android/id/full_description.txt +++ b/fastlane/metadata/android/id/full_description.txt @@ -8,10 +8,10 @@ Element adalah perpesanan yang aman dan aplikasi kolaborasi tim produktivitas ya - Obrolan video dengan VoIP dan berbagi layar - Integrasi yang mudah dengan alat kolaborasi online favorit Anda, alat manajemen proyek, layanan VoIP dan aplikasi perpesanan tim lainnya -Element benar-benar berbeda dari aplikasi perpesanan dan kolaborasi lainnya. Ini beroperasi pada Matrix, jaringan terbuka untuk pengiriman pesan yang aman dan komunikasi terdesentralisasi. Ini memungkinkan hosting sendiri untuk memberi pengguna kepemilikan maksimum dan kontrol data dan pesan mereka. +Element benar-benar berbeda dari aplikasi perpesanan dan kolaborasi lainnya. Element beroperasi pada Matrix, jaringan terbuka untuk pengiriman pesan yang aman dan komunikasi terdesentralisasi. Matrix memungkinkan hosting sendiri untuk memberi pengguna kepemilikan maksimum dan kontrol data dan pesan mereka. Pesan privasi dan terenkripsi -Element melindungi Anda dari iklan yang tidak diinginkan, data penambangan dan taman berdinding. Ini juga mengamankan semua data Anda, komunikasi video dan suara satu-ke-satu melalui enkripsi ujung-ke-ujung dan verifikasi perangkat yang di-cross-signed. +Element melindungi Anda dari iklan yang tidak diinginkan, penambangan data dan taman berdinding. Element juga mengamankan semua data Anda, komunikasi video dan suara satu-ke-satu melalui enkripsi ujung-ke-ujung dan verifikasi perangkat yang ditandatangani secara silang. Element memberi Anda kendali atas privasi Anda sambil memungkinkan Anda untuk berkomunikasi dengan aman dengan siapa pun di jaringan Matrix, atau alat kolaborasi bisnis lainnya dengan mengintegrasikan dengan aplikasi seperti Slack. @@ -30,10 +30,13 @@ Element menempatkan Anda dalam kendali dengan cara yang berbeda: Anda dapat mengobrol dengan siapa saja di jaringan Matrix, apakah mereka menggunakan Element, aplikasi Matrix lain atau bahkan jika mereka menggunakan aplikasi perpesanan yang berbeda. Sangat aman -Enkripsi ujung-ke-ujung beneran (hanya mereka yang dalam percakapan dapat mendekripsi pesan), dan verifikasi perangkat yang di-cross-signed. +Enkripsi ujung-ke-ujung yang nyata (hanya mereka yang dalam percakapan dapat mendekripsi pesan), dan verifikasi perangkat menggunakan penandatanganan-silang. Komunikasi dan integrasi lengkap Perpesanan, panggilan suara dan video, berbagi file, berbagi layar dan banyak integrasi, bot dan widget. Buat ruangan, komunitas, tetap terhubung dan selesaikan hal-hal. Ambil di mana Anda tinggalkan Tetap terhubung di mana pun Anda berada dengan riwayat pesan yang sepenuhnya disinkronkan di semua perangkat Anda dan di web di https://app.element.io + +Open source +Element Android adalah proyek sumber terbuka, di-host oleh GitHub. Silakan melaporkan bug dan/atau membuat kontribusi ke pengembangannya di https://github.com/vector-im/element-android diff --git a/fastlane/metadata/android/it-IT/changelogs/40102000.txt b/fastlane/metadata/android/it-IT/changelogs/40102000.txt index 3ba7f8ceb3..e10007a7b7 100644 --- a/fastlane/metadata/android/it-IT/changelogs/40102000.txt +++ b/fastlane/metadata/android/it-IT/changelogs/40102000.txt @@ -1,2 +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 +Cronologia completa: https://github.com/vector-im/element-android/releases/tag/v1.2.0 diff --git a/fastlane/metadata/android/it-IT/changelogs/40102010.txt b/fastlane/metadata/android/it-IT/changelogs/40102010.txt new file mode 100644 index 0000000000..33c2d998a7 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/40102010.txt @@ -0,0 +1,2 @@ +Modifiche principali in questa versione: molti miglioramenti nel VoIP e negli Spazi (ancora in beta). +Cronologia completa: https://github.com/vector-im/element-android/releases/tag/v1.2.1 diff --git a/fastlane/metadata/android/it-IT/changelogs/40103000.txt b/fastlane/metadata/android/it-IT/changelogs/40103000.txt new file mode 100644 index 0000000000..6ad9001bfd --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/40103000.txt @@ -0,0 +1,2 @@ +Modifiche principali in questa versione: organizza le tue stanze usando gli Spazi! +Cronologia completa: https://github.com/vector-im/element-android/releases/tag/v1.3.0 diff --git a/fastlane/metadata/android/it-IT/changelogs/40103010.txt b/fastlane/metadata/android/it-IT/changelogs/40103010.txt new file mode 100644 index 0000000000..125cf58137 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/40103010.txt @@ -0,0 +1,2 @@ +Modifiche principali in questa versione: organizza le tue stanze usando gli Spazi! v1.3.1 corregge un errore della v1.3.0. +Full changelog: https://github.com/vector-im/element-android/releases/tag/v1.3.1 diff --git a/fastlane/metadata/android/it-IT/changelogs/40103020.txt b/fastlane/metadata/android/it-IT/changelogs/40103020.txt new file mode 100644 index 0000000000..c088edc3b5 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/40103020.txt @@ -0,0 +1,2 @@ +Modifiche principali in questa versione: aggiunto supporto per Android Auto. Corretti molti errori! +Cronologia completa: https://github.com/vector-im/element-android/releases/tag/v1.3.2 diff --git a/fastlane/metadata/android/it-IT/changelogs/40103030.txt b/fastlane/metadata/android/it-IT/changelogs/40103030.txt new file mode 100644 index 0000000000..e5c12d2b5e --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/40103030.txt @@ -0,0 +1,2 @@ +Modifiche principali in questa versione: rese visibili le informative dei server d'identità nelle impostazioni. Rimosso temporaneamente il supporto per Android Auto. +Cronologia completa: https://github.com/vector-im/element-android/releases/tag/v1.3.3 diff --git a/fastlane/metadata/android/it-IT/full_description.txt b/fastlane/metadata/android/it-IT/full_description.txt index dd7716ffbf..f0e4c04477 100644 --- a/fastlane/metadata/android/it-IT/full_description.txt +++ b/fastlane/metadata/android/it-IT/full_description.txt @@ -37,3 +37,6 @@ Messaggi, chiamate audio e video, condivisione file e schermo, un vasto numero d Riprendi da dove ti eri fermato Resta in contatto ovunque tu sia con la cronologia dei messaggi sincronizzata tra tutti i tuoi dispositivi e in rete su https://app.element.io + +Open source +Element Android è un progetto open source, ospitato su GitHub. Segnala errori e/o contribuisci al suo sviluppo su https://github.com/vector-im/element-android diff --git a/fastlane/metadata/android/pt-BR/changelogs/40102000.txt b/fastlane/metadata/android/pt-BR/changelogs/40102000.txt index 3c600baeed..c6d01391da 100644 --- a/fastlane/metadata/android/pt-BR/changelogs/40102000.txt +++ b/fastlane/metadata/android/pt-BR/changelogs/40102000.txt @@ -1,2 +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 +Changelog completo: https://github.com/vector-im/element-android/releases/tag/v1.2.0 diff --git a/fastlane/metadata/android/pt-BR/changelogs/40102010.txt b/fastlane/metadata/android/pt-BR/changelogs/40102010.txt new file mode 100644 index 0000000000..0894dd2022 --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/40102010.txt @@ -0,0 +1,2 @@ +Principais mudanças nesta versão: Muitas melhorias em VoIP e Espaços (ainda em beta). +Changelog completo: https://github.com/vector-im/element-android/releases/tag/v1.2.1 diff --git a/fastlane/metadata/android/pt-BR/changelogs/40103000.txt b/fastlane/metadata/android/pt-BR/changelogs/40103000.txt new file mode 100644 index 0000000000..c046c1cbc9 --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/40103000.txt @@ -0,0 +1,2 @@ +Principais mudanças nesta versão: Organize suas salas usando Espaços! +Changelog completo: https://github.com/vector-im/element-android/releases/tag/v1.3.0 diff --git a/fastlane/metadata/android/pt-BR/changelogs/40103010.txt b/fastlane/metadata/android/pt-BR/changelogs/40103010.txt new file mode 100644 index 0000000000..25d497eaa9 --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/40103010.txt @@ -0,0 +1,2 @@ +Principais mudanças nesta versão: Organize suas salas usando Espaços! v1.3.1 está consertando um crash que pode ocorrer em v1.3.0. +Changelog completo: https://github.com/vector-im/element-android/releases/tag/v1.3.1 diff --git a/fastlane/metadata/android/pt-BR/changelogs/40103020.txt b/fastlane/metadata/android/pt-BR/changelogs/40103020.txt new file mode 100644 index 0000000000..e34e321494 --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/40103020.txt @@ -0,0 +1,2 @@ +Principais mudanças nesta versão: Adicionar suporte para Android Auto. Muitos consertos de bugs! +Changelog completo: https://github.com/vector-im/element-android/releases/tag/v1.3.2 diff --git a/fastlane/metadata/android/pt-BR/changelogs/40103030.txt b/fastlane/metadata/android/pt-BR/changelogs/40103030.txt new file mode 100644 index 0000000000..12ababb2ce --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/40103030.txt @@ -0,0 +1,2 @@ +Principais mudanças nesta versão: Fazer política(s) de servidor de identidade visível(is) nas configurações. Remover temporariamente suporte a Android Auto. +Changelog completo: https://github.com/vector-im/element-android/releases/tag/v1.3.3 diff --git a/fastlane/metadata/android/pt-BR/full_description.txt b/fastlane/metadata/android/pt-BR/full_description.txt index 7bb3d0981d..fe560cdda0 100644 --- a/fastlane/metadata/android/pt-BR/full_description.txt +++ b/fastlane/metadata/android/pt-BR/full_description.txt @@ -1,4 +1,4 @@ -Element é tanto um mensageiro seguro como um app de colaboração de time de produtividade que é ideal para chats de grupo enquanto se trabalha remotamente. Este app de chat usa encriptação ponta-a-ponta para prover conferência de vídeo, compartilhamento de arquivo e chamadas de voz poderasos. +Element é tanto um mensageiro seguro como um app de colaboração de time de produtividade que é ideal para chats de grupo enquanto se trabalha remotamente. Este app de chat usa encriptação ponta-a-ponta para prover conferência de vídeo, compartilhamento de arquivo e chamadas de voz poderosos. As funções de Element incluem: - Ferramentas de comunicação online avançadas @@ -22,9 +22,9 @@ Para permitir mais controle de seus dados e conversas sensíveis, Element pode s Você decide onde manter seus dados e mensagens. Sem o risco de data mining ou acesso de terceiros. Element põe você em controle de diferentes maneiras: -1. Pegar uma conta grátis no servidor público matrix.org hospedado pelos desenvolvedores Matrix, ou escolha de milhares de servidores públicos hospedados por pessoas se voluntariando -2. Auto-hospedar sua conta ao rodar um servidor em sua própria infraestrutura de TI -3. Fazer signup para uma conta num servidor personalizado ao simplesmente assinar a plataforma de hospedagem Element Matrix Services +1. Pegue uma conta grátis no servidor público matrix.org hospedado pelos desenvolvedores Matrix, ou escolha de milhares de servidores públicos hospedados por pessoas se voluntariando +2. Auto-hospede sua conta ao rodar um servidor em sua própria infraestrutura de TI +3. Faça signup para uma conta num servidor personalizado ao simplesmente assinar a plataforma de hospedagem Element Matrix Services Mensageria e colaboração abertos Você pode fazer chat com qualquer pessoa na rede Matrix, caso ela esteja usando Element, um outro app de Matrix ou mesmo se ela estiver usando um app de mensageria diferente. @@ -37,3 +37,6 @@ Messageria, chamadas de voz e vídeo, compartilhamento de arquivo, compartilhame Continue de onde você parou Fique em contato onde quer que você esteja com histórico de mensagem completamente sincronizado por todos os seus dispositivos e na web em https://app.element.io + +Open source +Element Android é um projeto open source, hospedado por GitHub. Por favor reporte bugs e/ou contribua para seu desenvolvimento em https://github.com/vector-im/element-android diff --git a/fastlane/metadata/android/ru-RU/changelogs/40101150.txt b/fastlane/metadata/android/ru-RU/changelogs/40101150.txt new file mode 100644 index 0000000000..cbf64e470b --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/40101150.txt @@ -0,0 +1,2 @@ +Основные изменения в этой версии: реализация голосовых сообщений в настройках лабораторий. +Полный список изменений: https://github.com/vector-im/element-android/releases/tag/v1.1.15 diff --git a/fastlane/metadata/android/ru-RU/changelogs/40101160.txt b/fastlane/metadata/android/ru-RU/changelogs/40101160.txt new file mode 100644 index 0000000000..5f0e555d94 --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/40101160.txt @@ -0,0 +1,2 @@ +Основные изменения в этой версии: Исправление ошибки при отправке зашифрованного сообщения, если кто-то в комнате выходит. +Полный список изменений: https://github.com/vector-im/element-android/releases/tag/v1.1.16 diff --git a/fastlane/metadata/android/ru-RU/changelogs/40102000.txt b/fastlane/metadata/android/ru-RU/changelogs/40102000.txt new file mode 100644 index 0000000000..af0a444afa --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/40102000.txt @@ -0,0 +1,2 @@ +Основные изменения в этой версии: Голосовое сообщение включено по умолчанию. +Полный список изменений: https://github.com/vector-im/element-android/releases/tag/v1.2.0 diff --git a/fastlane/metadata/android/ru-RU/changelogs/40102010.txt b/fastlane/metadata/android/ru-RU/changelogs/40102010.txt new file mode 100644 index 0000000000..167af260d5 --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/40102010.txt @@ -0,0 +1,2 @@ +Основные изменения в этой версии: Множество улучшений в VoIP и пространствах (все еще в бета-версии). +Полный список изменений: https://github.com/vector-im/element-android/releases/tag/v1.2.1 diff --git a/fastlane/metadata/android/ru-RU/changelogs/40103000.txt b/fastlane/metadata/android/ru-RU/changelogs/40103000.txt new file mode 100644 index 0000000000..7e87ca8524 --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/40103000.txt @@ -0,0 +1,2 @@ +Основные изменения в этой версии: Организуйте свои комнаты с помощью Пространств! +Весь список изменений: https://github.com/vector-im/element-android/releases/tag/v1.3.0 diff --git a/fastlane/metadata/android/ru-RU/changelogs/40103010.txt b/fastlane/metadata/android/ru-RU/changelogs/40103010.txt new file mode 100644 index 0000000000..e3a14e2d93 --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/40103010.txt @@ -0,0 +1,2 @@ +Основные изменения в этой версии: Организуйте свои комнаты с помощью Пространств! В версии 1.3.1 исправлен сбой, который мог произойти в версии 1.3.0. +Весь список изменений: https://github.com/vector-im/element-android/releases/tag/v1.3.1 diff --git a/fastlane/metadata/android/ru-RU/changelogs/40103020.txt b/fastlane/metadata/android/ru-RU/changelogs/40103020.txt new file mode 100644 index 0000000000..66059d5b92 --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/40103020.txt @@ -0,0 +1,2 @@ +Основные изменения в этой версии: Добавлена поддержка Android Auto. Исправлено множество ошибок! +Весь список изменений: https://github.com/vector-im/element-android/releases/tag/v1.3.2 diff --git a/fastlane/metadata/android/ru-RU/changelogs/40103030.txt b/fastlane/metadata/android/ru-RU/changelogs/40103030.txt new file mode 100644 index 0000000000..94b43e5f30 --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/40103030.txt @@ -0,0 +1,2 @@ +Основные изменения в этой версии: Правила сервера идентификации теперь видимы в настройках. Временно убрана поддержка Android Auto. +Полный список изменений: https://github.com/vector-im/element-android/releases/tag/v1.3.3 diff --git a/fastlane/metadata/android/ru-RU/full_description.txt b/fastlane/metadata/android/ru-RU/full_description.txt index 3d21b20a90..3d8949e466 100644 --- a/fastlane/metadata/android/ru-RU/full_description.txt +++ b/fastlane/metadata/android/ru-RU/full_description.txt @@ -37,3 +37,7 @@ Element дает вам возможность контролировать си Восстанавливайте связь с того места, где остановились. Оставайтесь на связи, где бы вы ни находились, с полностью синхронизированной историей сообщений на всех ваших устройствах и в Интернете по адресу https://app.element.io + + +Открытый исходный код +Element Android - это проект с открытым исходным кодом, размещенный на GitHub. Пожалуйста, сообщайте об ошибках и/или вносите вклад в его развитие по адресу https://github.com/vector-im/element-android. diff --git a/fastlane/metadata/android/sq/changelogs/40100100.txt b/fastlane/metadata/android/sq/changelogs/40100100.txt new file mode 100644 index 0000000000..aba7bebd5a --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40100100.txt @@ -0,0 +1,2 @@ +Ky version i ri përmban kryesisht ndreqje të metash dhe përmirësime. Dërgimi i një mesazhi tani është shumë i shpejtë. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases/tag/v1.0.10 diff --git a/fastlane/metadata/android/sq/changelogs/40100110.txt b/fastlane/metadata/android/sq/changelogs/40100110.txt new file mode 100644 index 0000000000..d1b8e9f9d3 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40100110.txt @@ -0,0 +1,2 @@ +Ky version i ri përmban kryesisht përmirësime të ndërfaqes dhe punimit të përdoruesit. Tani mund të ftoni shokë, dhe të krijoni MD shumë shpejt, përmes skanimit të kodesh QR. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases/tag/v1.0.11 diff --git a/fastlane/metadata/android/sq/changelogs/40100120.txt b/fastlane/metadata/android/sq/changelogs/40100120.txt new file mode 100644 index 0000000000..d7d9998e0b --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40100120.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: Paraparje URL-sh, tastierë e re për emoji, aftësi të reja për rregullime dhome, dhe dëborë për Krishtlindje! +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases/tag/v1.0.12 diff --git a/fastlane/metadata/android/sq/changelogs/40100130.txt b/fastlane/metadata/android/sq/changelogs/40100130.txt new file mode 100644 index 0000000000..5d50ff531d --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40100130.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: Paraparje URL-sh, tastierë e re për emoji, aftësi të reja për rregullime dhome, dhe dëborë për Krishtlindje! +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases/tag/v1.0.13 diff --git a/fastlane/metadata/android/sq/changelogs/40100140.txt b/fastlane/metadata/android/sq/changelogs/40100140.txt new file mode 100644 index 0000000000..bdab3841b0 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40100140.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: Përpunim lejesh dhome, temë e çelët/e errët e automatizuar, dhe një dorë ndreqjesh të metash. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases/tag/v1.0.14 diff --git a/fastlane/metadata/android/sq/changelogs/40100150.txt b/fastlane/metadata/android/sq/changelogs/40100150.txt new file mode 100644 index 0000000000..045f2369b6 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40100150.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: Mbulim Hyrjesh nga rrjete shoqërorë. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases/tag/v1.0.15 diff --git a/fastlane/metadata/android/sq/changelogs/40100160.txt b/fastlane/metadata/android/sq/changelogs/40100160.txt new file mode 100644 index 0000000000..ece7bbd2a6 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40100160.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: Mbulim Hyrjesh nga rrjete shoqërorë. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases/tag/v1.0.15 dhe https://github.com/vector-im/element-android/releases/tag/v1.0.16 diff --git a/fastlane/metadata/android/sq/changelogs/40100170.txt b/fastlane/metadata/android/sq/changelogs/40100170.txt new file mode 100644 index 0000000000..76cf7a9ffa --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40100170.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: Ndreqje të metash! +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases/tag/v1.0.17 diff --git a/fastlane/metadata/android/sq/changelogs/40101000.txt b/fastlane/metadata/android/sq/changelogs/40101000.txt new file mode 100644 index 0000000000..b4424f55bc --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40101000.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: Përmirësime për VoIP (thirrje audio dhe video në DM) dhe ndreqje të metash! +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases/tag/v1.1.0 diff --git a/fastlane/metadata/android/sq/changelogs/40101010.txt b/fastlane/metadata/android/sq/changelogs/40101010.txt new file mode 100644 index 0000000000..20b35d3439 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40101010.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: përmirësime funksionimi dhe ndreqje të metash! +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases/tag/v1.1.1 diff --git a/fastlane/metadata/android/sq/changelogs/40101020.txt b/fastlane/metadata/android/sq/changelogs/40101020.txt new file mode 100644 index 0000000000..6f53ec219e --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40101020.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: përmirësime funksionimi dhe ndreqje të metash! +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases/tag/v1.1.2 diff --git a/fastlane/metadata/android/sq/changelogs/40101030.txt b/fastlane/metadata/android/sq/changelogs/40101030.txt new file mode 100644 index 0000000000..9dbc4f142a --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40101030.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: përmirësime funksionimi dhe ndreqje të metash! +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases/tag/v1.1.3 diff --git a/fastlane/metadata/android/sq/changelogs/40101040.txt b/fastlane/metadata/android/sq/changelogs/40101040.txt new file mode 100644 index 0000000000..949fa629b9 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40101040.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: përmirësime funksionimi dhe ndreqje të metash! +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases/tag/v1.1.4 diff --git a/fastlane/metadata/android/sq/changelogs/40101050.txt b/fastlane/metadata/android/sq/changelogs/40101050.txt new file mode 100644 index 0000000000..28e2852356 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40101050.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: ndreqje të metash për 1.1.4 +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases/tag/v1.1.5 diff --git a/fastlane/metadata/android/sq/changelogs/40101060.txt b/fastlane/metadata/android/sq/changelogs/40101060.txt new file mode 100644 index 0000000000..ab01eede45 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40101060.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: ndreqje të metash për 1.1.5 +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases/tag/v1.1.6 diff --git a/fastlane/metadata/android/sq/changelogs/40101070.txt b/fastlane/metadata/android/sq/changelogs/40101070.txt new file mode 100644 index 0000000000..8d23bb5b94 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40101070.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: mbulim për Hapësira, në fazë beta. Ngjeshje videosh, përpara dërgimi. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases/tag/v1.1.7 diff --git a/fastlane/metadata/android/sq/changelogs/40101080.txt b/fastlane/metadata/android/sq/changelogs/40101080.txt new file mode 100644 index 0000000000..f9282142cb --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40101080.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: përmirësime për Hapësira. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases/tag/v1.1.8 diff --git a/fastlane/metadata/android/sq/changelogs/40101090.txt b/fastlane/metadata/android/sq/changelogs/40101090.txt new file mode 100644 index 0000000000..069ab4954d --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40101090.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: shtim mbulimi për rrjetin gitter.im. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases/tag/v1.1.9 diff --git a/fastlane/metadata/android/sq/changelogs/40101100.txt b/fastlane/metadata/android/sq/changelogs/40101100.txt new file mode 100644 index 0000000000..bf5079bc9a --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40101100.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: përditësime teme dhe stili dhe veçori të reja për hapësira. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases/tag/v1.1.10 diff --git a/fastlane/metadata/android/sq/changelogs/40101110.txt b/fastlane/metadata/android/sq/changelogs/40101110.txt new file mode 100644 index 0000000000..44d03bb8cb --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40101110.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: përditësime teme dhe stili dhe veçori të reja për hapësira (ndreqje të mete për 1.1.10) +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases/tag/v1.1.11 diff --git a/fastlane/metadata/android/sq/changelogs/40101120.txt b/fastlane/metadata/android/sq/changelogs/40101120.txt new file mode 100644 index 0000000000..aecede8d91 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40101120.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: përditësime teme dhe stili dhe ndreqje e një vithisjeje pas një thirrjeje video +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases/tag/v1.1.12 diff --git a/fastlane/metadata/android/sq/changelogs/40101130.txt b/fastlane/metadata/android/sq/changelogs/40101130.txt new file mode 100644 index 0000000000..535ccd7518 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40101130.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: përditësim kryesisht për qëndrueshmërinë dhe ndreqje të metash. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases/tag/v1.1.13 diff --git a/fastlane/metadata/android/sq/changelogs/40101140.txt b/fastlane/metadata/android/sq/changelogs/40101140.txt new file mode 100644 index 0000000000..2dc279e1f7 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40101140.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: ndreqje e një problemi rreth mesazhesh të fshehtëzuar. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases/tag/v1.1.14 diff --git a/fastlane/metadata/android/sq/changelogs/40101150.txt b/fastlane/metadata/android/sq/changelogs/40101150.txt new file mode 100644 index 0000000000..1fbf2bae7a --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40101150.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: sendërtim mesazhesh zanore, nën mjedis laboratori. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases/tag/v1.1.15 diff --git a/fastlane/metadata/android/sq/changelogs/40101160.txt b/fastlane/metadata/android/sq/changelogs/40101160.txt new file mode 100644 index 0000000000..ecb9a83918 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40101160.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: Ndreqje gabimi, kur dërgohet mesazh i fshehtëzuar, nëse dikush nga dhoma bën dalje prej saj. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases/tag/v1.1.16 diff --git a/fastlane/metadata/android/sq/changelogs/40102000.txt b/fastlane/metadata/android/sq/changelogs/40102000.txt new file mode 100644 index 0000000000..c1f2333f1c --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40102000.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: Mesazh Zanor është i aktivizuar, si parazgjedhje. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases/tag/v1.2.0 diff --git a/fastlane/metadata/android/sq/changelogs/40102010.txt b/fastlane/metadata/android/sq/changelogs/40102010.txt new file mode 100644 index 0000000000..6ffe456bd4 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40102010.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: Mjaft përmirësime në VoIP dhe Hapësira (ende në beta). +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases/tag/v1.2.1 diff --git a/fastlane/metadata/android/sq/changelogs/40103000.txt b/fastlane/metadata/android/sq/changelogs/40103000.txt new file mode 100644 index 0000000000..ecd5568c02 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40103000.txt @@ -0,0 +1,2 @@ +Ndryshime kryesore në këtë version: Sistemoni dhomat tuaja duke përdorur Hapësira! +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases/tag/v1.3.0 diff --git a/fastlane/metadata/android/sq/changelogs/40103010.txt b/fastlane/metadata/android/sq/changelogs/40103010.txt new file mode 100644 index 0000000000..1981135963 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40103010.txt @@ -0,0 +1,2 @@ +Ndryshime kryesore në këtë version: Sistemoni dhomat tuaja duke përdorur Hapësira! v1.3.1 ndreq një vithisje që mund të ndodhë në v1.3.0. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases/tag/v1.3.1 diff --git a/fastlane/metadata/android/sq/changelogs/40103020.txt b/fastlane/metadata/android/sq/changelogs/40103020.txt new file mode 100644 index 0000000000..6c8bd02cf0 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40103020.txt @@ -0,0 +1,2 @@ +Ndryshime kryesore në këtë version: Shtim mbulimi për Android Auto. Plot ndreqje të metash! +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases/tag/v1.3.2 diff --git a/fastlane/metadata/android/sq/changelogs/40103030.txt b/fastlane/metadata/android/sq/changelogs/40103030.txt new file mode 100644 index 0000000000..e52e91eed4 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40103030.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: Bërje të dukshëm e rregullit(ave) të shërbyesit të identiteteve te rregullimet. Heqje përkohësisht e mbulimit për Android Auto. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases/tag/v1.3.3 diff --git a/fastlane/metadata/android/sq/full_description.txt b/fastlane/metadata/android/sq/full_description.txt new file mode 100644 index 0000000000..ff0ea20da7 --- /dev/null +++ b/fastlane/metadata/android/sq/full_description.txt @@ -0,0 +1,42 @@ +Element-i është si aplikacion shkëmbyes i sigurt mesazhesh, ashtu edhe bashkëpunimi prodhimtar ekipi, i cili është ideal për fjalosje në grup, teksa punohet së largëti. Ky aplikacion fjalosjeje përdor fshehtëzim skaj-më-skaj për të furnizuar konferencë video, shkëmbim kartelash dhe thirrje me zë të fuqishme. + +Në veçoritë e Element-it përfshihen: +- Mjete të thelluara komunikimi internetor +- Mesazhe plotësisht të fshehtëzuar, për të lejuar komunikim në nivel korporate, madje edhe për punonjës së largëti +- Fjalosje e decentralizuar, bazuar në platformën me burim të hapët Matrix +- Shkëmbim i sigurt kartelash, me të dhëna të fshehtëzuara, teksa administrohen projekte +- Fjalosje video të llojit VoIP dhe tregim ekrani +- Integrim i kollajtë me mjetet tuaja të parapëlqyera të bashkëpunimit internetor, mjete administrimi projektesh, shërbime VoIP dhe aplikacione të tjera shkëmbimi mesazhesh në ekip + +Element-i është plotësisht i ndryshëm nga aplikacione të tjera shkëmbimi mesazhesh dhe bashkëpunimi. Funksionimi i tij bazohet në Matrix, një rrjet i hapët për mesazhe të siguruar dhe komunikim të decentralizuar. Lejon vetëstrehim, për t’u dhënë përdoruesve pronësi dhe kontroll maksimal të të dhënave dhe mesazheve të tyre. + +Privatësi dhe shkëmbim mesazhesh të fshehtëzuar +Element-i ju mbron nga reklama të padëshiruara, shfrytëzim të dhënash dhe vatha dixhitale. Ai siguron gjithashtu krejt të dhënat tuaja, komunikime tek-për-tek me video dhe me zë, përmes fshehtëzimi skaj-më-skaj dhe verifikim “cross-signed” pajisjesh. + +Element-i ju jep kontrollin e privatësisë tuaj, teksa ju lejon të komunikoni në mënyrë të siguruar me këdo në rrjetin Matrix, ose me mjete të tjera bashkëpunimi në shkallë biznesi, duke u integruar me aplikacione të tillë si Slack. + +Element-i mund të vetëstrehohet +Për të lejuar më tepër kontroll mbi të dhënat dhe bisedat tuaja rezervat, Element-i mund të vetëstrehohet, ose mund të zgjidhni cilëndo strehë të bazuar në Matrix - standardi për komunikim me burim të hapët, të decentralizuar. Element-i ju jep privatësi, pajtueshmëri sigurie dhe zhdërvjelltësi integrimesh. + +Jini zot i të dhënave tuaja +Ju vendosni ku të mbahen të dhënat dhe mesazhet tuaja. Pa rrezikun e shfrytëzimit të të dhënave apo hyrjes në to nga palë të treta. + +Element-i ju vë ju në kontroll përmes rrugësh të ndryshme: +1. Merrni një llogari falas te shërbyesi publik matrix.org strehuar nga zhvillues të Matrix-it, ose zgjidhni prej mijëra shërbyesish publikë të strehuar nga vullnetarë +2. Vetëstrehoni llogarinë tuaj duke xhiruar një shërbyes në infrastrukturën tuaj TI +3. Regjistrohuni për një llogari në një shërbyes vetjak, thjesht duke u pajtuar te platforma Element Matrix Services e strehimeve + +Shkëmbim mesazhesh dhe bashkëpunim me burim të hapët +Mund të fjaloseni me këdo në rrjetin Matrix, qoftë kur përdorin Element, një tjetër aplikacion Matrix, apo edhe kur përdorin një tjetër aplikacion shkëmbimi mesazhesh. + +Super i sigurt +Fshehtëzim i njëmendtë skaj-më-skaj (vetëm ata te biseda mund të shfshehtëzojnë mesazhe), dhe verifikim “cross-signed” pajisjesh. + +Komunikim dhe integrim i plotë +Shkëmbim mesazhesh, thirrje me zë dhe me video, shkëmbim kartelash, tregim ekrani dhe një grup i tërë integrimesh, robotësh dhe widget-esh. Krijoni dhoma, bashkësi, mbani lidhjet dhe mbaroni punë. + +Rifillojani atje ku e latë +Jini në dijeni, kudo ku gjendeni, me historik plotësisht të njëkohësuar mesazhesh nëpër krejt pajisjet tuaja dhe në internet te https://app.element.io + +Me burim të hapët +Element-i për Android është një projekt me burim të hapët, strehuar në GitHub. Ju lutemi, njoftoni të meta dhe/ose jepni ndihmesë në zhvillimin e tij te https://github.com/vector-im/element-android diff --git a/fastlane/metadata/android/sq/short_description.txt b/fastlane/metadata/android/sq/short_description.txt new file mode 100644 index 0000000000..21937ccce5 --- /dev/null +++ b/fastlane/metadata/android/sq/short_description.txt @@ -0,0 +1 @@ +Mesazhe grupi - mesazhe, fjalosje në grup dhe thirrje me video, të fshehtëzuara diff --git a/fastlane/metadata/android/sq/title.txt b/fastlane/metadata/android/sq/title.txt index b46bbc02b1..097f9c48ea 100644 --- a/fastlane/metadata/android/sq/title.txt +++ b/fastlane/metadata/android/sq/title.txt @@ -1 +1 @@ -Element - Shkëmbyes I Sigurt Mesazhesh +Element - Shkëmbyes i Sigurt Mesazhesh diff --git a/fastlane/metadata/android/sv-SE/changelogs/40102010.txt b/fastlane/metadata/android/sv-SE/changelogs/40102010.txt new file mode 100644 index 0000000000..f29b95de79 --- /dev/null +++ b/fastlane/metadata/android/sv-SE/changelogs/40102010.txt @@ -0,0 +1,2 @@ +Huvudsakliga ändringar i den här versionen: Många förbättringar för VoIP och utrymmen (fortfarande i beta). +Full ändringslogg: https://github.com/vector-im/element-android/releases/tag/v1.2.1 diff --git a/fastlane/metadata/android/sv-SE/changelogs/40103000.txt b/fastlane/metadata/android/sv-SE/changelogs/40103000.txt new file mode 100644 index 0000000000..d9a2c34f1d --- /dev/null +++ b/fastlane/metadata/android/sv-SE/changelogs/40103000.txt @@ -0,0 +1,2 @@ +Huvudsakliga ändringar i den här versionen: Organisera dina rum med utrymmen! +Full ändringslogg: https://github.com/vector-im/element-android/releases/tag/v1.3.0 diff --git a/fastlane/metadata/android/sv-SE/changelogs/40103010.txt b/fastlane/metadata/android/sv-SE/changelogs/40103010.txt new file mode 100644 index 0000000000..78c2c57ae6 --- /dev/null +++ b/fastlane/metadata/android/sv-SE/changelogs/40103010.txt @@ -0,0 +1,2 @@ +Huvudsakliga ändringar i den här versionen: Organisera dina rum med utrymmen! v1.3.1 fixar en krasch som kan hända i v1.3.0. +Full ändringslogg: https://github.com/vector-im/element-android/releases/tag/v1.3.1 diff --git a/fastlane/metadata/android/sv-SE/changelogs/40103020.txt b/fastlane/metadata/android/sv-SE/changelogs/40103020.txt new file mode 100644 index 0000000000..4afd54d9be --- /dev/null +++ b/fastlane/metadata/android/sv-SE/changelogs/40103020.txt @@ -0,0 +1,2 @@ +Huvudsakliga ändringar i den här versionen: Lägg till stöd för Android Auto. Massa buggfixar! +Full ändringslogg: https://github.com/vector-im/element-android/releases/tag/v1.3.2 diff --git a/fastlane/metadata/android/sv-SE/changelogs/40103030.txt b/fastlane/metadata/android/sv-SE/changelogs/40103030.txt new file mode 100644 index 0000000000..3d55703e86 --- /dev/null +++ b/fastlane/metadata/android/sv-SE/changelogs/40103030.txt @@ -0,0 +1,2 @@ +Huvudsakliga ändringar i den här versionen: Gör identitetsserverpolicy(er) synliga i inställningarna. Ta tillfälligt bort stöd för Android Auto. +Full ändringslogg: https://github.com/vector-im/element-android/releases/tag/v1.3.3 diff --git a/fastlane/metadata/android/sv-SE/full_description.txt b/fastlane/metadata/android/sv-SE/full_description.txt index 5302976ed7..03d837626e 100644 --- a/fastlane/metadata/android/sv-SE/full_description.txt +++ b/fastlane/metadata/android/sv-SE/full_description.txt @@ -37,3 +37,6 @@ Meddelanden, röst- och videosamtal, fildelning, skärmdelning och massa integra Fortsätt där du lämnade Håll kontakten vart du än är med fullt synkroniserad meddelandehistorik på alla dina enheter och på webben på https://app.element.io + +Öppen källkod +Element Android är projekt baserat på öppen källkod, som ligger på GitHub. Vänligen rapportera buggar och/eller bidra till dess utveckling på https://github.com/vector-im/element-android diff --git a/fastlane/metadata/android/uk/changelogs/40102000.txt b/fastlane/metadata/android/uk/changelogs/40102000.txt index 07defcbb57..9abc8c0298 100644 --- a/fastlane/metadata/android/uk/changelogs/40102000.txt +++ b/fastlane/metadata/android/uk/changelogs/40102000.txt @@ -1,2 +1,2 @@ Основні зміни в цій версії: голосові повідомлення типово увімкнено. -Повний журнал змін: https://github.com/vector-im/element-android/releases/tag/v1.1.16 +Повний журнал змін: https://github.com/vector-im/element-android/releases/tag/v1.2.0 diff --git a/fastlane/metadata/android/uk/changelogs/40102010.txt b/fastlane/metadata/android/uk/changelogs/40102010.txt new file mode 100644 index 0000000000..39a8d839b6 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/40102010.txt @@ -0,0 +1,2 @@ +Основні зміни в цій версії: багато вдосконалень VoIP і просторів (досі бета) +Повний журнал змін: https://github.com/vector-im/element-android/releases/tag/v1.2.1 diff --git a/fastlane/metadata/android/uk/changelogs/40103000.txt b/fastlane/metadata/android/uk/changelogs/40103000.txt new file mode 100644 index 0000000000..64a168cbe9 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/40103000.txt @@ -0,0 +1,2 @@ +Основні зміни в цій версії: Упорядковуйте свої кімнати за допомогою Просторів! +Повний журнал змін: https://github.com/vector-im/element-android/releases/tag/v1.3.0 diff --git a/fastlane/metadata/android/uk/changelogs/40103010.txt b/fastlane/metadata/android/uk/changelogs/40103010.txt new file mode 100644 index 0000000000..5940cdedb0 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/40103010.txt @@ -0,0 +1,2 @@ +Основні зміни в цій версії: Впорядковуйте кімнати у простори. v1.3.1 виправляє збої, які виникали у v1.3.0. +Повний журнал змін: https://github.com/vector-im/element-android/releases/tag/v1.3.1 diff --git a/fastlane/metadata/android/uk/changelogs/40103020.txt b/fastlane/metadata/android/uk/changelogs/40103020.txt new file mode 100644 index 0000000000..dc80b0be10 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/40103020.txt @@ -0,0 +1,2 @@ +Основні зміни в цій версії: Додано підтримку Android Auto. Виправлення багато помилок! +Повний журнал змін: https://github.com/vector-im/element-android/releases/tag/v1.3.2 diff --git a/fastlane/metadata/android/uk/changelogs/40103030.txt b/fastlane/metadata/android/uk/changelogs/40103030.txt new file mode 100644 index 0000000000..af25d23c42 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/40103030.txt @@ -0,0 +1,2 @@ +Основні зміни в цій версії: додано політику ідентифікації сервера (IES) у налаштуваннях. Тимчасово вилучено автозаповнення Android. +Усі зміни: https://github.com/vector-im/element-android/releases/tag/v1.3.3 diff --git a/fastlane/metadata/android/uk/full_description.txt b/fastlane/metadata/android/uk/full_description.txt index 285f577452..3c59d860ac 100644 --- a/fastlane/metadata/android/uk/full_description.txt +++ b/fastlane/metadata/android/uk/full_description.txt @@ -37,3 +37,6 @@ Element надає такі можливості на вибір: Продовжуйте, де зупинилися Залишайтеся на зв'язку, де б ви не знаходились, з повністю синхронізованою історією повідомлень на всіх своїх пристроях та в Інтернеті за адресою https://app.element.io + +Відкритий код +Element для Android це проєкт з відкритим кодом, розміщений GitHub. Будь ласка, повідомте про помилки та/або сприяйте його розвитку на https://github.com/vector-im/element-android diff --git a/fastlane/metadata/android/zh-CN/changelogs/40102000.txt b/fastlane/metadata/android/zh-CN/changelogs/40102000.txt index eedbe81bac..fa1db16805 100644 --- a/fastlane/metadata/android/zh-CN/changelogs/40102000.txt +++ b/fastlane/metadata/android/zh-CN/changelogs/40102000.txt @@ -1,2 +1,2 @@ 此版本中的主要更改:默认启用语音消息。 -完整更新日志:https://github.com/vector-im/element-android/releases/tag/v1.1.16 +完整更新日志:https://github.com/vector-im/element-android/releases/tag/v1.2.0 diff --git a/fastlane/metadata/android/zh-CN/changelogs/40102010.txt b/fastlane/metadata/android/zh-CN/changelogs/40102010.txt new file mode 100644 index 0000000000..2ec2ae22b3 --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/40102010.txt @@ -0,0 +1,2 @@ +这个版本的主要变化:VoIP和空间的许多改进(仍在测试中)。 +完整更新日志:https://github.com/vector-im/element-android/releases/tag/v1.2.1 diff --git a/fastlane/metadata/android/zh-CN/changelogs/40103000.txt b/fastlane/metadata/android/zh-CN/changelogs/40103000.txt new file mode 100644 index 0000000000..96ec8b3322 --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/40103000.txt @@ -0,0 +1,2 @@ +此版本主要更改:使用空间组织你的聊天室! +完整更新日志:https://github.com/vector-im/element-android/releases/tag/v1.3.0 diff --git a/fastlane/metadata/android/zh-CN/changelogs/40103010.txt b/fastlane/metadata/android/zh-CN/changelogs/40103010.txt new file mode 100644 index 0000000000..98b506fb6e --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/40103010.txt @@ -0,0 +1,2 @@ +此版本的主要变化:使用空间组织您的聊天室! v1.3.1 正在修复 v1.3.0 中可能发生的崩溃。 +完整更新日志:https://github.com/vector-im/element-android/releases/tag/v1.3.1 diff --git a/fastlane/metadata/android/zh-CN/changelogs/40103020.txt b/fastlane/metadata/android/zh-CN/changelogs/40103020.txt new file mode 100644 index 0000000000..586ba8d892 --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/40103020.txt @@ -0,0 +1,2 @@ +此版本的主要变化: 添加对 Android Auto 的支持。 许多错误修复! +完整更新日志:https://github.com/vector-im/element-android/releases/tag/v1.3.2 diff --git a/fastlane/metadata/android/zh-CN/changelogs/40103030.txt b/fastlane/metadata/android/zh-CN/changelogs/40103030.txt new file mode 100644 index 0000000000..1f6ff391e1 --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/40103030.txt @@ -0,0 +1,2 @@ +此版本中的主要更改:使身份服务器策略在设置中可见。 暂时移除 Android Auto 支持。 +完整更新日志:https://github.com/vector-im/element-android/releases/tag/v1.3.3 diff --git a/fastlane/metadata/android/zh-CN/full_description.txt b/fastlane/metadata/android/zh-CN/full_description.txt index fa6b00f1e4..3dae8deb67 100644 --- a/fastlane/metadata/android/zh-CN/full_description.txt +++ b/fastlane/metadata/android/zh-CN/full_description.txt @@ -37,3 +37,6 @@ Element 透过不同的方式让你掌控一切: 从上次离开的地方开始 无论你身在何处,都可以透过在你所有设备与网页 https://app.element.io 间完全同步的信息历史保持联络 + +开源 + Element Android 是一个开源项目,由 GitHub 托管。 请在 https://github.com/vector-im/element-android 报告错误和/或为其开发做出贡献 diff --git a/fastlane/metadata/android/zh-TW/changelogs/40102010.txt b/fastlane/metadata/android/zh-TW/changelogs/40102010.txt new file mode 100644 index 0000000000..b520266a78 --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/40102010.txt @@ -0,0 +1,2 @@ +此版本中的主要變動:對 VoIP 與空間功能的諸多改善(仍在測試中)。 +完整的變更紀錄:https://github.com/vector-im/element-android/releases/tag/v1.2.1 diff --git a/fastlane/metadata/android/zh-TW/changelogs/40103000.txt b/fastlane/metadata/android/zh-TW/changelogs/40103000.txt new file mode 100644 index 0000000000..fbae69cd21 --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/40103000.txt @@ -0,0 +1,2 @@ +此版本中的主要變動:使用空間來整理您的聊天室! +完整的變更紀錄:https://github.com/vector-im/element-android/releases/tag/v1.3.0 diff --git a/fastlane/metadata/android/zh-TW/changelogs/40103010.txt b/fastlane/metadata/android/zh-TW/changelogs/40103010.txt new file mode 100644 index 0000000000..95dcd59e46 --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/40103010.txt @@ -0,0 +1,2 @@ +此版本中的主要變動:使用空間來整理您的聊天室!v1.3.1 修復了在 v1.3.0 中遇到的當機問題。 +完整的變更紀錄:https://github.com/vector-im/element-android/releases/tag/v1.3.1 diff --git a/fastlane/metadata/android/zh-TW/changelogs/40103020.txt b/fastlane/metadata/android/zh-TW/changelogs/40103020.txt new file mode 100644 index 0000000000..6a00bed1e7 --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/40103020.txt @@ -0,0 +1,2 @@ +此版本中的主要變動:新增對 Android Auto 的支援。以及許多錯誤修復! +完整的變更紀錄:https://github.com/vector-im/element-android/releases/tag/v1.3.2 diff --git a/fastlane/metadata/android/zh-TW/changelogs/40103030.txt b/fastlane/metadata/android/zh-TW/changelogs/40103030.txt new file mode 100644 index 0000000000..7531d1d4a2 --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/40103030.txt @@ -0,0 +1,2 @@ +此版本中的主要變動:讓身份伺服器政策在設定中可見。暫時移除 Android Auto 支援。 +完整的變更紀錄:https://github.com/vector-im/element-android/releases/tag/v1.3.3 diff --git a/fastlane/metadata/android/zh-TW/full_description.txt b/fastlane/metadata/android/zh-TW/full_description.txt index 90c0eb4c4c..eda511c4af 100644 --- a/fastlane/metadata/android/zh-TW/full_description.txt +++ b/fastlane/metadata/android/zh-TW/full_description.txt @@ -37,3 +37,6 @@ Element 透過不同的方式讓您掌控一切: 從上次離開的地方開始 無論您身在何處,都可以透過在您所有裝置與網頁 https://app.element.io 間完全同步的訊息歷史保持聯絡 + +開放原始碼 +Android 版的 Element 是開放原始碼專案,託管於 GitHub 上。請在 https://github.com/vector-im/element-android 上回報臭蟲及/或貢獻其開發 diff --git a/gradle.properties b/gradle.properties index 98d561815b..23538c5285 100644 --- a/gradle.properties +++ b/gradle.properties @@ -23,3 +23,6 @@ vector.debugPrivateData=false # httpLogLevel values: NONE, BASIC, HEADERS, BODY vector.httpLogLevel=BASIC +# Note: to debug, you can put and uncomment the following lines in the file ~/.gradle/gradle.properties to override the value above +#vector.debugPrivateData=true +#vector.httpLogLevel=BODY \ No newline at end of file diff --git a/library/ui-styles/src/main/res/values/colors.xml b/library/ui-styles/src/main/res/values/colors.xml index b58829bb81..03d1ff69db 100644 --- a/library/ui-styles/src/main/res/values/colors.xml +++ b/library/ui-styles/src/main/res/values/colors.xml @@ -133,4 +133,8 @@ @color/palette_black_900 @color/palette_gray_400 + + + @color/palette_gray_100 + @color/palette_gray_450 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 f83953a527..d07e3c5297 100644 --- a/library/ui-styles/src/main/res/values/theme_dark.xml +++ b/library/ui-styles/src/main/res/values/theme_dark.xml @@ -42,6 +42,9 @@ @android:color/black #FFFFFFFF + + @color/vctr_presence_indicator_offline_dark + ?vctr_system ?vctr_content_quinary 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 cd5e17d607..14ec372f28 100644 --- a/library/ui-styles/src/main/res/values/theme_light.xml +++ b/library/ui-styles/src/main/res/values/theme_light.xml @@ -42,6 +42,9 @@ #FFEEEEEE #FF000000 + + @color/vctr_presence_indicator_offline_light + ?vctr_system ?vctr_content_quinary diff --git a/matrix-sdk-android-flow/.gitignore b/matrix-sdk-android-flow/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/matrix-sdk-android-flow/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/matrix-sdk-android-flow/build.gradle b/matrix-sdk-android-flow/build.gradle new file mode 100644 index 0000000000..ea43ce20c8 --- /dev/null +++ b/matrix-sdk-android-flow/build.gradle @@ -0,0 +1,47 @@ + +plugins { + id 'com.android.library' + id 'org.jetbrains.kotlin.android' +} + +android { + compileSdk versions.compileSdk + + defaultConfig { + minSdk versions.minSdk + targetSdk versions.targetSdk + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles "consumer-rules.pro" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility versions.sourceCompat + targetCompatibility versions.targetCompat + } + kotlinOptions { + jvmTarget = "11" + } +} + +dependencies { + + implementation project(":matrix-sdk-android") + implementation libs.androidx.appCompat + + implementation libs.jetbrains.coroutinesCore + implementation libs.jetbrains.coroutinesAndroid + implementation libs.androidx.lifecycleLivedata + + // Paging + implementation libs.androidx.pagingRuntimeKtx + + // Logging + implementation libs.jakewharton.timber +} diff --git a/matrix-sdk-android-flow/consumer-rules.pro b/matrix-sdk-android-flow/consumer-rules.pro new file mode 100644 index 0000000000..e69de29bb2 diff --git a/matrix-sdk-android-flow/proguard-rules.pro b/matrix-sdk-android-flow/proguard-rules.pro new file mode 100644 index 0000000000..481bb43481 --- /dev/null +++ b/matrix-sdk-android-flow/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/matrix-sdk-android-flow/src/main/AndroidManifest.xml b/matrix-sdk-android-flow/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..2392c0bfcb --- /dev/null +++ b/matrix-sdk-android-flow/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowExt.kt b/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowExt.kt new file mode 100644 index 0000000000..72493325c3 --- /dev/null +++ b/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowExt.kt @@ -0,0 +1,31 @@ +/* + * 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 org.matrix.android.sdk.flow + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.withContext + +internal fun Flow.startWith(dispatcher: CoroutineDispatcher, supplier: suspend () -> T): Flow { + return onStart { + val value = withContext(dispatcher) { + supplier() + } + emit(value) + } +} diff --git a/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt b/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt new file mode 100644 index 0000000000..42c1476b79 --- /dev/null +++ b/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt @@ -0,0 +1,105 @@ +/* + * Copyright 2020 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.flow + +import androidx.lifecycle.asFlow +import kotlinx.coroutines.flow.Flow +import org.matrix.android.sdk.api.query.QueryStringValue +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.room.Room +import org.matrix.android.sdk.api.session.room.members.RoomMemberQueryParams +import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary +import org.matrix.android.sdk.api.session.room.model.ReadReceipt +import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary +import org.matrix.android.sdk.api.session.room.model.RoomSummary +import org.matrix.android.sdk.api.session.room.notification.RoomNotificationState +import org.matrix.android.sdk.api.session.room.send.UserDraft +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.api.util.toOptional + +class FlowRoom(private val room: Room) { + + fun liveRoomSummary(): Flow> { + return room.getRoomSummaryLive().asFlow() + .startWith(room.coroutineDispatchers.io) { + room.roomSummary().toOptional() + } + } + + fun liveRoomMembers(queryParams: RoomMemberQueryParams): Flow> { + return room.getRoomMembersLive(queryParams).asFlow() + .startWith(room.coroutineDispatchers.io) { + room.getRoomMembers(queryParams) + } + } + + fun liveAnnotationSummary(eventId: String): Flow> { + return room.getEventAnnotationsSummaryLive(eventId).asFlow() + .startWith(room.coroutineDispatchers.io) { + room.getEventAnnotationsSummary(eventId).toOptional() + } + } + + fun liveTimelineEvent(eventId: String): Flow> { + return room.getTimeLineEventLive(eventId).asFlow() + .startWith(room.coroutineDispatchers.io) { + room.getTimeLineEvent(eventId).toOptional() + } + } + + fun liveStateEvent(eventType: String, stateKey: QueryStringValue): Flow> { + return room.getStateEventLive(eventType, stateKey).asFlow() + .startWith(room.coroutineDispatchers.io) { + room.getStateEvent(eventType, stateKey).toOptional() + } + } + + fun liveStateEvents(eventTypes: Set): Flow> { + return room.getStateEventsLive(eventTypes).asFlow() + .startWith(room.coroutineDispatchers.io) { + room.getStateEvents(eventTypes) + } + } + + fun liveReadMarker(): Flow> { + return room.getReadMarkerLive().asFlow() + } + + fun liveReadReceipt(): Flow> { + return room.getMyReadReceiptLive().asFlow() + } + + fun liveEventReadReceipts(eventId: String): Flow> { + return room.getEventReadReceiptsLive(eventId).asFlow() + } + + fun liveDraft(): Flow> { + return room.getDraftLive().asFlow() + .startWith(room.coroutineDispatchers.io) { + room.getDraft().toOptional() + } + } + + fun liveNotificationState(): Flow { + return room.getLiveRoomNotificationState().asFlow() + } +} + +fun Room.flow(): FlowRoom { + return FlowRoom(this) +} diff --git a/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowSession.kt b/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowSession.kt new file mode 100644 index 0000000000..13fd097bcd --- /dev/null +++ b/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowSession.kt @@ -0,0 +1,180 @@ +/* + * Copyright 2020 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.flow + +import androidx.lifecycle.asFlow +import androidx.paging.PagedList +import kotlinx.coroutines.flow.Flow +import org.matrix.android.sdk.api.query.QueryStringValue +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent +import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo +import org.matrix.android.sdk.api.session.group.GroupSummaryQueryParams +import org.matrix.android.sdk.api.session.group.model.GroupSummary +import org.matrix.android.sdk.api.session.identity.ThreePid +import org.matrix.android.sdk.api.session.pushers.Pusher +import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams +import org.matrix.android.sdk.api.session.room.accountdata.RoomAccountDataEvent +import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState +import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary +import org.matrix.android.sdk.api.session.room.model.RoomSummary +import org.matrix.android.sdk.api.session.space.SpaceSummaryQueryParams +import org.matrix.android.sdk.api.session.sync.SyncState +import org.matrix.android.sdk.api.session.user.model.User +import org.matrix.android.sdk.api.session.widgets.model.Widget +import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.api.util.toOptional +import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo +import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo +import org.matrix.android.sdk.internal.crypto.store.PrivateKeysInfo + +class FlowSession(private val session: Session) { + + fun liveRoomSummaries(queryParams: RoomSummaryQueryParams): Flow> { + return session.getRoomSummariesLive(queryParams).asFlow() + .startWith(session.coroutineDispatchers.io) { + session.getRoomSummaries(queryParams) + } + } + + fun liveGroupSummaries(queryParams: GroupSummaryQueryParams): Flow> { + return session.getGroupSummariesLive(queryParams).asFlow() + .startWith(session.coroutineDispatchers.io) { + session.getGroupSummaries(queryParams) + } + } + + fun liveSpaceSummaries(queryParams: SpaceSummaryQueryParams): Flow> { + return session.spaceService().getSpaceSummariesLive(queryParams).asFlow() + .startWith(session.coroutineDispatchers.io) { + session.spaceService().getSpaceSummaries(queryParams) + } + } + + fun liveBreadcrumbs(queryParams: RoomSummaryQueryParams): Flow> { + return session.getBreadcrumbsLive(queryParams).asFlow() + .startWith(session.coroutineDispatchers.io) { + session.getBreadcrumbs(queryParams) + } + } + + fun liveMyDevicesInfo(): Flow> { + return session.cryptoService().getLiveMyDevicesInfo().asFlow() + .startWith(session.coroutineDispatchers.io) { + session.cryptoService().getMyDevicesInfo() + } + } + + fun liveSyncState(): Flow { + return session.getSyncStateLive().asFlow() + } + + fun livePushers(): Flow> { + return session.getPushersLive().asFlow() + } + + fun liveUser(userId: String): Flow> { + return session.getUserLive(userId).asFlow() + .startWith(session.coroutineDispatchers.io) { + session.getUser(userId).toOptional() + } + } + + fun liveRoomMember(userId: String, roomId: String): Flow> { + return session.getRoomMemberLive(userId, roomId).asFlow() + .startWith(session.coroutineDispatchers.io) { + session.getRoomMember(userId, roomId).toOptional() + } + } + + fun liveUsers(): Flow> { + return session.getUsersLive().asFlow() + } + + fun liveIgnoredUsers(): Flow> { + return session.getIgnoredUsersLive().asFlow() + } + + fun livePagedUsers(filter: String? = null, excludedUserIds: Set? = null): Flow> { + return session.getPagedUsersLive(filter, excludedUserIds).asFlow() + } + + fun liveThreePIds(refreshData: Boolean): Flow> { + return session.getThreePidsLive(refreshData).asFlow() + .startWith(session.coroutineDispatchers.io) { session.getThreePids() } + } + + fun livePendingThreePIds(): Flow> { + return session.getPendingThreePidsLive().asFlow() + .startWith(session.coroutineDispatchers.io) { session.getPendingThreePids() } + } + + fun liveUserCryptoDevices(userId: String): Flow> { + return session.cryptoService().getLiveCryptoDeviceInfo(userId).asFlow() + .startWith(session.coroutineDispatchers.io) { + session.cryptoService().getCryptoDeviceInfo(userId) + } + } + + fun liveCrossSigningInfo(userId: String): Flow> { + return session.cryptoService().crossSigningService().getLiveCrossSigningKeys(userId).asFlow() + .startWith(session.coroutineDispatchers.io) { + session.cryptoService().crossSigningService().getUserCrossSigningKeys(userId).toOptional() + } + } + + fun liveCrossSigningPrivateKeys(): Flow> { + return session.cryptoService().crossSigningService().getLiveCrossSigningPrivateKeys().asFlow() + .startWith(session.coroutineDispatchers.io) { + session.cryptoService().crossSigningService().getCrossSigningPrivateKeys().toOptional() + } + } + + fun liveUserAccountData(types: Set): Flow> { + return session.accountDataService().getLiveUserAccountDataEvents(types).asFlow() + .startWith(session.coroutineDispatchers.io) { + session.accountDataService().getUserAccountDataEvents(types) + } + } + + fun liveRoomAccountData(types: Set): Flow> { + return session.accountDataService().getLiveRoomAccountDataEvents(types).asFlow() + .startWith(session.coroutineDispatchers.io) { + session.accountDataService().getRoomAccountDataEvents(types) + } + } + + fun liveRoomWidgets( + roomId: String, + widgetId: QueryStringValue, + widgetTypes: Set? = null, + excludedTypes: Set? = null + ): Flow> { + return session.widgetService().getRoomWidgetsLive(roomId, widgetId, widgetTypes, excludedTypes).asFlow() + .startWith(session.coroutineDispatchers.io) { + session.widgetService().getRoomWidgets(roomId, widgetId, widgetTypes, excludedTypes) + } + } + + fun liveRoomChangeMembershipState(): Flow> { + return session.getChangeMembershipsLive().asFlow() + } +} + +fun Session.flow(): FlowSession { + return FlowSession(this) +} diff --git a/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/OptionalFlow.kt b/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/OptionalFlow.kt new file mode 100644 index 0000000000..a9f062f379 --- /dev/null +++ b/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/OptionalFlow.kt @@ -0,0 +1,32 @@ +/* + * 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 org.matrix.android.sdk.flow + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.map +import org.matrix.android.sdk.api.util.Optional + +fun Flow>.unwrap(): Flow { + return filter { it.hasValue() }.map { it.get() } +} + +fun Flow>.mapOptional(fn: (T) -> U?): Flow> { + return map { + it.map(fn) + } +} diff --git a/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/LiveDataObservable.kt b/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/LiveDataObservable.kt index 2174c6f118..56b52facf9 100644 --- a/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/LiveDataObservable.kt +++ b/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/LiveDataObservable.kt @@ -34,8 +34,8 @@ private class LiveDataObservable( liveData.observeForever(relay) } - private inner class RemoveObserverInMainThread(private val observer: io.reactivex.Observer) - : MainThreadDisposable(), Observer { + private inner class RemoveObserverInMainThread(private val observer: io.reactivex.Observer) : + MainThreadDisposable(), Observer { override fun onChanged(t: T?) { if (!isDisposed) { diff --git a/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/OptionalRx.kt b/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/OptionalRx.kt index ff4b0d755c..936bd824e7 100644 --- a/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/OptionalRx.kt +++ b/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/OptionalRx.kt @@ -16,8 +16,8 @@ package org.matrix.android.sdk.rx -import org.matrix.android.sdk.api.util.Optional import io.reactivex.Observable +import org.matrix.android.sdk.api.util.Optional fun Observable>.unwrap(): Observable { return filter { it.hasValue() }.map { it.get() } diff --git a/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxSession.kt b/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxSession.kt index 58fb760ff5..47203816b4 100644 --- a/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxSession.kt +++ b/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxSession.kt @@ -32,6 +32,7 @@ import org.matrix.android.sdk.api.session.crypto.crosssigning.SELF_SIGNING_KEY_S import org.matrix.android.sdk.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME import org.matrix.android.sdk.api.session.group.GroupSummaryQueryParams import org.matrix.android.sdk.api.session.group.model.GroupSummary +import org.matrix.android.sdk.api.session.identity.FoundThreePid import org.matrix.android.sdk.api.session.identity.ThreePid import org.matrix.android.sdk.api.session.pushers.Pusher import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams @@ -239,6 +240,10 @@ class RxSession(private val session: Session) { ) .distinctUntilChanged() } + + fun lookupThreePid(threePid: ThreePid): Single> = rxSingle { + session.identityService().lookUp(listOf(threePid)).firstOrNull().toOptional() + } } fun Session.rx(): RxSession { diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index 824d51d589..deea87e8a3 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -31,7 +31,7 @@ android { // that the app's state is completely cleared between tests. testInstrumentationRunnerArguments clearPackageData: 'true' - buildConfigField "String", "SDK_VERSION", "\"1.2.2\"" + buildConfigField "String", "SDK_VERSION", "\"1.3.7\"" buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\"" resValue "string", "git_sdk_revision", "\"${gitRevision()}\"" @@ -100,8 +100,6 @@ static def gitRevisionDate() { } dependencies { - - implementation libs.jetbrains.kotlinStdlibJdk7 implementation libs.jetbrains.coroutinesCore implementation libs.jetbrains.coroutinesAndroid @@ -115,7 +113,7 @@ dependencies { implementation libs.squareup.retrofit implementation libs.squareup.retrofitMoshi - implementation(platform("com.squareup.okhttp3:okhttp-bom:4.9.1")) + implementation(platform("com.squareup.okhttp3:okhttp-bom:4.9.2")) implementation 'com.squareup.okhttp3:okhttp' implementation 'com.squareup.okhttp3:logging-interceptor' implementation 'com.squareup.okhttp3:okhttp-urlconnection' @@ -130,6 +128,7 @@ dependencies { // Database implementation 'com.github.Zhuinden:realm-monarchy:0.7.1' + kapt 'dk.ilios:realmfieldnameshelper:2.0.0' // Work @@ -151,10 +150,13 @@ dependencies { implementation 'com.facebook.stetho:stetho-okhttp3:1.6.0' // Video compression - implementation 'com.otaliastudios:transcoder:0.10.3' + implementation 'com.otaliastudios:transcoder:0.10.4' + + // Exif data handling + implementation libs.apache.commonsImaging // Phone number https://github.com/google/libphonenumber - implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.32' + implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.36' testImplementation libs.tests.junit testImplementation 'org.robolectric:robolectric:4.6.1' @@ -165,6 +167,8 @@ dependencies { implementation libs.jetbrains.coroutinesAndroid // Plant Timber tree for test testImplementation 'net.lachlanmckee:timber-junit-rule:1.0.1' + // Transitively required for mocking realm as monarchy doesn't expose Rx + testImplementation libs.rx.rxKotlin kaptAndroidTest libs.dagger.daggerCompiler androidTestImplementation libs.androidx.testCore @@ -179,5 +183,5 @@ dependencies { // Plant Timber tree for test androidTestImplementation libs.tests.timberJunitRule - androidTestUtil 'androidx.test:orchestrator:1.4.0' + androidTestUtil libs.androidx.orchestrator } diff --git a/matrix-sdk-android/src/androidTest/AndroidManifest.xml b/matrix-sdk-android/src/androidTest/AndroidManifest.xml index 274bd8c87b..40360fcd19 100644 --- a/matrix-sdk-android/src/androidTest/AndroidManifest.xml +++ b/matrix-sdk-android/src/androidTest/AndroidManifest.xml @@ -8,10 +8,15 @@ This is mandatory to run integration tests --> + tools:node="merge"> + + diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/InstrumentedTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/InstrumentedTest.kt index 583406346e..a763766821 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/InstrumentedTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/InstrumentedTest.kt @@ -18,8 +18,8 @@ package org.matrix.android.sdk import android.content.Context import androidx.test.core.app.ApplicationProvider -import org.matrix.android.sdk.test.shared.createTimberTestRule import org.junit.Rule +import org.matrix.android.sdk.test.shared.createTimberTestRule interface InstrumentedTest { diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/SingleThreadCoroutineDispatcher.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/SingleThreadCoroutineDispatcher.kt index 9942ea9db3..3e3af10799 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/SingleThreadCoroutineDispatcher.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/SingleThreadCoroutineDispatcher.kt @@ -16,9 +16,9 @@ package org.matrix.android.sdk -import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.asCoroutineDispatcher +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import java.util.concurrent.Executors internal val testCoroutineDispatchers = MatrixCoroutineDispatchers(Main, Main, Main, Main, diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/AccountCreationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/AccountCreationTest.kt index 5dede9dcfd..e0451bea38 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/AccountCreationTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/AccountCreationTest.kt @@ -16,16 +16,16 @@ package org.matrix.android.sdk.account -import org.matrix.android.sdk.InstrumentedTest -import org.matrix.android.sdk.common.CommonTestHelper -import org.matrix.android.sdk.common.CryptoTestHelper -import org.matrix.android.sdk.common.SessionTestParams -import org.matrix.android.sdk.common.TestConstants import org.junit.FixMethodOrder import org.junit.Test 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.common.CommonTestHelper +import org.matrix.android.sdk.common.CryptoTestHelper +import org.matrix.android.sdk.common.SessionTestParams +import org.matrix.android.sdk.common.TestConstants @RunWith(JUnit4::class) @FixMethodOrder(MethodSorters.JVM) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/ChangePasswordTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/ChangePasswordTest.kt index 103b638c39..d32bcb3fe5 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/ChangePasswordTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/ChangePasswordTest.kt @@ -16,17 +16,17 @@ package org.matrix.android.sdk.account -import org.matrix.android.sdk.InstrumentedTest -import org.matrix.android.sdk.api.failure.isInvalidPassword -import org.matrix.android.sdk.common.CommonTestHelper -import org.matrix.android.sdk.common.SessionTestParams -import org.matrix.android.sdk.common.TestConstants import org.amshove.kluent.shouldBeTrue import org.junit.FixMethodOrder import org.junit.Test 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.failure.isInvalidPassword +import org.matrix.android.sdk.common.CommonTestHelper +import org.matrix.android.sdk.common.SessionTestParams +import org.matrix.android.sdk.common.TestConstants @RunWith(JUnit4::class) @FixMethodOrder(MethodSorters.JVM) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/DeactivateAccountTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/DeactivateAccountTest.kt index 01c4f8ccb3..f8d108fb73 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/DeactivateAccountTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/DeactivateAccountTest.kt @@ -67,9 +67,9 @@ class DeactivateAccountTest : InstrumentedTest { val throwable = commonTestHelper.logAccountWithError(session.myUserId, TestConstants.PASSWORD) // Test the error - assertTrue(throwable is Failure.ServerError - && throwable.error.code == MatrixError.M_USER_DEACTIVATED - && throwable.error.message == "This account has been deactivated") + assertTrue(throwable is Failure.ServerError && + throwable.error.code == MatrixError.M_USER_DEACTIVATED && + throwable.error.message == "This account has been deactivated") // Try to create an account with the deactivate account user id, it will fail (M_USER_IN_USE) val hs = commonTestHelper.createHomeServerConfig() @@ -95,8 +95,8 @@ class DeactivateAccountTest : InstrumentedTest { // Test the error accountCreationError.let { - assertTrue(it is Failure.ServerError - && it.error.code == MatrixError.M_USER_IN_USE) + assertTrue(it is Failure.ServerError && + it.error.code == MatrixError.M_USER_IN_USE) } // No need to close the session, it has been deactivated diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/MockOkHttpInterceptor.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/MockOkHttpInterceptor.kt index e7978a9cb2..b6d833a77c 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/MockOkHttpInterceptor.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/MockOkHttpInterceptor.kt @@ -15,12 +15,12 @@ */ package org.matrix.android.sdk.common -import org.matrix.android.sdk.internal.session.TestInterceptor import okhttp3.Interceptor import okhttp3.Protocol import okhttp3.Request import okhttp3.Response import okhttp3.ResponseBody.Companion.toResponseBody +import org.matrix.android.sdk.internal.session.TestInterceptor import javax.net.ssl.HttpsURLConnection /** diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestMatrixCallback.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestMatrixCallback.kt index c2e1ec0f92..9f6d6eb136 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestMatrixCallback.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestMatrixCallback.kt @@ -17,8 +17,8 @@ package org.matrix.android.sdk.common import androidx.annotation.CallSuper -import org.matrix.android.sdk.api.MatrixCallback import org.junit.Assert.fail +import org.matrix.android.sdk.api.MatrixCallback import timber.log.Timber import java.util.concurrent.CountDownLatch diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/AttachmentEncryptionTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/AttachmentEncryptionTest.kt index b6cb7f9e79..aaf779212b 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/AttachmentEncryptionTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/AttachmentEncryptionTest.kt @@ -26,9 +26,9 @@ import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.MethodSorters import org.matrix.android.sdk.internal.crypto.attachments.MXEncryptedAttachments +import org.matrix.android.sdk.internal.crypto.attachments.toElementToDecrypt import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileKey -import org.matrix.android.sdk.internal.crypto.attachments.toElementToDecrypt import java.io.ByteArrayOutputStream import java.io.InputStream diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/CryptoStoreHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/CryptoStoreHelper.kt index 75ccce0db9..c717c8e33f 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/CryptoStoreHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/CryptoStoreHelper.kt @@ -16,12 +16,12 @@ package org.matrix.android.sdk.internal.crypto +import io.realm.RealmConfiguration import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStore import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStoreModule import org.matrix.android.sdk.internal.crypto.store.db.mapper.CrossSigningKeysMapper import org.matrix.android.sdk.internal.di.MoshiProvider -import io.realm.RealmConfiguration import kotlin.random.Random internal class CryptoStoreHelper { diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/CryptoStoreTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/CryptoStoreTest.kt index 1d838b5c84..f43c425cc9 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/CryptoStoreTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/CryptoStoreTest.kt @@ -17,9 +17,6 @@ package org.matrix.android.sdk.internal.crypto import androidx.test.ext.junit.runners.AndroidJUnit4 -import org.matrix.android.sdk.InstrumentedTest -import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import io.realm.Realm import org.junit.Assert.assertEquals import org.junit.Assert.assertNotEquals @@ -27,6 +24,9 @@ import org.junit.Assert.assertNull import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.matrix.android.sdk.InstrumentedTest +import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.olm.OlmAccount import org.matrix.olm.OlmManager import org.matrix.olm.OlmSession diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/PreShareKeysTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/PreShareKeysTest.kt index a2566c1414..825fba570a 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/PreShareKeysTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/PreShareKeysTest.kt @@ -50,8 +50,8 @@ class PreShareKeysTest : InstrumentedTest { aliceSession.cryptoService().discardOutboundSession(e2eRoomID) val preShareCount = bobSession.cryptoService().getGossipingEvents().count { - it.senderId == aliceSession.myUserId - && it.getClearType() == EventType.ROOM_KEY + it.senderId == aliceSession.myUserId && + it.getClearType() == EventType.ROOM_KEY } assertEquals("Bob should not have receive any key from alice at this point", 0, preShareCount) @@ -65,16 +65,16 @@ class PreShareKeysTest : InstrumentedTest { mTestHelper.waitWithLatch { latch -> mTestHelper.retryPeriodicallyWithLatch(latch) { val newGossipCount = bobSession.cryptoService().getGossipingEvents().count { - it.senderId == aliceSession.myUserId - && it.getClearType() == EventType.ROOM_KEY + it.senderId == aliceSession.myUserId && + it.getClearType() == EventType.ROOM_KEY } newGossipCount > preShareCount } } val latest = bobSession.cryptoService().getGossipingEvents().lastOrNull { - it.senderId == aliceSession.myUserId - && it.getClearType() == EventType.ROOM_KEY + it.senderId == aliceSession.myUserId && + it.getClearType() == EventType.ROOM_KEY } val content = latest?.getClearContent().toModel() diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt index b2516ea2be..c939952dc9 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt @@ -18,6 +18,11 @@ package org.matrix.android.sdk.internal.crypto.gossiping import android.util.Log import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Assert +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters import org.matrix.android.sdk.InstrumentedTest import org.matrix.android.sdk.api.NoOpMatrixCallback import org.matrix.android.sdk.api.extensions.tryOrNull @@ -31,11 +36,6 @@ import org.matrix.android.sdk.common.SessionTestParams import org.matrix.android.sdk.common.TestConstants import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent import org.matrix.android.sdk.internal.crypto.model.event.WithHeldCode -import org.junit.Assert -import org.junit.FixMethodOrder -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters @RunWith(AndroidJUnit4::class) @FixMethodOrder(MethodSorters.JVM) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupPasswordTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupPasswordTest.kt index cc71f88fc0..63e74603d0 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupPasswordTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupPasswordTest.kt @@ -17,9 +17,6 @@ package org.matrix.android.sdk.internal.crypto.keysbackup import androidx.test.ext.junit.runners.AndroidJUnit4 -import org.matrix.android.sdk.InstrumentedTest -import org.matrix.android.sdk.api.listeners.ProgressListener -import org.matrix.android.sdk.common.assertByteArrayNotEqual import org.junit.Assert.assertArrayEquals import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue @@ -28,6 +25,9 @@ import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.MethodSorters +import org.matrix.android.sdk.InstrumentedTest +import org.matrix.android.sdk.api.listeners.ProgressListener +import org.matrix.android.sdk.common.assertByteArrayNotEqual import org.matrix.olm.OlmManager import org.matrix.olm.OlmPkDecryption diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt index 89d297c592..0785dba8b9 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt @@ -17,6 +17,16 @@ package org.matrix.android.sdk.internal.crypto.keysbackup import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Assert.fail +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters import org.matrix.android.sdk.InstrumentedTest import org.matrix.android.sdk.api.listeners.ProgressListener import org.matrix.android.sdk.api.listeners.StepProgressListener @@ -33,16 +43,6 @@ import org.matrix.android.sdk.internal.crypto.keysbackup.model.MegolmBackupCreat import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersion import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersionResult import org.matrix.android.sdk.internal.crypto.model.ImportRoomKeysResult -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertNotNull -import org.junit.Assert.assertNull -import org.junit.Assert.assertTrue -import org.junit.Assert.fail -import org.junit.FixMethodOrder -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters import java.util.ArrayList import java.util.Collections import java.util.concurrent.CountDownLatch diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTestHelper.kt index b6e5ae7364..a625ffc0e9 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTestHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTestHelper.kt @@ -16,6 +16,7 @@ package org.matrix.android.sdk.internal.crypto.keysbackup +import org.junit.Assert import org.matrix.android.sdk.api.listeners.ProgressListener import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupService @@ -28,7 +29,6 @@ import org.matrix.android.sdk.common.assertListEquals import org.matrix.android.sdk.internal.crypto.MegolmSessionData import org.matrix.android.sdk.internal.crypto.keysbackup.model.MegolmBackupCreationInfo import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersion -import org.junit.Assert import java.util.concurrent.CountDownLatch class KeysBackupTestHelper( diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/StateObserver.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/StateObserver.kt index ff8ce43b55..80e54d82ec 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/StateObserver.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/StateObserver.kt @@ -16,11 +16,11 @@ package org.matrix.android.sdk.internal.crypto.keysbackup +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupService import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupState import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupStateListener -import org.junit.Assert.assertEquals -import org.junit.Assert.assertNull import java.util.concurrent.CountDownLatch /** @@ -91,8 +91,8 @@ internal class StateObserver(private val keysBackup: KeysBackupService, stateList.add(newState) // Check that state transition is valid - if (stateList.size >= 2 - && !allowedStateTransitions.contains(stateList[stateList.size - 2] to newState)) { + if (stateList.size >= 2 && + !allowedStateTransitions.contains(stateList[stateList.size - 2] to newState)) { // Forbidden transition detected lastTransitionError = "Forbidden transition detected from " + stateList[stateList.size - 2] + " to " + newState } 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 74855b8630..b343d7334a 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 @@ -18,8 +18,20 @@ package org.matrix.android.sdk.internal.crypto.ssss import androidx.lifecycle.Observer import androidx.test.ext.junit.runners.AndroidJUnit4 +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters import org.matrix.android.sdk.InstrumentedTest import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent import org.matrix.android.sdk.api.session.securestorage.EncryptedSecretContent import org.matrix.android.sdk.api.session.securestorage.KeySigner import org.matrix.android.sdk.api.session.securestorage.RawBytesKeySpec @@ -33,18 +45,6 @@ import org.matrix.android.sdk.common.TestConstants import org.matrix.android.sdk.internal.crypto.SSSS_ALGORITHM_AES_HMAC_SHA2 import org.matrix.android.sdk.internal.crypto.crosssigning.toBase64NoPadding import org.matrix.android.sdk.internal.crypto.secrets.DefaultSharedSecretStorageService -import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking -import org.junit.Assert.assertEquals -import org.junit.Assert.assertNotNull -import org.junit.Assert.assertNull -import org.junit.FixMethodOrder -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters import java.util.concurrent.CountDownLatch @RunWith(AndroidJUnit4::class) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/SASTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/SASTest.kt index 4ea8cdc074..e0d49b3f5e 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/SASTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/SASTest.kt @@ -18,6 +18,16 @@ package org.matrix.android.sdk.internal.crypto.verification import android.util.Log import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Assert.fail +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters import org.matrix.android.sdk.InstrumentedTest import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.crypto.verification.CancelCode @@ -38,16 +48,6 @@ import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationCancel import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationStart import org.matrix.android.sdk.internal.crypto.model.rest.toValue -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertNotNull -import org.junit.Assert.assertNull -import org.junit.Assert.assertTrue -import org.junit.Assert.fail -import org.junit.FixMethodOrder -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters import java.util.concurrent.CountDownLatch @RunWith(AndroidJUnit4::class) @@ -551,7 +551,7 @@ class SASTest : InstrumentedTest { cryptoTestData.roomId ) - var requestID : String? = null + var requestID: String? = null mTestHelper.waitWithLatch { mTestHelper.retryPeriodicallyWithLatch(it) { diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/SharedSecretTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/SharedSecretTest.kt index 97b93dcf5a..9b10f9e9af 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/SharedSecretTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/SharedSecretTest.kt @@ -17,13 +17,13 @@ package org.matrix.android.sdk.internal.crypto.verification.qrcode import androidx.test.ext.junit.runners.AndroidJUnit4 -import org.matrix.android.sdk.InstrumentedTest import org.amshove.kluent.shouldBe import org.amshove.kluent.shouldNotBeEqualTo import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.MethodSorters +import org.matrix.android.sdk.InstrumentedTest @RunWith(AndroidJUnit4::class) @FixMethodOrder(MethodSorters.JVM) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParserTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParserTest.kt index 94303dda08..1ed2f89977 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParserTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParserTest.kt @@ -25,6 +25,9 @@ import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.MethodSorters import org.matrix.android.sdk.InstrumentedTest +import org.matrix.android.sdk.api.MatrixConfiguration +import org.matrix.android.sdk.common.TestRoomDisplayNameFallbackProvider +import org.matrix.android.sdk.internal.session.displayname.DisplayNameResolver import org.matrix.android.sdk.internal.session.room.send.pills.MentionLinkSpecComparator import org.matrix.android.sdk.internal.session.room.send.pills.TextPillsUtils @@ -48,7 +51,14 @@ class MarkdownParserTest : InstrumentedTest { private val markdownParser = MarkdownParser( Parser.builder().build(), HtmlRenderer.builder().softbreak("
").build(), - TextPillsUtils(MentionLinkSpecComparator()) + TextPillsUtils( + MentionLinkSpecComparator(), + DisplayNameResolver( + MatrixConfiguration( + applicationFlavor = "TestFlavor", + roomDisplayNameFallbackProvider = TestRoomDisplayNameFallbackProvider() + ) + )) ) @Test diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/util/JsonCanonicalizerTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/util/JsonCanonicalizerTest.kt index b5ab6589ff..d38afc6b62 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/util/JsonCanonicalizerTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/util/JsonCanonicalizerTest.kt @@ -17,10 +17,10 @@ package org.matrix.android.sdk.internal.util import androidx.test.ext.junit.runners.AndroidJUnit4 -import org.matrix.android.sdk.InstrumentedTest import org.junit.Assert.assertEquals import org.junit.Test import org.junit.runner.RunWith +import org.matrix.android.sdk.InstrumentedTest @RunWith(AndroidJUnit4::class) internal class JsonCanonicalizerTest : InstrumentedTest { diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineBackToPreviousLastForwardTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineBackToPreviousLastForwardTest.kt index 3774e6f513..7628f287c9 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineBackToPreviousLastForwardTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineBackToPreviousLastForwardTest.kt @@ -16,6 +16,14 @@ package org.matrix.android.sdk.session.room.timeline +import org.amshove.kluent.shouldBeFalse +import org.amshove.kluent.shouldBeTrue +import org.junit.Assert.assertTrue +import org.junit.FixMethodOrder +import org.junit.Test +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.extensions.orFalse import org.matrix.android.sdk.api.session.events.model.EventType @@ -26,14 +34,6 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings import org.matrix.android.sdk.common.CommonTestHelper import org.matrix.android.sdk.common.CryptoTestHelper import org.matrix.android.sdk.common.checkSendOrder -import org.amshove.kluent.shouldBeFalse -import org.amshove.kluent.shouldBeTrue -import org.junit.Assert.assertTrue -import org.junit.FixMethodOrder -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.JUnit4 -import org.junit.runners.MethodSorters import timber.log.Timber import java.util.concurrent.CountDownLatch @@ -111,8 +111,8 @@ class TimelineBackToPreviousLastForwardTest : InstrumentedTest { } // Ok, we have the 10 last messages from Alice. - snapshot.size == 10 - && snapshot.all { it.root.content.toModel()?.body?.startsWith(messageRoot).orFalse() } + snapshot.size == 10 && + snapshot.all { it.root.content.toModel()?.body?.startsWith(messageRoot).orFalse() } } bobTimeline.addListener(eventsListener) @@ -160,10 +160,10 @@ class TimelineBackToPreviousLastForwardTest : InstrumentedTest { } // Bob can see the first event of the room (so Back pagination has worked) - snapshot.lastOrNull()?.root?.getClearType() == EventType.STATE_ROOM_CREATE + snapshot.lastOrNull()?.root?.getClearType() == EventType.STATE_ROOM_CREATE && // 8 for room creation item, and 30 for the forward pagination - && snapshot.size == 38 - && snapshot.checkSendOrder(messageRoot, 30, 0) + snapshot.size == 38 && + snapshot.checkSendOrder(messageRoot, 30, 0) } bobTimeline.addListener(eventsListener) 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 0fe341cad6..dfa6ec10ae 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 @@ -16,6 +16,13 @@ package org.matrix.android.sdk.session.room.timeline +import org.amshove.kluent.shouldBeFalse +import org.amshove.kluent.shouldBeTrue +import org.junit.FixMethodOrder +import org.junit.Test +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.extensions.orFalse import org.matrix.android.sdk.api.session.events.model.EventType @@ -26,13 +33,6 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings import org.matrix.android.sdk.common.CommonTestHelper import org.matrix.android.sdk.common.CryptoTestHelper import org.matrix.android.sdk.common.checkSendOrder -import org.amshove.kluent.shouldBeFalse -import org.amshove.kluent.shouldBeTrue -import org.junit.FixMethodOrder -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.JUnit4 -import org.junit.runners.MethodSorters import timber.log.Timber import java.util.concurrent.CountDownLatch @@ -86,8 +86,8 @@ class TimelineForwardPaginationTest : InstrumentedTest { } // Ok, we have the 10 last messages of the initial sync - snapshot.size == 10 - && snapshot.all { it.root.content.toModel()?.body?.startsWith(message).orFalse() } + snapshot.size == 10 && + snapshot.all { it.root.content.toModel()?.body?.startsWith(message).orFalse() } } // Open the timeline at last sent message @@ -110,8 +110,8 @@ class TimelineForwardPaginationTest : InstrumentedTest { } // The event is not in db, so it is fetch alone - snapshot.size == 1 - && snapshot.all { it.root.content.toModel()?.body?.startsWith("Message from Alice").orFalse() } + snapshot.size == 1 && + snapshot.all { it.root.content.toModel()?.body?.startsWith("Message from Alice").orFalse() } } aliceTimeline.addListener(aliceEventsListener) @@ -137,9 +137,9 @@ 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 + 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 == 57 // 6 + 1 + 50 + snapshot.size == 57 // 6 + 1 + 50 } aliceTimeline.addListener(aliceEventsListener) @@ -166,8 +166,8 @@ class TimelineForwardPaginationTest : InstrumentedTest { Timber.w(" event ${it.root.content}") } // 6 for room creation item (backward pagination),and numberOfMessagesToSend (all the message of the room) - snapshot.size == 6 + numberOfMessagesToSend - && snapshot.checkSendOrder(message, numberOfMessagesToSend, 0) + snapshot.size == 6 + numberOfMessagesToSend && + snapshot.checkSendOrder(message, numberOfMessagesToSend, 0) } 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 03a4d41988..e865fe17da 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 @@ -16,6 +16,13 @@ package org.matrix.android.sdk.session.room.timeline +import org.amshove.kluent.shouldBeFalse +import org.amshove.kluent.shouldBeTrue +import org.junit.FixMethodOrder +import org.junit.Test +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.extensions.orFalse import org.matrix.android.sdk.api.session.events.model.EventType @@ -26,13 +33,6 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings import org.matrix.android.sdk.common.CommonTestHelper import org.matrix.android.sdk.common.CryptoTestHelper import org.matrix.android.sdk.common.checkSendOrder -import org.amshove.kluent.shouldBeFalse -import org.amshove.kluent.shouldBeTrue -import org.junit.FixMethodOrder -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.JUnit4 -import org.junit.runners.MethodSorters import timber.log.Timber import java.util.concurrent.CountDownLatch @@ -107,8 +107,8 @@ class TimelinePreviousLastForwardTest : InstrumentedTest { } // Ok, we have the 10 last messages from Alice. This will be our future previous lastForward chunk - snapshot.size == 10 - && snapshot.all { it.root.content.toModel()?.body?.startsWith(firstMessage).orFalse() } + snapshot.size == 10 && + snapshot.all { it.root.content.toModel()?.body?.startsWith(firstMessage).orFalse() } } bobTimeline.addListener(eventsListener) @@ -141,8 +141,8 @@ class TimelinePreviousLastForwardTest : InstrumentedTest { } // Ok, we have the 10 last messages from Alice. This will be our future previous lastForward chunk - snapshot.size == 10 - && snapshot.all { it.root.content.toModel()?.body?.startsWith(secondMessage).orFalse() } + snapshot.size == 10 && + snapshot.all { it.root.content.toModel()?.body?.startsWith(secondMessage).orFalse() } } bobTimeline.addListener(eventsListener) @@ -216,11 +216,11 @@ 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 + snapshot.lastOrNull()?.root?.getClearType() == EventType.STATE_ROOM_CREATE && // 8 for room creation item 60 message from Alice - && snapshot.size == 68 // 8 + 60 - && snapshot.checkSendOrder(secondMessage, 30, 0) - && snapshot.checkSendOrder(firstMessage, 30, 30) + snapshot.size == 68 && // 8 + 60 + snapshot.checkSendOrder(secondMessage, 30, 0) && + snapshot.checkSendOrder(firstMessage, 30, 30) } bobTimeline.addListener(eventsListener) diff --git a/matrix-sdk-android/src/debug/java/org/matrix/android/sdk/internal/network/interceptors/CurlLoggingInterceptor.kt b/matrix-sdk-android/src/debug/java/org/matrix/android/sdk/internal/network/interceptors/CurlLoggingInterceptor.kt index 2103dc954d..3add757efa 100644 --- a/matrix-sdk-android/src/debug/java/org/matrix/android/sdk/internal/network/interceptors/CurlLoggingInterceptor.kt +++ b/matrix-sdk-android/src/debug/java/org/matrix/android/sdk/internal/network/interceptors/CurlLoggingInterceptor.kt @@ -17,10 +17,10 @@ package org.matrix.android.sdk.internal.network.interceptors -import org.matrix.android.sdk.internal.di.MatrixScope import okhttp3.Interceptor import okhttp3.Response import okio.Buffer +import org.matrix.android.sdk.internal.di.MatrixScope import timber.log.Timber import java.io.IOException import java.nio.charset.Charset @@ -36,8 +36,8 @@ import javax.inject.Inject * non-production environment. */ @MatrixScope -internal class CurlLoggingInterceptor @Inject constructor() - : Interceptor { +internal class CurlLoggingInterceptor @Inject constructor() : + Interceptor { /** * Set any additional curl command options (see 'curl --help'). @@ -90,8 +90,8 @@ internal class CurlLoggingInterceptor @Inject constructor() curlCmd += ((if (compressed) " --compressed " else " ") + "'" + request.url.toString() // Replace localhost for emulator by localhost for shell - .replace("://10.0.2.2:8080/".toRegex(), "://127.0.0.1:8080/") - + "'") + .replace("://10.0.2.2:8080/".toRegex(), "://127.0.0.1:8080/") + + "'") // Add Json formatting curlCmd += " | python -m json.tool" diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt index ed809cdb04..306ed45500 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt @@ -16,6 +16,7 @@ package org.matrix.android.sdk.api +import okhttp3.ConnectionSpec import org.matrix.android.sdk.api.crypto.MXCryptoConfig import java.net.Proxy @@ -32,14 +33,30 @@ data class MatrixConfiguration( "https://scalar-staging.riot.im/scalar/api" ), /** - * Optional proxy to connect to the matrix servers - * You can create one using for instance Proxy(proxyType, InetSocketAddress.createUnresolved(hostname, port) + * Optional base url to create client permalinks (eg. https://www.example.com/#/) instead of Matrix ones (matrix.to links). + * Do not forget to add the "#" which is required by the permalink parser. + * + * Note: this field is only used for permalinks creation, you will also have to edit the string-array `permalink_supported_hosts` in the config file + * and add it to your manifest to handle these links in the application. + */ + val clientPermalinkBaseUrl: String? = null, + /** + * Optional proxy to connect to the matrix servers. + * You can create one using for instance Proxy(proxyType, InetSocketAddress.createUnresolved(hostname, port). */ val proxy: Proxy? = null, + /** + * TLS versions and cipher suites limitation for unauthenticated requests + */ + val connectionSpec: ConnectionSpec = ConnectionSpec.RESTRICTED_TLS, /** * True to advertise support for call transfers to other parties on Matrix calls. */ val supportsCallTransfer: Boolean = false, + /** + * MatrixItemDisplayNameFallbackProvider to provide default display name for MatrixItem. By default, the id will be used + */ + val matrixItemDisplayNameFallbackProvider: MatrixItemDisplayNameFallbackProvider? = null, /** * RoomDisplayNameFallbackProvider to provide default room display name. */ @@ -47,7 +64,7 @@ data class MatrixConfiguration( ) { /** - * Can be implemented by your Application class + * Can be implemented by your Application class. */ interface Provider { fun providesMatrixConfiguration(): MatrixConfiguration diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConstants.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConstants.kt new file mode 100644 index 0000000000..49520f3678 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConstants.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 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.api + +/** + * This object define some global constants regarding the Matrix specification + */ +object MatrixConstants { + /** + * Max length for an alias. Room aliases MUST NOT exceed 255 bytes (including the # sigil and the domain). + * See [maxAliasLocalPartLength] + * Ref. https://matrix.org/docs/spec/appendices#room-aliases + */ + const val ALIAS_MAX_LENGTH = 255 + + fun maxAliasLocalPartLength(domain: String): Int { + return (ALIAS_MAX_LENGTH - 1 /* # sigil */ - 1 /* ':' */ - domain.length) + .coerceAtLeast(0) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/MatrixCoroutineDispatchers.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixCoroutineDispatchers.kt similarity index 85% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/MatrixCoroutineDispatchers.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixCoroutineDispatchers.kt index b44a543c1c..592974be74 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/MatrixCoroutineDispatchers.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixCoroutineDispatchers.kt @@ -5,7 +5,7 @@ * 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 + * 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, @@ -14,11 +14,11 @@ * limitations under the License. */ -package org.matrix.android.sdk.internal.util +package org.matrix.android.sdk.api import kotlinx.coroutines.CoroutineDispatcher -internal data class MatrixCoroutineDispatchers( +data class MatrixCoroutineDispatchers( val io: CoroutineDispatcher, val computation: CoroutineDispatcher, val main: CoroutineDispatcher, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixItemDisplayNameFallbackProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixItemDisplayNameFallbackProvider.kt new file mode 100644 index 0000000000..82008cda8c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixItemDisplayNameFallbackProvider.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 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.api + +import org.matrix.android.sdk.api.util.MatrixItem + +interface MatrixItemDisplayNameFallbackProvider { + fun getDefaultName(matrixItem: MatrixItem): String +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixPatterns.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixPatterns.kt index 9a5e40ffeb..2a26b612fb 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixPatterns.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixPatterns.kt @@ -17,6 +17,8 @@ package org.matrix.android.sdk.api import org.matrix.android.sdk.BuildConfig +import org.matrix.android.sdk.internal.util.removeInvalidRoomNameChars +import org.matrix.android.sdk.internal.util.replaceSpaceChars import timber.log.Timber /** @@ -128,10 +130,10 @@ object MatrixPatterns { * @return true if the string is a valid event id. */ fun isEventId(str: String?): Boolean { - return str != null - && (str matches PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER - || str matches PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V3 - || str matches PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V4) + return str != null && + (str matches PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER || + str matches PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V3 || + str matches PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V4) } /** @@ -162,10 +164,11 @@ object MatrixPatterns { return order != null && order.length < 50 && order matches ORDER_STRING_REGEX } - fun candidateAliasFromRoomName(name: String): String { - return Regex("\\s").replace(name.lowercase(), "_").let { - "[^a-z0-9._%#@=+-]".toRegex().replace(it, "") - } + fun candidateAliasFromRoomName(roomName: String, domain: String): String { + return roomName.lowercase() + .replaceSpaceChars(replacement = "_") + .removeInvalidRoomNameChars() + .take(MatrixConstants.maxAliasLocalPartLength(domain)) } /** diff --git a/vector/src/main/java/im/vector/app/core/di/HasScreenInjector.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixUrls.kt similarity index 65% rename from vector/src/main/java/im/vector/app/core/di/HasScreenInjector.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixUrls.kt index 4618bd04d1..dc4e0f152d 100644 --- a/vector/src/main/java/im/vector/app/core/di/HasScreenInjector.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixUrls.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019 New Vector Ltd + * 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. @@ -14,9 +14,13 @@ * limitations under the License. */ -package im.vector.app.core.di +package org.matrix.android.sdk.api -interface HasScreenInjector { +/** + * This class contains pattern to match Matrix Url, aka mxc urls + */ +object MatrixUrls { + const val MATRIX_CONTENT_URI_SCHEME = "mxc://" - fun injector(): ScreenComponent + fun String.isMxcUrl() = startsWith(MATRIX_CONTENT_URI_SCHEME) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/AuthenticationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/AuthenticationService.kt index 5e35917243..9cb784c9c0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/AuthenticationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/AuthenticationService.kt @@ -105,9 +105,15 @@ interface AuthenticationService { /** * Authenticate with a matrixId and a password * Usually call this after a successful call to getWellKnownData() + * @param homeServerConnectionConfig the information about the homeserver and other configuration + * @param matrixId the matrixId of the user + * @param password the password of the account + * @param initialDeviceName the initial device name + * @param deviceId the device id, optional. If not provided or null, the server will generate one. */ suspend fun directAuthentication(homeServerConnectionConfig: HomeServerConnectionConfig, matrixId: String, password: String, - initialDeviceName: String): Session + initialDeviceName: String, + deviceId: String? = null): Session } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/UIABaseAuth.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/UIABaseAuth.kt index d5e323e457..3a8e6b1084 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/UIABaseAuth.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/UIABaseAuth.kt @@ -27,5 +27,5 @@ interface UIABaseAuth { fun copyWithSession(session: String): UIABaseAuth - fun asMap() : Map + fun asMap(): Map } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/HomeServerConnectionConfig.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/HomeServerConnectionConfig.kt index 215f0a0351..e87824d6a5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/HomeServerConnectionConfig.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/HomeServerConnectionConfig.kt @@ -19,6 +19,7 @@ package org.matrix.android.sdk.api.auth.data import android.net.Uri import com.squareup.moshi.JsonClass import okhttp3.CipherSuite +import okhttp3.ConnectionSpec import okhttp3.TlsVersion import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig.Builder import org.matrix.android.sdk.internal.network.ssl.Fingerprint @@ -191,32 +192,25 @@ data class HomeServerConnectionConfig( /** * Convenient method to limit the TLS versions and cipher suites for this Builder * Ref: - * - https://www.ssi.gouv.fr/uploads/2017/02/security-recommendations-for-tls_v1.1.pdf + * - https://www.ssi.gouv.fr/uploads/2017/07/anssi-guide-recommandations_de_securite_relatives_a_tls-v1.2.pdf * - https://developer.android.com/reference/javax/net/ssl/SSLEngine * * @param tlsLimitations true to use Tls limitations * @param enableCompatibilityMode set to true for Android < 20 * @return this builder */ + @Deprecated("TLS versions and cipher suites are limited by default") fun withTlsLimitations(tlsLimitations: Boolean, enableCompatibilityMode: Boolean): Builder { if (tlsLimitations) { withShouldAcceptTlsExtensions(false) - // Tls versions - addAcceptedTlsVersion(TlsVersion.TLS_1_2) - addAcceptedTlsVersion(TlsVersion.TLS_1_3) + // TlS versions + ConnectionSpec.RESTRICTED_TLS.tlsVersions?.let { this.tlsVersions.addAll(it) } forceUsageOfTlsVersions(enableCompatibilityMode) // Cipher suites - addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256) - addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256) - addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256) - addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256) - addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384) - addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) - addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256) - addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256) + ConnectionSpec.RESTRICTED_TLS.cipherSuites?.let { this.tlsCipherSuites.addAll(it) } if (enableCompatibilityMode) { // Adopt some preceding cipher suites for Android < 20 to be able to negotiate diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/login/LoginWizard.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/login/LoginWizard.kt index a2a9373837..247d58ce79 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/login/LoginWizard.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/login/LoginWizard.kt @@ -17,6 +17,7 @@ package org.matrix.android.sdk.api.auth.login import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.util.JsonDict /** * Set of methods to be able to login to an existing account on a homeserver. @@ -34,12 +35,14 @@ interface LoginWizard { * * @param login the login field. Can be a user name, or a msisdn (email or phone number) associated to the account * @param password the password of the account - * @param deviceName the initial device name + * @param initialDeviceName the initial device name + * @param deviceId the device id, optional. If not provided or null, the server will generate one. * @return a [Session] if the login is successful */ suspend fun login(login: String, password: String, - deviceName: String): Session + initialDeviceName: String, + deviceId: String? = null): Session /** * Exchange a login token to an access token. @@ -49,6 +52,12 @@ interface LoginWizard { */ suspend fun loginWithToken(loginToken: String): Session + /** + * Login to the homeserver by sending a custom JsonDict. + * The data should contain at least one entry "type" with a String value. + */ + suspend fun loginCustom(data: JsonDict): Session + /** * Ask the homeserver to reset the user password. The password will not be reset until * [resetPasswordMailConfirmed] is successfully called. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt index 3149a0218b..b2035bb2eb 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt @@ -23,20 +23,20 @@ import java.io.IOException import javax.net.ssl.HttpsURLConnection fun Throwable.is401() = - this is Failure.ServerError - && httpCode == HttpsURLConnection.HTTP_UNAUTHORIZED /* 401 */ - && error.code == MatrixError.M_UNAUTHORIZED + this is Failure.ServerError && + httpCode == HttpsURLConnection.HTTP_UNAUTHORIZED && /* 401 */ + error.code == MatrixError.M_UNAUTHORIZED fun Throwable.isTokenError() = - this is Failure.ServerError - && (error.code == MatrixError.M_UNKNOWN_TOKEN - || error.code == MatrixError.M_MISSING_TOKEN - || error.code == MatrixError.ORG_MATRIX_EXPIRED_ACCOUNT) + this is Failure.ServerError && + (error.code == MatrixError.M_UNKNOWN_TOKEN || + error.code == MatrixError.M_MISSING_TOKEN || + error.code == MatrixError.ORG_MATRIX_EXPIRED_ACCOUNT) fun Throwable.shouldBeRetried(): Boolean { - return this is Failure.NetworkConnection - || this is IOException - || (this is Failure.ServerError && error.code == MatrixError.M_LIMIT_EXCEEDED) + return this is Failure.NetworkConnection || + this is IOException || + (this is Failure.ServerError && error.code == MatrixError.M_LIMIT_EXCEEDED) } /** @@ -52,31 +52,31 @@ fun Throwable.getRetryDelay(defaultValue: Long): Long { } fun Throwable.isInvalidPassword(): Boolean { - return this is Failure.ServerError - && error.code == MatrixError.M_FORBIDDEN - && error.message == "Invalid password" + return this is Failure.ServerError && + error.code == MatrixError.M_FORBIDDEN && + error.message == "Invalid password" } fun Throwable.isInvalidUIAAuth(): Boolean { - return this is Failure.ServerError - && error.code == MatrixError.M_FORBIDDEN - && error.flows != null + return this is Failure.ServerError && + error.code == MatrixError.M_FORBIDDEN && + error.flows != null } /** * Try to convert to a RegistrationFlowResponse. Return null in the cases it's not possible */ fun Throwable.toRegistrationFlowResponse(): RegistrationFlowResponse? { - return if (this is Failure.OtherServerError - && httpCode == HttpsURLConnection.HTTP_UNAUTHORIZED /* 401 */) { + return if (this is Failure.OtherServerError && + httpCode == HttpsURLConnection.HTTP_UNAUTHORIZED /* 401 */) { tryOrNull { MoshiProvider.providesMoshi() .adapter(RegistrationFlowResponse::class.java) .fromJson(errorBody) } - } else if (this is Failure.ServerError - && httpCode == HttpsURLConnection.HTTP_UNAUTHORIZED /* 401 */ - && error.code == MatrixError.M_FORBIDDEN) { + } else if (this is Failure.ServerError && + httpCode == HttpsURLConnection.HTTP_UNAUTHORIZED && /* 401 */ + error.code == MatrixError.M_FORBIDDEN) { // This happens when the submission for this stage was bad (like bad password) if (error.session != null && error.flows != null) { RegistrationFlowResponse( @@ -94,9 +94,9 @@ fun Throwable.toRegistrationFlowResponse(): RegistrationFlowResponse? { } fun Throwable.isRegistrationAvailabilityError(): Boolean { - return this is Failure.ServerError - && httpCode == HttpsURLConnection.HTTP_BAD_REQUEST /* 400 */ - && (error.code == MatrixError.M_USER_IN_USE - || error.code == MatrixError.M_INVALID_USERNAME - || error.code == MatrixError.M_EXCLUSIVE) + return this is Failure.ServerError && + httpCode == HttpsURLConnection.HTTP_BAD_REQUEST && /* 400 */ + (error.code == MatrixError.M_USER_IN_USE || + error.code == MatrixError.M_INVALID_USERNAME || + error.code == MatrixError.M_EXCLUSIVE) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/logger/LoggerTag.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/logger/LoggerTag.kt index 51f9b50699..0d204edcee 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/logger/LoggerTag.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/logger/LoggerTag.kt @@ -24,6 +24,7 @@ package org.matrix.android.sdk.api.logger */ open class LoggerTag(_value: String, parentTag: LoggerTag? = null) { + object SYNC : LoggerTag("SYNC") object VOIP : LoggerTag("VOIP") val value: String = if (parentTag == null) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/network/ApiPath.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/network/ApiPath.kt index db112a30b2..baf33a59c5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/network/ApiPath.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/network/ApiPath.kt @@ -154,7 +154,6 @@ enum class ApiPath(val path: String, val method: String) { SEND_STATE_EVENT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/state/{state_event_type}", "PUT"), SEND_STATE_EVENT_WITH_STATE_KEY(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/state/{state_event_type}/{state_key}", "PUT"), GET_ROOM_STATE(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/state", "GET"), - SEND_RELATION(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/send_relation/{parent_id}/{relation_type}/{event_type}", "POST"), GET_RELATIONS(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "rooms/{roomId}/relations/{eventId}/{relationType}/{eventType}", "GET"), JOIN_ROOM(NetworkConstants.URI_API_PREFIX_PATH_R0 + "join/{roomIdOrAlias}", "POST"), LEAVE_ROOM(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/leave", "POST"), diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/query/QueryStringValue.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/query/QueryStringValue.kt index 8f83beface..31ec131c5c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/query/QueryStringValue.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/query/QueryStringValue.kt @@ -19,17 +19,41 @@ package org.matrix.android.sdk.api.query /** * Basic query language. All these cases are mutually exclusive. */ -sealed class QueryStringValue { - object NoCondition : QueryStringValue() - object IsNull : QueryStringValue() - object IsNotNull : QueryStringValue() - object IsEmpty : QueryStringValue() - object IsNotEmpty : QueryStringValue() - data class Equals(val string: String, val case: Case = Case.SENSITIVE) : QueryStringValue() - data class Contains(val string: String, val case: Case = Case.SENSITIVE) : QueryStringValue() +sealed interface QueryStringValue { + sealed interface ContentQueryStringValue : QueryStringValue { + val string: String + val case: Case + } + + object NoCondition : QueryStringValue + object IsNull : QueryStringValue + object IsNotNull : QueryStringValue + object IsEmpty : QueryStringValue + object IsNotEmpty : QueryStringValue + + data class Equals(override val string: String, override val case: Case = Case.SENSITIVE) : ContentQueryStringValue + data class Contains(override val string: String, override val case: Case = Case.SENSITIVE) : ContentQueryStringValue enum class Case { + /** + * Match query sensitive to case + */ SENSITIVE, - INSENSITIVE + + /** + * Match query insensitive to case, this only works for Latin-1 character sets + */ + INSENSITIVE, + + /** + * Match query with input normalized (case insensitive) + * Works around Realms inability to sort or filter by case for non Latin-1 character sets + * Expects the target field to contain normalized data + * + * @see org.matrix.android.sdk.internal.util.Normalizer.normalize + */ + NORMALIZED } } + +internal fun QueryStringValue.isNormalized() = this is QueryStringValue.ContentQueryStringValue && case == QueryStringValue.Case.NORMALIZED diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt index 2f981ffbbe..d4bfd4ee8c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt @@ -18,7 +18,9 @@ package org.matrix.android.sdk.api.session import androidx.annotation.MainThread import androidx.lifecycle.LiveData +import kotlinx.coroutines.flow.SharedFlow import okhttp3.OkHttpClient +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.auth.data.SessionParams import org.matrix.android.sdk.api.failure.GlobalError import org.matrix.android.sdk.api.federation.FederationService @@ -36,11 +38,12 @@ import org.matrix.android.sdk.api.session.file.FileService import org.matrix.android.sdk.api.session.group.GroupService import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService import org.matrix.android.sdk.api.session.identity.IdentityService -import org.matrix.android.sdk.api.session.initsync.InitialSyncProgressService +import org.matrix.android.sdk.api.session.initsync.SyncStatusService import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerService import org.matrix.android.sdk.api.session.media.MediaService import org.matrix.android.sdk.api.session.openid.OpenIdService import org.matrix.android.sdk.api.session.permalinks.PermalinkService +import org.matrix.android.sdk.api.session.presence.PresenceService import org.matrix.android.sdk.api.session.profile.ProfileService import org.matrix.android.sdk.api.session.pushers.PushersService import org.matrix.android.sdk.api.session.room.RoomDirectoryService @@ -52,6 +55,7 @@ import org.matrix.android.sdk.api.session.signout.SignOutService import org.matrix.android.sdk.api.session.space.SpaceService import org.matrix.android.sdk.api.session.sync.FilterService import org.matrix.android.sdk.api.session.sync.SyncState +import org.matrix.android.sdk.api.session.sync.model.SyncResponse import org.matrix.android.sdk.api.session.terms.TermsService import org.matrix.android.sdk.api.session.thirdparty.ThirdPartyService import org.matrix.android.sdk.api.session.typing.TypingUsersTracker @@ -73,13 +77,16 @@ interface Session : TermsService, EventService, ProfileService, + PresenceService, PushRuleService, PushersService, - InitialSyncProgressService, + SyncStatusService, HomeServerCapabilitiesService, SecureStorageService, AccountService { + val coroutineDispatchers: MatrixCoroutineDispatchers + /** * The params associated to the session */ @@ -143,6 +150,11 @@ interface Session : */ fun getSyncState(): SyncState + /** + * This method returns a flow of SyncResponse. New value will be pushed through the sync thread. + */ + fun syncFlow(): SharedFlow + /** * This methods return true if an initial sync has been processed */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallState.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallState.kt index 47a63b4a25..ff1df63300 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallState.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallState.kt @@ -26,7 +26,7 @@ sealed class CallState { /** * CreateOffer. Intermediate state between Idle and Dialing. */ - object CreateOffer: CallState() + object CreateOffer : CallState() /** Dialing. Outgoing call is signaling the remote peer */ object Dialing : CallState() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/MXCrossSigningInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/MXCrossSigningInfo.kt index 6327dd92fd..20ee68d228 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/MXCrossSigningInfo.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/MXCrossSigningInfo.kt @@ -24,8 +24,8 @@ data class MXCrossSigningInfo( val crossSigningKeys: List ) { - fun isTrusted(): Boolean = masterKey()?.trustLevel?.isVerified() == true - && selfSigningKey()?.trustLevel?.isVerified() == true + fun isTrusted(): Boolean = masterKey()?.trustLevel?.isVerified() == true && + selfSigningKey()?.trustLevel?.isVerified() == true fun masterKey(): CryptoCrossSigningKey? = crossSigningKeys .firstOrNull { it.usages?.contains(KeyUsage.MASTER.value) == true } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keyshare/GossipingRequestListener.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keyshare/GossipingRequestListener.kt index 810d28dd83..ba2d4ba3f6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keyshare/GossipingRequestListener.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keyshare/GossipingRequestListener.kt @@ -16,8 +16,8 @@ package org.matrix.android.sdk.api.session.crypto.keyshare -import org.matrix.android.sdk.internal.crypto.IncomingRoomKeyRequest import org.matrix.android.sdk.internal.crypto.IncomingRequestCancellation +import org.matrix.android.sdk.internal.crypto.IncomingRoomKeyRequest import org.matrix.android.sdk.internal.crypto.IncomingSecretShareRequest /** diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/PendingVerificationRequest.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/PendingVerificationRequest.kt index 1ee161724a..be450b9d03 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/PendingVerificationRequest.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/PendingVerificationRequest.kt @@ -48,8 +48,8 @@ data class PendingVerificationRequest( * SAS is supported if I support it and the other party support it */ fun isSasSupported(): Boolean { - return requestInfo?.methods?.contains(VERIFICATION_METHOD_SAS).orFalse() - && readyInfo?.methods?.contains(VERIFICATION_METHOD_SAS).orFalse() + return requestInfo?.methods?.contains(VERIFICATION_METHOD_SAS).orFalse() && + readyInfo?.methods?.contains(VERIFICATION_METHOD_SAS).orFalse() } /** @@ -57,11 +57,11 @@ data class PendingVerificationRequest( */ fun otherCanShowQrCode(): Boolean { return if (isIncoming) { - requestInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SHOW).orFalse() - && readyInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SCAN).orFalse() + requestInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SHOW).orFalse() && + readyInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SCAN).orFalse() } else { - requestInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SCAN).orFalse() - && readyInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SHOW).orFalse() + requestInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SCAN).orFalse() && + readyInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SHOW).orFalse() } } @@ -70,11 +70,11 @@ data class PendingVerificationRequest( */ fun otherCanScanQrCode(): Boolean { return if (isIncoming) { - requestInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SCAN).orFalse() - && readyInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SHOW).orFalse() + requestInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SCAN).orFalse() && + readyInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SHOW).orFalse() } else { - requestInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SHOW).orFalse() - && readyInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SCAN).orFalse() + requestInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SHOW).orFalse() && + readyInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SCAN).orFalse() } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt index 1f8471c111..aad5fce33e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt @@ -18,7 +18,12 @@ package org.matrix.android.sdk.api.session.events.model import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import org.json.JSONObject +import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.failure.MatrixError import org.matrix.android.sdk.api.session.crypto.MXCryptoError +import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.api.session.room.model.RoomMemberContent import org.matrix.android.sdk.api.session.room.model.message.MessageContent import org.matrix.android.sdk.api.session.room.model.message.MessageType import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent @@ -27,9 +32,7 @@ import org.matrix.android.sdk.api.util.JsonDict import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent import org.matrix.android.sdk.internal.di.MoshiProvider -import org.json.JSONObject -import org.matrix.android.sdk.api.extensions.tryOrNull -import org.matrix.android.sdk.api.failure.MatrixError +import org.matrix.android.sdk.internal.session.presence.model.PresenceContent import timber.log.Timber typealias Content = JsonDict @@ -238,8 +241,8 @@ data class Event( } fun Event.isTextMessage(): Boolean { - return getClearType() == EventType.MESSAGE - && when (getClearContent()?.get(MessageContent.MSG_TYPE_JSON_KEY)) { + return getClearType() == EventType.MESSAGE && + when (getClearContent()?.get(MessageContent.MSG_TYPE_JSON_KEY)) { MessageType.MSGTYPE_TEXT, MessageType.MSGTYPE_EMOTE, MessageType.MSGTYPE_NOTICE -> true @@ -248,40 +251,40 @@ fun Event.isTextMessage(): Boolean { } fun Event.isImageMessage(): Boolean { - return getClearType() == EventType.MESSAGE - && when (getClearContent()?.get(MessageContent.MSG_TYPE_JSON_KEY)) { + return getClearType() == EventType.MESSAGE && + when (getClearContent()?.get(MessageContent.MSG_TYPE_JSON_KEY)) { MessageType.MSGTYPE_IMAGE -> true else -> false } } fun Event.isVideoMessage(): Boolean { - return getClearType() == EventType.MESSAGE - && when (getClearContent()?.get(MessageContent.MSG_TYPE_JSON_KEY)) { + return getClearType() == EventType.MESSAGE && + when (getClearContent()?.get(MessageContent.MSG_TYPE_JSON_KEY)) { MessageType.MSGTYPE_VIDEO -> true else -> false } } fun Event.isAudioMessage(): Boolean { - return getClearType() == EventType.MESSAGE - && when (getClearContent()?.get(MessageContent.MSG_TYPE_JSON_KEY)) { + return getClearType() == EventType.MESSAGE && + when (getClearContent()?.get(MessageContent.MSG_TYPE_JSON_KEY)) { MessageType.MSGTYPE_AUDIO -> true else -> false } } fun Event.isFileMessage(): Boolean { - return getClearType() == EventType.MESSAGE - && when (getClearContent()?.get(MessageContent.MSG_TYPE_JSON_KEY)) { + return getClearType() == EventType.MESSAGE && + when (getClearContent()?.get(MessageContent.MSG_TYPE_JSON_KEY)) { MessageType.MSGTYPE_FILE -> true else -> false } } fun Event.isAttachmentMessage(): Boolean { - return getClearType() == EventType.MESSAGE - && when (getClearContent()?.get(MessageContent.MSG_TYPE_JSON_KEY)) { + return getClearType() == EventType.MESSAGE && + when (getClearContent()?.get(MessageContent.MSG_TYPE_JSON_KEY)) { MessageType.MSGTYPE_IMAGE, MessageType.MSGTYPE_AUDIO, MessageType.MSGTYPE_VIDEO, @@ -305,3 +308,10 @@ fun Event.isReply(): Boolean { fun Event.isEdition(): Boolean { return getRelationContent()?.takeIf { it.type == RelationType.REPLACE }?.eventId != null } + +fun Event.getPresenceContent(): PresenceContent? { + return content.toModel() +} + +fun Event.isInvitation(): Boolean = type == EventType.STATE_ROOM_MEMBER && + content?.toModel()?.membership == Membership.INVITE diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt index 9c3fdd57da..d0ce55070e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt @@ -106,13 +106,13 @@ object EventType { internal const val DUMMY = "m.dummy" fun isCallEvent(type: String): Boolean { - return type == CALL_INVITE - || type == CALL_CANDIDATES - || type == CALL_ANSWER - || type == CALL_HANGUP - || type == CALL_SELECT_ANSWER - || type == CALL_NEGOTIATE - || type == CALL_REJECT - || type == CALL_REPLACES + return type == CALL_INVITE || + type == CALL_CANDIDATES || + type == CALL_ANSWER || + type == CALL_HANGUP || + type == CALL_SELECT_ANSWER || + type == CALL_NEGOTIATE || + type == CALL_REJECT || + type == CALL_REPLACES } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt index b49236c338..3ed6a7ebb2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt @@ -89,7 +89,7 @@ data class HomeServerCapabilities( * You can also use #isFeatureSupported prior to this call to check if the * feature is supported and report some feedback to user. */ - fun versionOverrideForFeature(feature: String) : String? { + fun versionOverrideForFeature(feature: String): String? { val cap = roomVersions?.capabilities?.get(feature) return cap?.preferred ?: cap?.support?.lastOrNull() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/identity/IdentityService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/identity/IdentityService.kt index 1485ec478b..a22cd572fa 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/identity/IdentityService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/identity/IdentityService.kt @@ -132,7 +132,7 @@ interface IdentityService { * the identity server offers some crypto functionality to help in accepting invitations. * This is less secure than the client doing it itself, but may be useful where this isn't possible. */ - suspend fun sign3pidInvitation(identiyServer: String, token: String, secret: String) : SignInvitationResult + suspend fun sign3pidInvitation(identiyServer: String, token: String, secret: String): SignInvitationResult fun addListener(listener: IdentityServiceListener) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/initsync/SyncStatusService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/initsync/SyncStatusService.kt new file mode 100644 index 0000000000..daab6d9761 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/initsync/SyncStatusService.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2020 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.api.session.initsync + +import androidx.lifecycle.LiveData + +interface SyncStatusService { + + fun getSyncStatusLive(): LiveData + + sealed class Status { + /** + * For initial sync + */ + abstract class InitialSyncStatus : Status() + + object Idle : InitialSyncStatus() + data class Progressing( + val initSyncStep: InitSyncStep, + val percentProgress: Int = 0 + ) : InitialSyncStatus() + + /** + * For incremental sync + */ + abstract class IncrementalSyncStatus : Status() + + object IncrementalSyncIdle : IncrementalSyncStatus() + data class IncrementalSyncParsing( + val rooms: Int, + val toDevice: Int + ) : IncrementalSyncStatus() + object IncrementalSyncError : IncrementalSyncStatus() + object IncrementalSyncDone : IncrementalSyncStatus() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/MatrixLinkify.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/MatrixLinkify.kt index 5e9f3e1eb9..3e27da0c41 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/MatrixLinkify.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/MatrixLinkify.kt @@ -50,11 +50,11 @@ object MatrixLinkify { if (startPos == 0 || text[startPos - 1] != '/') { val endPos = match.range.last + 1 var url = text.substring(match.range) - if (MatrixPatterns.isUserId(url) - || MatrixPatterns.isRoomAlias(url) - || MatrixPatterns.isRoomId(url) - || MatrixPatterns.isGroupId(url) - || MatrixPatterns.isEventId(url)) { + if (MatrixPatterns.isUserId(url) || + MatrixPatterns.isRoomAlias(url) || + MatrixPatterns.isRoomId(url) || + MatrixPatterns.isGroupId(url) || + MatrixPatterns.isEventId(url)) { url = PermalinkService.MATRIX_TO_URL_BASE + url } val span = MatrixPermalinkSpan(url, callback) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/MatrixToConverter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/MatrixToConverter.kt new file mode 100644 index 0000000000..a904e89681 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/MatrixToConverter.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2020 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.api.session.permalinks + +import android.net.Uri + +/** + * Mapping of an input URI to a matrix.to compliant URI. + */ +object MatrixToConverter { + + /** + * Try to convert a URL from an element web instance or from a client permalink to a matrix.to url. + * To be successfully converted, URL path should contain one of the [SUPPORTED_PATHS]. + * Examples: + * - https://riot.im/develop/#/room/#element-android:matrix.org -> https://matrix.to/#/#element-android:matrix.org + * - https://app.element.io/#/room/#element-android:matrix.org -> https://matrix.to/#/#element-android:matrix.org + * - https://www.example.org/#/room/#element-android:matrix.org -> https://matrix.to/#/#element-android:matrix.org + */ + fun convert(uri: Uri): Uri? { + val uriString = uri.toString() + + return when { + // URL is already a matrix.to + uriString.startsWith(PermalinkService.MATRIX_TO_URL_BASE) -> uri + // Web or client url + SUPPORTED_PATHS.any { it in uriString } -> { + val path = SUPPORTED_PATHS.first { it in uriString } + Uri.parse(PermalinkService.MATRIX_TO_URL_BASE + uriString.substringAfter(path)) + } + // URL is not supported + else -> null + } + } + + private val SUPPORTED_PATHS = listOf( + "/#/room/", + "/#/user/", + "/#/group/" + ) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkParser.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkParser.kt index 005a2edae7..edb748c76e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkParser.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkParser.kt @@ -26,6 +26,7 @@ import java.net.URLDecoder * This class turns a uri to a [PermalinkData] * element-based domains (e.g. https://app.element.io/#/user/@chagai95:matrix.org) permalinks * or matrix.to permalinks (e.g. https://matrix.to/#/@chagai95:matrix.org) + * or client permalinks (e.g. user/@chagai95:matrix.org) */ object PermalinkParser { @@ -42,13 +43,15 @@ object PermalinkParser { * https://github.com/matrix-org/matrix-doc/blob/master/proposals/1704-matrix.to-permalinks.md */ fun parse(uri: Uri): PermalinkData { - if (!uri.toString().startsWith(PermalinkService.MATRIX_TO_URL_BASE)) { - return PermalinkData.FallbackLink(uri) - } + // the client or 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 + // so convert URI to matrix.to to simplify parsing process + val matrixToUri = MatrixToConverter.convert(uri) ?: return PermalinkData.FallbackLink(uri) + // We can't use uri.fragment as it is decoding to early and it will break the parsing // of parameters that represents url (like signurl) - val fragment = uri.toString().substringAfter("#") // uri.fragment - if (fragment.isNullOrEmpty()) { + val fragment = matrixToUri.toString().substringAfter("#") // uri.fragment + if (fragment.isEmpty()) { return PermalinkData.FallbackLink(uri) } val safeFragment = fragment.substringBefore('?') @@ -61,20 +64,14 @@ object PermalinkParser { .map { URLDecoder.decode(it, "UTF-8") } .take(2) - // 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 - var identifier = params.getOrNull(0) - if (identifier.equals("user")) { - identifier = params.getOrNull(1) - } - + val identifier = params.getOrNull(0) val extraParameter = params.getOrNull(1) return when { identifier.isNullOrEmpty() -> PermalinkData.FallbackLink(uri) MatrixPatterns.isUserId(identifier) -> PermalinkData.UserLink(userId = identifier) MatrixPatterns.isGroupId(identifier) -> PermalinkData.GroupLink(groupId = identifier) MatrixPatterns.isRoomId(identifier) -> { - handleRoomIdCase(fragment, identifier, uri, extraParameter, viaQueryParameters) + handleRoomIdCase(fragment, identifier, matrixToUri, extraParameter, viaQueryParameters) } MatrixPatterns.isRoomAlias(identifier) -> { PermalinkData.RoomLink( @@ -125,12 +122,13 @@ object PermalinkParser { } } - private fun safeExtractParams(fragment: String) = fragment.substringAfter("?").split('&').mapNotNull { - val splitNameValue = it.split("=") - if (splitNameValue.size == 2) { - Pair(splitNameValue[0], URLDecoder.decode(splitNameValue[1], "UTF-8")) - } else null - } + private fun safeExtractParams(fragment: String) = + fragment.substringAfter("?").split('&').mapNotNull { + val splitNameValue = it.split("=") + if (splitNameValue.size == 2) { + Pair(splitNameValue[0], URLDecoder.decode(splitNameValue[1], "UTF-8")) + } else null + } private fun String.getViaParameters(): List { return UrlQuerySanitizer(this) @@ -138,9 +136,7 @@ object PermalinkParser { .filter { it.mParameter == "via" }.map { - it.mValue.let { - URLDecoder.decode(it, "UTF-8") - } + URLDecoder.decode(it.mValue, "UTF-8") } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkService.kt index a6d4583c76..920dc85c7a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkService.kt @@ -19,7 +19,8 @@ package org.matrix.android.sdk.api.session.permalinks import org.matrix.android.sdk.api.session.events.model.Event /** - * Useful methods to create Matrix permalink (matrix.to links). + * Useful methods to create permalink (like matrix.to links or client permalinks). + * See [org.matrix.android.sdk.api.MatrixConfiguration.clientPermalinkBaseUrl] to setup a custom permalink base url. */ interface PermalinkService { @@ -32,10 +33,11 @@ interface PermalinkService { * Ex: "https://matrix.to/#/!nbzmcXAqpxBXjAdgoX:matrix.org/$1531497316352799BevdV:matrix.org" * * @param event the event + * @param forceMatrixTo whether we should force using matrix.to base URL * * @return the permalink, or null in case of error */ - fun createPermalink(event: Event): String? + fun createPermalink(event: Event, forceMatrixTo: Boolean = false): String? /** * Creates a permalink for an id (can be a user Id, etc.). @@ -43,18 +45,21 @@ interface PermalinkService { * Ex: "https://matrix.to/#/@benoit:matrix.org" * * @param id the id + * @param forceMatrixTo whether we should force using matrix.to base URL + * * @return the permalink, or null in case of error */ - fun createPermalink(id: String): String? + fun createPermalink(id: String, forceMatrixTo: Boolean = false): String? /** * Creates a permalink for a roomId, including the via parameters * * @param roomId the room id + * @param forceMatrixTo whether we should force using matrix.to base URL * * @return the permalink, or null in case of error */ - fun createRoomPermalink(roomId: String, viaServers: List? = null): String? + fun createRoomPermalink(roomId: String, viaServers: List? = null, forceMatrixTo: Boolean = false): String? /** * Creates a permalink for an event. If you have an event you can use [createPermalink] @@ -62,10 +67,11 @@ interface PermalinkService { * * @param roomId the id of the room * @param eventId the id of the event + * @param forceMatrixTo whether we should force using matrix.to base URL * * @return the permalink */ - fun createPermalink(roomId: String, eventId: String): String + fun createPermalink(roomId: String, eventId: String, forceMatrixTo: Boolean = false): String /** * Extract the linked id from the universal link diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/presence/PresenceService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/presence/PresenceService.kt new file mode 100644 index 0000000000..82a81f4b64 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/presence/PresenceService.kt @@ -0,0 +1,41 @@ +/* + * 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.api.session.presence + +import org.matrix.android.sdk.api.session.presence.model.PresenceEnum +import org.matrix.android.sdk.api.session.presence.model.UserPresence + +/** + * This interface defines methods for handling user presence information. + */ +interface PresenceService { + /** + * Update the presence status for the current user + * @param presence the new presence state + * @param statusMsg the status message to attach to this state + */ + suspend fun setMyPresence(presence: PresenceEnum, statusMsg: String? = null) + + /** + * Fetch the given user's presence state. + * @param userId the userId whose presence state to get. + */ + suspend fun fetchPresence(userId: String): UserPresence + + // TODO Add live data (of Flow) of the presence of a userId +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/presence/model/PresenceEnum.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/presence/model/PresenceEnum.kt new file mode 100644 index 0000000000..6d9994ef1c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/presence/model/PresenceEnum.kt @@ -0,0 +1,36 @@ +/* + * 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.api.session.presence.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = false) +enum class PresenceEnum(val value: String) { + @Json(name = "online") + ONLINE("online"), + + @Json(name = "offline") + OFFLINE("offline"), + + @Json(name = "unavailable") + UNAVAILABLE("unavailable"); + + companion object { + fun from(s: String): PresenceEnum? = values().find { it.value == s } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/presence/model/UserPresence.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/presence/model/UserPresence.kt new file mode 100644 index 0000000000..6b33ff07d5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/presence/model/UserPresence.kt @@ -0,0 +1,24 @@ +/* + * 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.api.session.presence.model + +data class UserPresence( + val lastActiveAgo: Long? = null, + val statusMessage: String? = null, + val isCurrentlyActive: Boolean? = null, + val presence: PresenceEnum = PresenceEnum.OFFLINE +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushers/Pusher.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushers/Pusher.kt index eed75c9daf..b85ab32b21 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushers/Pusher.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushers/Pusher.kt @@ -26,7 +26,14 @@ data class Pusher( val data: PusherData, val state: PusherState -) +) { + companion object { + + const val KIND_EMAIL = "email" + const val KIND_HTTP = "http" + const val APP_ID_EMAIL = "m.email" + } +} enum class PusherState { UNREGISTERED, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushers/PushersService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushers/PushersService.kt index a5ec100f64..f884d3e890 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushers/PushersService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushers/PushersService.kt @@ -27,42 +27,43 @@ interface PushersService { /** * Add a new HTTP pusher. - * Note that only `http` kind is supported by the SDK for now. * Ref: https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-pushers-set * - * @param pushkey This is a unique identifier for this pusher. The value you should use for - * this is the routing or destination address information for the notification, - * for example, the APNS token for APNS or the Registration ID for GCM. If your - * notification client has no such concept, use any unique identifier. Max length, 512 chars. - * If the kind is "email", this is the email address to send notifications to. - * @param appId the application id - * This is a reverse-DNS style identifier for the application. It is recommended - * that this end with the platform, such that different platform versions get - * different app identifiers. Max length, 64 chars. - * @param profileTag This string determines which set of device specific rules this pusher executes. - * @param lang The preferred language for receiving notifications (e.g. "en" or "en-US"). - * @param appDisplayName A human readable string that will allow the user to identify what application owns this pusher. - * @param deviceDisplayName A human readable string that will allow the user to identify what device owns this pusher. - * @param url The URL to use to send notifications to. MUST be an HTTPS URL with a path of /_matrix/push/v1/notify. - * @param append If true, the homeserver should add another pusher with the given pushkey and App ID in addition - * to any others with different user IDs. Otherwise, the homeserver must remove any other pushers - * with the same App ID and pushkey for different users. - * @param withEventIdOnly true to limit the push content to only id and not message content - * Ref: https://matrix.org/docs/spec/push_gateway/r0.1.1#homeserver-behaviour - * - * @return A work request uuid. Can be used to listen to the status - * (LiveData status = workManager.getWorkInfoByIdLiveData()) * @throws [InvalidParameterException] if a parameter is not correct */ - fun addHttpPusher(pushkey: String, - appId: String, - profileTag: String, - lang: String, - appDisplayName: String, - deviceDisplayName: String, - url: String, - append: Boolean, - withEventIdOnly: Boolean): UUID + suspend fun addHttpPusher(httpPusher: HttpPusher) + + /** + * Enqueues a new HTTP pusher via the WorkManager API. + * Ref: https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-pushers-set + * + * @return A work request uuid. Can be used to listen to the status + * (LiveData status = workManager.getWorkInfoByIdLiveData()) + * @throws [InvalidParameterException] if a parameter is not correct + */ + fun enqueueAddHttpPusher(httpPusher: HttpPusher): UUID + + /** + * Add a new Email pusher. + * Ref: https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-pushers-set + * + * @param email The email address to send notifications to. + * @param lang The preferred language for receiving notifications (e.g. "en" or "en-US"). + * @param emailBranding The branding placeholder to include in the email communications. + * @param appDisplayName A human readable string that will allow the user to identify what application owns this pusher. + * @param deviceDisplayName A human readable string that will allow the user to identify what device owns this pusher. + * @param append If true, the homeserver should add another pusher with the given pushkey and App ID in addition + * to any others with different user IDs. Otherwise, the homeserver must remove any other pushers + * with the same App ID and pushkey for different users. Typically We always want to append for + * email pushers since we don't want to stop other accounts notifying to the same email address. + * @throws [InvalidParameterException] if a parameter is not correct + */ + suspend fun addEmailPusher(email: String, + lang: String, + emailBranding: String, + appDisplayName: String, + deviceDisplayName: String, + append: Boolean = true) /** * Directly ask the push gateway to send a push to this device @@ -80,10 +81,23 @@ interface PushersService { eventId: String) /** - * Remove the http pusher + * Remove a registered pusher + * @param pusher the pusher to remove, can be http or email + */ + suspend fun removePusher(pusher: Pusher) + + /** + * Remove a Http pusher by its pushkey and appId + * @see addHttpPusher */ suspend fun removeHttpPusher(pushkey: String, appId: String) + /** + * Remove an Email pusher + * @see addEmailPusher + */ + suspend fun removeEmailPusher(email: String) + /** * Get the current pushers, as a LiveData */ @@ -93,4 +107,61 @@ interface PushersService { * Get the current pushers */ fun getPushers(): List + + data class HttpPusher( + + /** + * This is a unique identifier for this pusher. The value you should use for + * this is the routing or destination address information for the notification, + * for example, the APNS token for APNS or the Registration ID for GCM. If your + * notification client has no such concept, use any unique identifier. Max length, 512 chars. + */ + val pushkey: String, + + /** + * The application id + * This is a reverse-DNS style identifier for the application. It is recommended + * that this end with the platform, such that different platform versions get + * different app identifiers. Max length, 64 chars. + */ + val appId: String, + + /** + * This string determines which set of device specific rules this pusher executes. + */ + val profileTag: String, + + /** + * The preferred language for receiving notifications (e.g. "en" or "en-US"). + */ + val lang: String, + + /** + * A human readable string that will allow the user to identify what application owns this pusher. + */ + val appDisplayName: String, + + /** + * A human readable string that will allow the user to identify what device owns this pusher. + */ + val deviceDisplayName: String, + + /** + * The URL to use to send notifications to. MUST be an HTTPS URL with a path of /_matrix/push/v1/notify. + */ + val url: String, + + /** + * If true, the homeserver should add another pusher with the given pushkey and App ID in addition + * to any others with different user IDs. Otherwise, the homeserver must remove any other pushers + * with the same App ID and pushkey for different users. + */ + val append: Boolean, + + /** + * true to limit the push content to only id and not message content + * Ref: https://matrix.org/docs/spec/push_gateway/r0.1.1#homeserver-behaviour + */ + val withEventIdOnly: Boolean + ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/AliasAvailabilityResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/AliasAvailabilityResult.kt index 6f607569c0..fff6adecbf 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/AliasAvailabilityResult.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/AliasAvailabilityResult.kt @@ -19,6 +19,6 @@ package org.matrix.android.sdk.api.session.room import org.matrix.android.sdk.api.session.room.alias.RoomAliasError sealed class AliasAvailabilityResult { - object Available: AliasAvailabilityResult() + object Available : AliasAvailabilityResult() data class NotAvailable(val roomAliasError: RoomAliasError) : AliasAvailabilityResult() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt index ebe96b6382..6c0e730499 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt @@ -17,6 +17,7 @@ package org.matrix.android.sdk.api.session.room import androidx.lifecycle.LiveData +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.session.room.accountdata.RoomAccountDataService import org.matrix.android.sdk.api.session.room.alias.AliasService import org.matrix.android.sdk.api.session.room.call.RoomCallService @@ -61,6 +62,8 @@ interface Room : RoomAccountDataService, RoomVersionService { + val coroutineDispatchers: MatrixCoroutineDispatchers + /** * The roomId of this room */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomDirectoryService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomDirectoryService.kt index f3e3913bc1..9446f0fdff 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomDirectoryService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomDirectoryService.kt @@ -41,5 +41,5 @@ interface RoomDirectoryService { */ suspend fun setRoomDirectoryVisibility(roomId: String, roomDirectoryVisibility: RoomDirectoryVisibility) - suspend fun checkAliasAvailability(aliasLocalPart: String?) : AliasAvailabilityResult + suspend fun checkAliasAvailability(aliasLocalPart: String?): AliasAvailabilityResult } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt index 5d26b21349..e4bd498990 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt @@ -94,13 +94,15 @@ interface RoomService { * Get a snapshot list of room summaries. * @return the immutable list of [RoomSummary] */ - fun getRoomSummaries(queryParams: RoomSummaryQueryParams): List + fun getRoomSummaries(queryParams: RoomSummaryQueryParams, + sortOrder: RoomSortOrder = RoomSortOrder.NONE): List /** * Get a live list of room summaries. This list is refreshed as soon as the data changes. * @return the [LiveData] of List[RoomSummary] */ - fun getRoomSummariesLive(queryParams: RoomSummaryQueryParams): LiveData> + fun getRoomSummariesLive(queryParams: RoomSummaryQueryParams, + sortOrder: RoomSortOrder = RoomSortOrder.ACTIVITY): LiveData> /** * Get a snapshot list of Breadcrumbs @@ -220,7 +222,7 @@ interface RoomService { .setPrefetchDistance(10) .build() - fun getFlattenRoomSummaryChildrenOf(spaceId: String?, memberships: List = Membership.activeMemberships()) : List + fun getFlattenRoomSummaryChildrenOf(spaceId: String?, memberships: List = Membership.activeMemberships()): List /** * Returns all the children of this space, as LiveData diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomMemberSummary.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomMemberSummary.kt index fba3a1dd71..39177a4296 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomMemberSummary.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomMemberSummary.kt @@ -16,12 +16,15 @@ package org.matrix.android.sdk.api.session.room.model +import org.matrix.android.sdk.api.session.presence.model.UserPresence + /** * Class representing a simplified version of EventType.STATE_ROOM_MEMBER state event content */ data class RoomMemberSummary constructor( val membership: Membership, val userId: String, + val userPresence: UserPresence? = null, val displayName: String? = null, val avatarUrl: String? = null ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomSummary.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomSummary.kt index cae4775e71..10cad026bc 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomSummary.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomSummary.kt @@ -17,6 +17,7 @@ package org.matrix.android.sdk.api.session.room.model import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel +import org.matrix.android.sdk.api.session.presence.model.UserPresence import org.matrix.android.sdk.api.session.room.model.tag.RoomTag import org.matrix.android.sdk.api.session.room.send.UserDraft import org.matrix.android.sdk.api.session.room.sender.SenderInfo @@ -38,6 +39,7 @@ data class RoomSummary( val joinRules: RoomJoinRules? = null, val isDirect: Boolean = false, val directUserId: String? = null, + val directUserPresence: UserPresence? = null, val joinedMembersCount: Int? = 0, val invitedMembersCount: Int? = 0, val latestPreviewableEvent: TimelineEvent? = null, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallAnswerContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallAnswerContent.kt index 180b32db05..6b4d782832 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallAnswerContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallAnswerContent.kt @@ -44,7 +44,7 @@ data class CallAnswerContent( * Capability advertisement. */ @Json(name = "capabilities") val capabilities: CallCapabilities? = null -): CallSignalingContent { +) : CallSignalingContent { @JsonClass(generateAdapter = true) data class Answer( diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallCandidatesContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallCandidatesContent.kt index dc0a1e3b2e..deec80f4ff 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallCandidatesContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallCandidatesContent.kt @@ -41,4 +41,4 @@ data class CallCandidatesContent( * Required. The version of the VoIP specification this messages adheres to. */ @Json(name = "version") override val version: String? -): CallSignalingContent +) : CallSignalingContent diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallInviteContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallInviteContent.kt index e4332f0ea7..d70e63d122 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallInviteContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallInviteContent.kt @@ -55,7 +55,7 @@ data class CallInviteContent( */ @Json(name = "capabilities") val capabilities: CallCapabilities? = null -): CallSignalingContent { +) : CallSignalingContent { @JsonClass(generateAdapter = true) data class Offer( /** diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallNegotiateContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallNegotiateContent.kt index 68dd5ef043..bbbfbe68ab 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallNegotiateContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallNegotiateContent.kt @@ -47,7 +47,7 @@ data class CallNegotiateContent( */ @Json(name = "version") override val version: String? - ): CallSignalingContent { + ) : CallSignalingContent { @JsonClass(generateAdapter = true) data class Description( /** diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallReplacesContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallReplacesContent.kt index 4559c5db6d..7947b7d0bd 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallReplacesContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallReplacesContent.kt @@ -61,7 +61,7 @@ data class CallReplacesContent( * Required. The version of the VoIP specification this messages adheres to. */ @Json(name = "version") override val version: String? -): CallSignalingContent { +) : CallSignalingContent { @JsonClass(generateAdapter = true) data class TargetUser( diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallSelectAnswerContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallSelectAnswerContent.kt index 795f332711..634bee1d83 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallSelectAnswerContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallSelectAnswerContent.kt @@ -41,4 +41,4 @@ data class CallSelectAnswerContent( * Required. The version of the VoIP specification this message adheres to. */ @Json(name = "version") override val version: String? -): CallSignalingContent +) : CallSignalingContent diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/version/RoomVersionService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/version/RoomVersionService.kt index ea67b55174..d806e6007e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/version/RoomVersionService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/version/RoomVersionService.kt @@ -31,7 +31,7 @@ interface RoomVersionService { /** * Get the recommended room version for the current homeserver */ - fun getRecommendedVersion() : String + fun getRecommendedVersion(): String /** * Ask if the user has enough power level to upgrade the room diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SharedSecretStorageError.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SharedSecretStorageError.kt index 59325f9903..a91b97b86c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SharedSecretStorageError.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SharedSecretStorageError.kt @@ -22,8 +22,8 @@ sealed class SharedSecretStorageError(message: String?) : Throwable(message) { data class UnknownAlgorithm(val keyId: String) : SharedSecretStorageError("Unknown algorithm $keyId") data class UnsupportedAlgorithm(val algorithm: String) : SharedSecretStorageError("Unknown algorithm $algorithm") data class SecretNotEncrypted(val secretName: String) : SharedSecretStorageError("Missing content for secret $secretName") - data class SecretNotEncryptedWithKey(val secretName: String, val keyId: String) - : SharedSecretStorageError("Missing content for secret $secretName with key $keyId") + data class SecretNotEncryptedWithKey(val secretName: String, val keyId: String) : + SharedSecretStorageError("Missing content for secret $secretName with key $keyId") object BadKeyFormat : SharedSecretStorageError("Bad Key Format") object ParsingError : SharedSecretStorageError("parsing Error") diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceService.kt index f40572518f..357c0b941a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceService.kt @@ -19,6 +19,7 @@ package org.matrix.android.sdk.api.session.space import android.net.Uri import androidx.lifecycle.LiveData import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.room.RoomSortOrder import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.internal.session.space.peeking.SpacePeekResult @@ -74,9 +75,11 @@ interface SpaceService { * Get a live list of space summaries. This list is refreshed as soon as the data changes. * @return the [LiveData] of List[SpaceSummary] */ - fun getSpaceSummariesLive(queryParams: SpaceSummaryQueryParams): LiveData> + fun getSpaceSummariesLive(queryParams: SpaceSummaryQueryParams, + sortOrder: RoomSortOrder = RoomSortOrder.NONE): LiveData> - fun getSpaceSummaries(spaceSummaryQueryParams: SpaceSummaryQueryParams): List + fun getSpaceSummaries(spaceSummaryQueryParams: SpaceSummaryQueryParams, + sortOrder: RoomSortOrder = RoomSortOrder.NONE): List suspend fun joinSpace(spaceIdOrAlias: String, reason: String? = null, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/DeviceListResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/DeviceListResponse.kt similarity index 90% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/DeviceListResponse.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/DeviceListResponse.kt index bfa8c342b6..c05e1e5187 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/DeviceListResponse.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/DeviceListResponse.kt @@ -13,7 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.matrix.android.sdk.internal.session.sync.model + +package org.matrix.android.sdk.api.session.sync.model import com.squareup.moshi.JsonClass @@ -21,7 +22,7 @@ import com.squareup.moshi.JsonClass * This class describes the device list response from a sync request */ @JsonClass(generateAdapter = true) -internal data class DeviceListResponse( +data class DeviceListResponse( // user ids list which have new crypto devices val changed: List = emptyList(), // List of user ids who are no more tracked. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/DeviceOneTimeKeysCountSyncResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/DeviceOneTimeKeysCountSyncResponse.kt similarity index 87% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/DeviceOneTimeKeysCountSyncResponse.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/DeviceOneTimeKeysCountSyncResponse.kt index d5b435ac27..930cfb153f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/DeviceOneTimeKeysCountSyncResponse.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/DeviceOneTimeKeysCountSyncResponse.kt @@ -14,12 +14,12 @@ * limitations under the License. */ -package org.matrix.android.sdk.internal.session.sync.model +package org.matrix.android.sdk.api.session.sync.model import com.squareup.moshi.Json import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) -internal data class DeviceOneTimeKeysCountSyncResponse( +data class DeviceOneTimeKeysCountSyncResponse( @Json(name = "signed_curve25519") val signedCurve25519: Int? = null ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/GroupSyncProfile.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/GroupSyncProfile.kt similarity index 91% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/GroupSyncProfile.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/GroupSyncProfile.kt index ee6aabb0a9..581e6824ee 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/GroupSyncProfile.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/GroupSyncProfile.kt @@ -14,13 +14,13 @@ * limitations under the License. */ -package org.matrix.android.sdk.internal.session.sync.model +package org.matrix.android.sdk.api.session.sync.model import com.squareup.moshi.Json import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) -internal data class GroupSyncProfile( +data class GroupSyncProfile( /** * The name of the group, if any. May be nil. */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/GroupsSyncResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/GroupsSyncResponse.kt similarity index 92% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/GroupsSyncResponse.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/GroupsSyncResponse.kt index 4c2dce3ba8..fd8710bbda 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/GroupsSyncResponse.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/GroupsSyncResponse.kt @@ -14,13 +14,13 @@ * limitations under the License. */ -package org.matrix.android.sdk.internal.session.sync.model +package org.matrix.android.sdk.api.session.sync.model import com.squareup.moshi.Json import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) -internal data class GroupsSyncResponse( +data class GroupsSyncResponse( /** * Joined groups: An array of groups ids. */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/InvitedGroupSync.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/InvitedGroupSync.kt similarity index 90% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/InvitedGroupSync.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/InvitedGroupSync.kt index 148c2aeab9..d41df9f0f6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/InvitedGroupSync.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/InvitedGroupSync.kt @@ -14,13 +14,13 @@ * limitations under the License. */ -package org.matrix.android.sdk.internal.session.sync.model +package org.matrix.android.sdk.api.session.sync.model import com.squareup.moshi.Json import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) -internal data class InvitedGroupSync( +data class InvitedGroupSync( /** * The identifier of the inviter. */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/InvitedRoomSync.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/InvitedRoomSync.kt similarity index 93% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/InvitedRoomSync.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/InvitedRoomSync.kt index c21a73abc2..dc63c5ba07 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/InvitedRoomSync.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/InvitedRoomSync.kt @@ -13,14 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.matrix.android.sdk.internal.session.sync.model + +package org.matrix.android.sdk.api.session.sync.model import com.squareup.moshi.Json import com.squareup.moshi.JsonClass // InvitedRoomSync represents a room invitation during server sync v2. @JsonClass(generateAdapter = true) -internal data class InvitedRoomSync( +data class InvitedRoomSync( /** * The state of a room that the user has been invited to. These state events may only have the 'sender', 'type', 'state_key' diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/LazyRoomSyncEphemeral.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/LazyRoomSyncEphemeral.kt similarity index 77% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/LazyRoomSyncEphemeral.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/LazyRoomSyncEphemeral.kt index 83006c646b..087a5f52dc 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/LazyRoomSyncEphemeral.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/LazyRoomSyncEphemeral.kt @@ -1,11 +1,11 @@ /* - * Copyright (c) 2021 The Matrix.org Foundation C.I.C. + * Copyright 2020 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 + * 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, @@ -14,12 +14,12 @@ * limitations under the License. */ -package org.matrix.android.sdk.internal.session.sync.model +package org.matrix.android.sdk.api.session.sync.model import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = false) -internal sealed class LazyRoomSyncEphemeral { +sealed class LazyRoomSyncEphemeral { data class Parsed(val _roomSyncEphemeral: RoomSyncEphemeral) : LazyRoomSyncEphemeral() object Stored : LazyRoomSyncEphemeral() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/PresenceSyncResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/PresenceSyncResponse.kt similarity index 90% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/PresenceSyncResponse.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/PresenceSyncResponse.kt index 92d09aa4f5..d632552888 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/PresenceSyncResponse.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/PresenceSyncResponse.kt @@ -14,14 +14,14 @@ * limitations under the License. */ -package org.matrix.android.sdk.internal.session.sync.model +package org.matrix.android.sdk.api.session.sync.model import com.squareup.moshi.JsonClass import org.matrix.android.sdk.api.session.events.model.Event // PresenceSyncResponse represents the updates to the presence status of other users during server sync v2. @JsonClass(generateAdapter = true) -internal data class PresenceSyncResponse( +data class PresenceSyncResponse( /** * List of presence events (array of Event with type m.presence). diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomInviteState.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/RoomInviteState.kt similarity index 91% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomInviteState.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/RoomInviteState.kt index ded9e2a350..59b4b4fc32 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomInviteState.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/RoomInviteState.kt @@ -13,7 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.matrix.android.sdk.internal.session.sync.model + +package org.matrix.android.sdk.api.session.sync.model import com.squareup.moshi.Json import com.squareup.moshi.JsonClass @@ -21,7 +22,7 @@ import org.matrix.android.sdk.api.session.events.model.Event // RoomInviteState represents the state of a room that the user has been invited to. @JsonClass(generateAdapter = true) -internal data class RoomInviteState( +data class RoomInviteState( /** * List of state events (array of MXEvent). diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSync.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/RoomSync.kt similarity index 95% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSync.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/RoomSync.kt index 9aed0d37d6..e3d07602c7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSync.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/RoomSync.kt @@ -13,14 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.matrix.android.sdk.internal.session.sync.model + +package org.matrix.android.sdk.api.session.sync.model import com.squareup.moshi.Json import com.squareup.moshi.JsonClass // RoomSync represents the response for a room during server sync v2. @JsonClass(generateAdapter = true) -internal data class RoomSync( +data class RoomSync( /** * The state updates for the room. */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncAccountData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/RoomSyncAccountData.kt similarity index 90% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncAccountData.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/RoomSyncAccountData.kt index a2375507d8..f2c4ed551c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncAccountData.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/RoomSyncAccountData.kt @@ -14,14 +14,14 @@ * limitations under the License. */ -package org.matrix.android.sdk.internal.session.sync.model +package org.matrix.android.sdk.api.session.sync.model import com.squareup.moshi.Json import com.squareup.moshi.JsonClass import org.matrix.android.sdk.api.session.events.model.Event @JsonClass(generateAdapter = true) -internal data class RoomSyncAccountData( +data class RoomSyncAccountData( /** * List of account data events (array of Event). */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncEphemeral.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/RoomSyncEphemeral.kt similarity index 91% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncEphemeral.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/RoomSyncEphemeral.kt index f2135db6b7..f4d831c16f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncEphemeral.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/RoomSyncEphemeral.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.matrix.android.sdk.internal.session.sync.model +package org.matrix.android.sdk.api.session.sync.model import com.squareup.moshi.Json import com.squareup.moshi.JsonClass @@ -22,7 +22,7 @@ import org.matrix.android.sdk.api.session.events.model.Event // RoomSyncEphemeral represents the ephemeral events in the room that aren't recorded in the timeline or state of the room (e.g. typing). @JsonClass(generateAdapter = true) -internal data class RoomSyncEphemeral( +data class RoomSyncEphemeral( /** * List of ephemeral events (array of Event). */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncState.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/RoomSyncState.kt similarity index 91% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncState.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/RoomSyncState.kt index f86f05d000..7822467564 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncState.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/RoomSyncState.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.matrix.android.sdk.internal.session.sync.model +package org.matrix.android.sdk.api.session.sync.model import com.squareup.moshi.Json import com.squareup.moshi.JsonClass @@ -22,7 +22,7 @@ import org.matrix.android.sdk.api.session.events.model.Event // RoomSyncState represents the state updates for a room during server sync v2. @JsonClass(generateAdapter = true) -internal data class RoomSyncState( +data class RoomSyncState( /** * List of state events (array of Event). The resulting state corresponds to the *start* of the timeline. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncSummary.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/RoomSyncSummary.kt similarity index 95% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncSummary.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/RoomSyncSummary.kt index 228a71ec28..7216a0c992 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncSummary.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/RoomSyncSummary.kt @@ -14,13 +14,13 @@ * limitations under the License. */ -package org.matrix.android.sdk.internal.session.sync.model +package org.matrix.android.sdk.api.session.sync.model import com.squareup.moshi.Json import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) -internal data class RoomSyncSummary( +data class RoomSyncSummary( /** * Present only if the room has no m.room.name or m.room.canonical_alias. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/RoomSyncTimeline.kt similarity index 93% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncTimeline.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/RoomSyncTimeline.kt index 27bbc4343f..82d29a52e2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncTimeline.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/RoomSyncTimeline.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.matrix.android.sdk.internal.session.sync.model +package org.matrix.android.sdk.api.session.sync.model import com.squareup.moshi.Json import com.squareup.moshi.JsonClass @@ -22,7 +22,7 @@ import org.matrix.android.sdk.api.session.events.model.Event // RoomSyncTimeline represents the timeline of messages and state changes for a room during server sync v2. @JsonClass(generateAdapter = true) -internal data class RoomSyncTimeline( +data class RoomSyncTimeline( /** * List of events (array of Event). diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncUnreadNotifications.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/RoomSyncUnreadNotifications.kt similarity index 92% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncUnreadNotifications.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/RoomSyncUnreadNotifications.kt index f01534b884..6618bceacd 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncUnreadNotifications.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/RoomSyncUnreadNotifications.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.matrix.android.sdk.internal.session.sync.model +package org.matrix.android.sdk.api.session.sync.model import com.squareup.moshi.Json import com.squareup.moshi.JsonClass @@ -24,7 +24,7 @@ import org.matrix.android.sdk.api.session.events.model.Event * `MXRoomSyncUnreadNotifications` represents the unread counts for a room. */ @JsonClass(generateAdapter = true) -internal data class RoomSyncUnreadNotifications( +data class RoomSyncUnreadNotifications( /** * List of account data events (array of Event). */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomsSyncResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/RoomsSyncResponse.kt similarity index 93% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomsSyncResponse.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/RoomsSyncResponse.kt index dd2f96c988..ff3ed54264 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomsSyncResponse.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/RoomsSyncResponse.kt @@ -13,14 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.matrix.android.sdk.internal.session.sync.model + +package org.matrix.android.sdk.api.session.sync.model import com.squareup.moshi.Json import com.squareup.moshi.JsonClass // RoomsSyncResponse represents the rooms list in server sync v2 response. @JsonClass(generateAdapter = true) -internal data class RoomsSyncResponse( +data class RoomsSyncResponse( /** * Joined rooms: keys are rooms ids. */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/SyncResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/SyncResponse.kt similarity index 91% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/SyncResponse.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/SyncResponse.kt index f2b2fb7e8f..876e99da63 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/SyncResponse.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/SyncResponse.kt @@ -14,15 +14,14 @@ * limitations under the License. */ -package org.matrix.android.sdk.internal.session.sync.model +package org.matrix.android.sdk.api.session.sync.model import com.squareup.moshi.Json import com.squareup.moshi.JsonClass -import org.matrix.android.sdk.internal.session.sync.model.accountdata.UserAccountDataSync // SyncResponse represents the request response for server sync v2. @JsonClass(generateAdapter = true) -internal data class SyncResponse( +data class SyncResponse( /** * The user private data. */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/ToDeviceSyncResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/ToDeviceSyncResponse.kt similarity index 90% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/ToDeviceSyncResponse.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/ToDeviceSyncResponse.kt index 8f3af56cde..082460cc2d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/ToDeviceSyncResponse.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/ToDeviceSyncResponse.kt @@ -14,14 +14,14 @@ * limitations under the License. */ -package org.matrix.android.sdk.internal.session.sync.model +package org.matrix.android.sdk.api.session.sync.model import com.squareup.moshi.JsonClass import org.matrix.android.sdk.api.session.events.model.Event // ToDeviceSyncResponse represents the data directly sent to one of user's devices. @JsonClass(generateAdapter = true) -internal data class ToDeviceSyncResponse( +data class ToDeviceSyncResponse( /** * List of direct-to-device events. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/accountdata/UserAccountDataSync.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/UserAccountDataSync.kt similarity index 88% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/accountdata/UserAccountDataSync.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/UserAccountDataSync.kt index 05b50ab2c5..9e1b791919 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/accountdata/UserAccountDataSync.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/UserAccountDataSync.kt @@ -14,13 +14,13 @@ * limitations under the License. */ -package org.matrix.android.sdk.internal.session.sync.model.accountdata +package org.matrix.android.sdk.api.session.sync.model import com.squareup.moshi.Json import com.squareup.moshi.JsonClass import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent @JsonClass(generateAdapter = true) -internal data class UserAccountDataSync( +data class UserAccountDataSync( @Json(name = "events") val list: List = emptyList() ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/user/model/User.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/user/model/User.kt index 7cd939a5c2..54ae9e54f6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/user/model/User.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/user/model/User.kt @@ -23,13 +23,8 @@ package org.matrix.android.sdk.api.session.user.model data class User( val userId: String, /** - * For usage in UI, consider using [getBestName] + * For usage in UI, consider converting to MatrixItem and call getBestName() */ val displayName: String? = null, val avatarUrl: String? = null -) { - /** - * Return the display name or the user id - */ - fun getBestName() = displayName?.takeIf { it.isNotEmpty() } ?: userId -} +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixItem.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixItem.kt index 3d2773fb4b..3396c4a6c9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixItem.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixItem.kt @@ -34,8 +34,8 @@ sealed class MatrixItem( ) { data class UserItem(override val id: String, override val displayName: String? = null, - override val avatarUrl: String? = null) - : MatrixItem(id, displayName?.removeSuffix(ircPattern), avatarUrl) { + override val avatarUrl: String? = null) : + MatrixItem(id, displayName?.removeSuffix(ircPattern), avatarUrl) { init { if (BuildConfig.DEBUG) checkId() } @@ -45,8 +45,8 @@ sealed class MatrixItem( data class EventItem(override val id: String, override val displayName: String? = null, - override val avatarUrl: String? = null) - : MatrixItem(id, displayName, avatarUrl) { + override val avatarUrl: String? = null) : + MatrixItem(id, displayName, avatarUrl) { init { if (BuildConfig.DEBUG) checkId() } @@ -56,8 +56,8 @@ sealed class MatrixItem( data class RoomItem(override val id: String, override val displayName: String? = null, - override val avatarUrl: String? = null) - : MatrixItem(id, displayName, avatarUrl) { + override val avatarUrl: String? = null) : + MatrixItem(id, displayName, avatarUrl) { init { if (BuildConfig.DEBUG) checkId() } @@ -67,8 +67,8 @@ sealed class MatrixItem( data class SpaceItem(override val id: String, override val displayName: String? = null, - override val avatarUrl: String? = null) - : MatrixItem(id, displayName, avatarUrl) { + override val avatarUrl: String? = null) : + MatrixItem(id, displayName, avatarUrl) { init { if (BuildConfig.DEBUG) checkId() } @@ -78,36 +78,26 @@ sealed class MatrixItem( data class RoomAliasItem(override val id: String, override val displayName: String? = null, - override val avatarUrl: String? = null) - : MatrixItem(id, displayName, avatarUrl) { + override val avatarUrl: String? = null) : + MatrixItem(id, displayName, avatarUrl) { init { if (BuildConfig.DEBUG) checkId() } - // Best name is the id, and we keep the displayName of the room for the case we need the first letter - override fun getBestName() = id - override fun updateAvatar(newAvatar: String?) = copy(avatarUrl = newAvatar) } data class GroupItem(override val id: String, override val displayName: String? = null, - override val avatarUrl: String? = null) - : MatrixItem(id, displayName, avatarUrl) { + override val avatarUrl: String? = null) : + MatrixItem(id, displayName, avatarUrl) { init { if (BuildConfig.DEBUG) checkId() } - // Best name is the id, and we keep the displayName of the room for the case we need the first letter - override fun getBestName() = id - override fun updateAvatar(newAvatar: String?) = copy(avatarUrl = newAvatar) } - open fun getBestName(): String { - return displayName?.takeIf { it.isNotBlank() } ?: id - } - protected fun checkId() { if (!id.startsWith(getIdPrefix())) { error("Wrong usage of MatrixItem: check the id $id should start with ${getIdPrefix()}") diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/AuthAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/AuthAPI.kt index 50d9e5a06c..554e21ce55 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/AuthAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/AuthAPI.kt @@ -121,6 +121,10 @@ internal interface AuthAPI { @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "login") suspend fun login(@Body loginParams: TokenLoginParams): Credentials + @Headers("CONNECT_TIMEOUT:60000", "READ_TIMEOUT:60000", "WRITE_TIMEOUT:60000") + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "login") + suspend fun login(@Body loginParams: JsonDict): Credentials + /** * Ask the homeserver to reset the password associated with the provided email. */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt index e76dc28734..8784d85c10 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt @@ -172,8 +172,8 @@ internal class DefaultAuthenticationService @Inject constructor( return try { getWellknownLoginFlowInternal(homeServerConnectionConfig) } catch (failure: Throwable) { - if (failure is Failure.OtherServerError - && failure.httpCode == HttpsURLConnection.HTTP_NOT_FOUND /* 404 */) { + if (failure is Failure.OtherServerError && + failure.httpCode == HttpsURLConnection.HTTP_NOT_FOUND /* 404 */) { // 404, no well-known data, try direct access to the API // First check the homeserver version return runCatching { @@ -190,8 +190,8 @@ internal class DefaultAuthenticationService @Inject constructor( it }, { - if (it is Failure.OtherServerError - && it.httpCode == HttpsURLConnection.HTTP_NOT_FOUND /* 404 */) { + if (it is Failure.OtherServerError && + it.httpCode == HttpsURLConnection.HTTP_NOT_FOUND /* 404 */) { // It's maybe a Web client url? getWebClientDomainLoginFlowInternal(homeServerConnectionConfig) } else { @@ -225,8 +225,8 @@ internal class DefaultAuthenticationService @Inject constructor( it }, { - if (it is Failure.OtherServerError - && it.httpCode == HttpsURLConnection.HTTP_NOT_FOUND /* 404 */) { + if (it is Failure.OtherServerError && + it.httpCode == HttpsURLConnection.HTTP_NOT_FOUND /* 404 */) { // Try with config.json getWebClientLoginFlowInternal(homeServerConnectionConfig) } else { @@ -388,8 +388,15 @@ internal class DefaultAuthenticationService @Inject constructor( override suspend fun directAuthentication(homeServerConnectionConfig: HomeServerConnectionConfig, matrixId: String, password: String, - initialDeviceName: String): Session { - return directLoginTask.execute(DirectLoginTask.Params(homeServerConnectionConfig, matrixId, password, initialDeviceName)) + initialDeviceName: String, + deviceId: String?): Session { + return directLoginTask.execute(DirectLoginTask.Params( + homeServerConnectionConfig = homeServerConnectionConfig, + userId = matrixId, + password = password, + deviceName = initialDeviceName, + deviceId = deviceId + )) } private fun buildAuthAPI(homeServerConnectionConfig: HomeServerConnectionConfig): AuthAPI { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/IsValidClientServerApiTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/IsValidClientServerApiTask.kt index bc3d887000..94b301649f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/IsValidClientServerApiTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/IsValidClientServerApiTask.kt @@ -54,8 +54,8 @@ internal class DefaultIsValidClientServerApiTask @Inject constructor( // We get a response, so the API is valid true } catch (failure: Throwable) { - if (failure is Failure.OtherServerError - && failure.httpCode == HttpsURLConnection.HTTP_NOT_FOUND /* 404 */) { + if (failure is Failure.OtherServerError && + failure.httpCode == HttpsURLConnection.HTTP_NOT_FOUND /* 404 */) { // Probably not valid false } else { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/PasswordLoginParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/PasswordLoginParams.kt index d4b14f1ca9..5be480f633 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/PasswordLoginParams.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/PasswordLoginParams.kt @@ -49,51 +49,54 @@ internal data class PasswordLoginParams( fun userIdentifier(user: String, password: String, - deviceDisplayName: String? = null, - deviceId: String? = null): PasswordLoginParams { + deviceDisplayName: String?, + deviceId: String?): PasswordLoginParams { return PasswordLoginParams( - mapOf( + identifier = mapOf( IDENTIFIER_KEY_TYPE to IDENTIFIER_KEY_TYPE_USER, IDENTIFIER_KEY_USER to user ), - password, - LoginFlowTypes.PASSWORD, - deviceDisplayName, - deviceId) + password = password, + type = LoginFlowTypes.PASSWORD, + deviceDisplayName = deviceDisplayName, + deviceId = deviceId + ) } fun thirdPartyIdentifier(medium: String, address: String, password: String, - deviceDisplayName: String? = null, - deviceId: String? = null): PasswordLoginParams { + deviceDisplayName: String?, + deviceId: String?): PasswordLoginParams { return PasswordLoginParams( - mapOf( + identifier = mapOf( IDENTIFIER_KEY_TYPE to IDENTIFIER_KEY_TYPE_THIRD_PARTY, IDENTIFIER_KEY_MEDIUM to medium, IDENTIFIER_KEY_ADDRESS to address ), - password, - LoginFlowTypes.PASSWORD, - deviceDisplayName, - deviceId) + password = password, + type = LoginFlowTypes.PASSWORD, + deviceDisplayName = deviceDisplayName, + deviceId = deviceId + ) } fun phoneIdentifier(country: String, phone: String, password: String, - deviceDisplayName: String? = null, - deviceId: String? = null): PasswordLoginParams { + deviceDisplayName: String?, + deviceId: String?): PasswordLoginParams { return PasswordLoginParams( - mapOf( + identifier = mapOf( IDENTIFIER_KEY_TYPE to IDENTIFIER_KEY_TYPE_PHONE, IDENTIFIER_KEY_COUNTRY to country, IDENTIFIER_KEY_PHONE to phone ), - password, - LoginFlowTypes.PASSWORD, - deviceDisplayName, - deviceId) + password = password, + type = LoginFlowTypes.PASSWORD, + deviceDisplayName = deviceDisplayName, + deviceId = deviceId + ) } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/RealmPendingSessionStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/RealmPendingSessionStore.kt index 968ae22eda..13f26e321d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/RealmPendingSessionStore.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/RealmPendingSessionStore.kt @@ -16,11 +16,11 @@ package org.matrix.android.sdk.internal.auth.db +import io.realm.Realm +import io.realm.RealmConfiguration import org.matrix.android.sdk.internal.auth.PendingSessionStore import org.matrix.android.sdk.internal.database.awaitTransaction import org.matrix.android.sdk.internal.di.AuthDatabase -import io.realm.Realm -import io.realm.RealmConfiguration import javax.inject.Inject internal class RealmPendingSessionStore @Inject constructor(private val mapper: PendingSessionMapper, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/RealmSessionParamsStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/RealmSessionParamsStore.kt index edd3e2bed5..235ef6b709 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/RealmSessionParamsStore.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/RealmSessionParamsStore.kt @@ -16,15 +16,15 @@ package org.matrix.android.sdk.internal.auth.db +import io.realm.Realm +import io.realm.RealmConfiguration +import io.realm.exceptions.RealmPrimaryKeyConstraintException import org.matrix.android.sdk.api.auth.data.Credentials import org.matrix.android.sdk.api.auth.data.SessionParams import org.matrix.android.sdk.api.auth.data.sessionId import org.matrix.android.sdk.internal.auth.SessionParamsStore import org.matrix.android.sdk.internal.database.awaitTransaction import org.matrix.android.sdk.internal.di.AuthDatabase -import io.realm.Realm -import io.realm.RealmConfiguration -import io.realm.exceptions.RealmPrimaryKeyConstraintException import timber.log.Timber import javax.inject.Inject diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DefaultLoginWizard.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DefaultLoginWizard.kt index 854caf8a62..b72cff3cf1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DefaultLoginWizard.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DefaultLoginWizard.kt @@ -21,6 +21,7 @@ import org.matrix.android.sdk.api.auth.login.LoginProfileInfo import org.matrix.android.sdk.api.auth.login.LoginWizard import org.matrix.android.sdk.api.auth.registration.RegisterThreePid import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.util.JsonDict import org.matrix.android.sdk.internal.auth.AuthAPI import org.matrix.android.sdk.internal.auth.PendingSessionStore import org.matrix.android.sdk.internal.auth.SessionCreator @@ -52,11 +53,23 @@ internal class DefaultLoginWizard( override suspend fun login(login: String, password: String, - deviceName: String): Session { + initialDeviceName: String, + deviceId: String?): Session { val loginParams = if (Patterns.EMAIL_ADDRESS.matcher(login).matches()) { - PasswordLoginParams.thirdPartyIdentifier(ThreePidMedium.EMAIL, login, password, deviceName) + PasswordLoginParams.thirdPartyIdentifier( + medium = ThreePidMedium.EMAIL, + address = login, + password = password, + deviceDisplayName = initialDeviceName, + deviceId = deviceId + ) } else { - PasswordLoginParams.userIdentifier(login, password, deviceName) + PasswordLoginParams.userIdentifier( + user = login, + password = password, + deviceDisplayName = initialDeviceName, + deviceId = deviceId + ) } val credentials = executeRequest(null) { authAPI.login(loginParams) @@ -79,6 +92,14 @@ internal class DefaultLoginWizard( return sessionCreator.createSession(credentials, pendingSessionData.homeServerConnectionConfig) } + override suspend fun loginCustom(data: JsonDict): Session { + val credentials = executeRequest(null) { + authAPI.login(data) + } + + return sessionCreator.createSession(credentials, pendingSessionData.homeServerConnectionConfig) + } + override suspend fun resetPassword(email: String, newPassword: String) { val param = RegisterAddThreePidTask.Params( RegisterThreePid.Email(email), diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DirectLoginTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DirectLoginTask.kt index 3888633723..28706c7e80 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DirectLoginTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DirectLoginTask.kt @@ -17,6 +17,7 @@ package org.matrix.android.sdk.internal.auth.login import dagger.Lazy +import okhttp3.OkHttpClient import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.session.Session @@ -29,7 +30,6 @@ import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.network.httpclient.addSocketFactory import org.matrix.android.sdk.internal.network.ssl.UnrecognizedCertificateException import org.matrix.android.sdk.internal.task.Task -import okhttp3.OkHttpClient import javax.inject.Inject internal interface DirectLoginTask : Task { @@ -37,7 +37,8 @@ internal interface DirectLoginTask : Task { val homeServerConnectionConfig: HomeServerConnectionConfig, val userId: String, val password: String, - val deviceName: String + val deviceName: String, + val deviceId: String? ) } @@ -55,7 +56,12 @@ internal class DefaultDirectLoginTask @Inject constructor( val authAPI = retrofitFactory.create(client, homeServerUrl) .create(AuthAPI::class.java) - val loginParams = PasswordLoginParams.userIdentifier(params.userId, params.password, params.deviceName) + val loginParams = PasswordLoginParams.userIdentifier( + user = params.userId, + password = params.password, + deviceDisplayName = params.deviceName, + deviceId = params.deviceId + ) val credentials = try { executeRequest(null) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt index 4e599516ed..74cb3de2ac 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt @@ -63,9 +63,9 @@ internal fun Versions.isSupportedBySdk(): Boolean { * Return true if the SDK supports this homeserver version for login and registration */ internal fun Versions.isLoginAndRegistrationSupportedBySdk(): Boolean { - return !doesServerRequireIdentityServerParam() - && doesServerAcceptIdentityAccessToken() - && doesServerSeparatesAddAndBind() + return !doesServerRequireIdentityServerParam() && + doesServerAcceptIdentityAccessToken() && + doesServerSeparatesAddAndBind() } /** @@ -74,8 +74,8 @@ internal fun Versions.isLoginAndRegistrationSupportedBySdk(): Boolean { * @return true if the server support the lazy loading of room members */ private fun Versions.supportLazyLoadMembers(): Boolean { - return getMaxVersion() >= HomeServerVersion.r0_5_0 - || unstableFeatures?.get(FEATURE_LAZY_LOAD_MEMBERS) == true + return getMaxVersion() >= HomeServerVersion.r0_5_0 || + unstableFeatures?.get(FEATURE_LAZY_LOAD_MEMBERS) == true } /** @@ -92,13 +92,13 @@ private fun Versions.doesServerRequireIdentityServerParam(): Boolean { * Some homeservers may trigger errors if they are not prepared for the new parameter. */ private fun Versions.doesServerAcceptIdentityAccessToken(): Boolean { - return getMaxVersion() >= HomeServerVersion.r0_6_0 - || unstableFeatures?.get(FEATURE_ID_ACCESS_TOKEN) ?: false + return getMaxVersion() >= HomeServerVersion.r0_6_0 || + unstableFeatures?.get(FEATURE_ID_ACCESS_TOKEN) ?: false } private fun Versions.doesServerSeparatesAddAndBind(): Boolean { - return getMaxVersion() >= HomeServerVersion.r0_6_0 - || unstableFeatures?.get(FEATURE_SEPARATE_ADD_AND_BIND) ?: false + return getMaxVersion() >= HomeServerVersion.r0_6_0 || + unstableFeatures?.get(FEATURE_SEPARATE_ADD_AND_BIND) ?: false } private fun Versions.getMaxVersion(): HomeServerVersion { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CancelGossipRequestWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CancelGossipRequestWorker.kt index 0ec020bc46..c11d00278b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CancelGossipRequestWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CancelGossipRequestWorker.kt @@ -35,8 +35,8 @@ import org.matrix.android.sdk.internal.worker.SessionWorkerParams import javax.inject.Inject internal class CancelGossipRequestWorker(context: Context, - params: WorkerParameters) - : SessionSafeCoroutineWorker(context, params, Params::class.java) { + params: WorkerParameters) : + SessionSafeCoroutineWorker(context, params, Params::class.java) { @JsonClass(generateAdapter = true) internal data class Params( diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoModule.kt index 84d4fef5af..fe388b44e2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoModule.kt @@ -19,6 +19,9 @@ package org.matrix.android.sdk.internal.crypto import dagger.Binds import dagger.Module import dagger.Provides +import io.realm.RealmConfiguration +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob import org.matrix.android.sdk.api.session.crypto.CryptoService import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService import org.matrix.android.sdk.internal.crypto.api.CryptoApi @@ -93,9 +96,6 @@ import org.matrix.android.sdk.internal.di.UserMd5 import org.matrix.android.sdk.internal.session.SessionScope import org.matrix.android.sdk.internal.session.cache.ClearCacheTask import org.matrix.android.sdk.internal.session.cache.RealmClearCacheTask -import io.realm.RealmConfiguration -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.SupervisorJob import retrofit2.Retrofit import java.io.File diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt index 563c890950..7b96148e2e 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt @@ -29,6 +29,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.NoOpMatrixCallback import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor import org.matrix.android.sdk.api.crypto.MXCryptoConfig @@ -50,6 +51,7 @@ import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibilityContent import org.matrix.android.sdk.api.session.room.model.RoomMemberContent +import org.matrix.android.sdk.api.session.sync.model.SyncResponse import org.matrix.android.sdk.internal.crypto.actions.MegolmSessionDataImporter import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction import org.matrix.android.sdk.internal.crypto.algorithms.IMXEncrypting @@ -87,13 +89,11 @@ import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.extensions.foldToCallback import org.matrix.android.sdk.internal.session.SessionScope import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask -import org.matrix.android.sdk.internal.session.sync.model.SyncResponse import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.task.TaskThread import org.matrix.android.sdk.internal.task.configureWith import org.matrix.android.sdk.internal.task.launchToCallback import org.matrix.android.sdk.internal.util.JsonCanonicalizer -import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers import org.matrix.olm.OlmManager import timber.log.Timber import java.util.concurrent.atomic.AtomicBoolean @@ -868,8 +868,8 @@ internal class DefaultCryptoService @Inject constructor( } private fun getRoomUserIds(roomId: String): List { - val encryptForInvitedMembers = isEncryptionEnabledForInvitedUser() - && shouldEncryptForInvitedMembers(roomId) + val encryptForInvitedMembers = isEncryptionEnabledForInvitedUser() && + shouldEncryptForInvitedMembers(roomId) return cryptoSessionInfoProvider.getRoomUserIds(roomId, encryptForInvitedMembers) } @@ -887,9 +887,9 @@ internal class DefaultCryptoService @Inject constructor( if (membership == Membership.JOIN) { // make sure we are tracking the deviceList for this user. deviceListManager.startTrackingDeviceList(listOf(userId)) - } else if (membership == Membership.INVITE - && shouldEncryptForInvitedMembers(roomId) - && isEncryptionEnabledForInvitedUser()) { + } else if (membership == Membership.INVITE && + shouldEncryptForInvitedMembers(roomId) && + isEncryptionEnabledForInvitedUser()) { // track the deviceList for this invited user. // Caution: there's a big edge case here in that federated servers do not // know what other servers are in the room at the time they've been invited. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt index 79910c6de2..494e6d7cc7 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt @@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.crypto import kotlinx.coroutines.CancellationException import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.MatrixPatterns import org.matrix.android.sdk.api.auth.data.Credentials import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustLevel @@ -29,7 +30,6 @@ import org.matrix.android.sdk.internal.crypto.tasks.DownloadKeysForUsersTask import org.matrix.android.sdk.internal.session.SessionScope import org.matrix.android.sdk.internal.session.sync.SyncTokenStore import org.matrix.android.sdk.internal.task.TaskExecutor -import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers import org.matrix.android.sdk.internal.util.logLimit import timber.log.Timber import javax.inject.Inject @@ -475,8 +475,8 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM } if (!isVerified) { - Timber.e("## CRYPTO | validateDeviceKeys() : Unable to verify signature on device " + userId + ":" - + deviceKeys.deviceId + " with error " + errorMessage) + Timber.e("## CRYPTO | validateDeviceKeys() : Unable to verify signature on device " + userId + ":" + + deviceKeys.deviceId + " with error " + errorMessage) return false } @@ -486,9 +486,9 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM // best off sticking with the original keys. // // Should we warn the user about it somehow? - Timber.e("## CRYPTO | validateDeviceKeys() : WARNING:Ed25519 key for device " + userId + ":" - + deviceKeys.deviceId + " has changed : " - + previouslyStoredDeviceKeys.fingerprint() + " -> " + signKey) + Timber.e("## CRYPTO | validateDeviceKeys() : WARNING:Ed25519 key for device " + userId + ":" + + deviceKeys.deviceId + " has changed : " + + previouslyStoredDeviceKeys.fingerprint() + " -> " + signKey) Timber.e("## CRYPTO | validateDeviceKeys() : $previouslyStoredDeviceKeys -> $deviceKeys") Timber.e("## CRYPTO | validateDeviceKeys() : ${previouslyStoredDeviceKeys.keys} -> ${deviceKeys.keys}") diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt index 8d86380e39..57381eacfb 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt @@ -20,6 +20,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType @@ -27,16 +28,17 @@ import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo +import org.matrix.android.sdk.internal.crypto.model.MXOlmSessionResult import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap import org.matrix.android.sdk.internal.crypto.model.event.OlmEventContent import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask import org.matrix.android.sdk.internal.extensions.foldToCallback import org.matrix.android.sdk.internal.session.SessionScope -import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers import timber.log.Timber import javax.inject.Inject -import kotlin.jvm.Throws + +private const val SEND_TO_DEVICE_RETRY_COUNT = 3 @SessionScope internal class EventDecryptor @Inject constructor( @@ -107,8 +109,8 @@ internal class EventDecryptor @Inject constructor( } catch (mxCryptoError: MXCryptoError) { Timber.v("## CRYPTO | internalDecryptEvent : Failed to decrypt ${event.eventId} reason: $mxCryptoError") if (algorithm == MXCRYPTO_ALGORITHM_OLM) { - if (mxCryptoError is MXCryptoError.Base - && mxCryptoError.errorType == MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE) { + if (mxCryptoError is MXCryptoError.Base && + mxCryptoError.errorType == MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE) { // need to find sending device cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { val olmContent = event.content.toModel() @@ -146,29 +148,36 @@ internal class EventDecryptor @Inject constructor( // offload this from crypto thread (?) cryptoCoroutineScope.launch(coroutineDispatchers.computation) { - val ensured = ensureOlmSessionsForDevicesAction.handle(mapOf(senderId to listOf(deviceInfo)), force = true) + runCatching { ensureOlmSessionsForDevicesAction.handle(mapOf(senderId to listOf(deviceInfo)), force = true) }.fold( + onSuccess = { sendDummyToDevice(ensured = it, deviceInfo, senderId) }, + onFailure = { + Timber.e("## CRYPTO | markOlmSessionForUnwedging() : failed to ensure device info ${senderId}${deviceInfo.deviceId}") + } + ) + } + } - Timber.i("## CRYPTO | markOlmSessionForUnwedging() : ensureOlmSessionsForDevicesAction isEmpty:${ensured.isEmpty}") + private suspend fun sendDummyToDevice(ensured: MXUsersDevicesMap, deviceInfo: CryptoDeviceInfo, senderId: String) { + Timber.i("## CRYPTO | markOlmSessionForUnwedging() : ensureOlmSessionsForDevicesAction isEmpty:${ensured.isEmpty}") - // Now send a blank message on that session so the other side knows about it. - // (The keyshare request is sent in the clear so that won't do) - // We send this first such that, as long as the toDevice messages arrive in the - // same order we sent them, the other end will get this first, set up the new session, - // then get the keyshare request and send the key over this new session (because it - // is the session it has most recently received a message on). - val payloadJson = mapOf("type" to EventType.DUMMY) + // Now send a blank message on that session so the other side knows about it. + // (The keyshare request is sent in the clear so that won't do) + // We send this first such that, as long as the toDevice messages arrive in the + // same order we sent them, the other end will get this first, set up the new session, + // then get the keyshare request and send the key over this new session (because it + // is the session it has most recently received a message on). + val payloadJson = mapOf("type" to EventType.DUMMY) - val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo)) - val sendToDeviceMap = MXUsersDevicesMap() - sendToDeviceMap.setObject(senderId, deviceInfo.deviceId, encodedPayload) - Timber.i("## CRYPTO | markOlmSessionForUnwedging() : sending dummy to $senderId:${deviceInfo.deviceId}") - withContext(coroutineDispatchers.io) { - val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap) - try { - sendToDeviceTask.execute(sendToDeviceParams) - } catch (failure: Throwable) { - Timber.e(failure, "## CRYPTO | markOlmSessionForUnwedging() : failed to send dummy to $senderId:${deviceInfo.deviceId}") - } + val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo)) + val sendToDeviceMap = MXUsersDevicesMap() + sendToDeviceMap.setObject(senderId, deviceInfo.deviceId, encodedPayload) + Timber.i("## CRYPTO | markOlmSessionForUnwedging() : sending dummy to $senderId:${deviceInfo.deviceId}") + withContext(coroutineDispatchers.io) { + val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap) + try { + sendToDeviceTask.executeRetry(sendToDeviceParams, remainingRetry = SEND_TO_DEVICE_RETRY_COUNT) + } catch (failure: Throwable) { + Timber.e(failure, "## CRYPTO | markOlmSessionForUnwedging() : failed to send dummy to $senderId:${deviceInfo.deviceId}") } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt index 06c667ee4a..e7a46750b0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt @@ -19,10 +19,10 @@ package org.matrix.android.sdk.internal.crypto import android.util.LruCache import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2 import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers import timber.log.Timber import java.util.Timer import java.util.TimerTask @@ -71,18 +71,24 @@ internal class InboundGroupSessionStore @Inject constructor( } @Synchronized - fun storeInBoundGroupSession(wrapper: OlmInboundGroupSessionWrapper2) { + fun storeInBoundGroupSession(wrapper: OlmInboundGroupSessionWrapper2, sessionId: String, senderKey: String) { Timber.v("## Inbound: getInboundGroupSession mark as dirty ${wrapper.roomId}-${wrapper.senderKey}") // We want to batch this a bit for performances dirtySession.add(wrapper) + if (sessionCache[CacheKey(sessionId, senderKey)] == null) { + // first time seen, put it in memory cache while waiting for batch insert + // If it's already known, no need to update cache it's already there + sessionCache.put(CacheKey(sessionId, senderKey), wrapper) + } + timerTask?.cancel() timerTask = object : TimerTask() { override fun run() { batchSave() } } - timer.schedule(timerTask!!, 2_000) + timer.schedule(timerTask!!, 300) } @Synchronized diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingGossipingRequestManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingGossipingRequestManager.kt index e8640d5011..220f25ec80 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingGossipingRequestManager.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingGossipingRequestManager.kt @@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.crypto import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.auth.data.Credentials import org.matrix.android.sdk.api.crypto.MXCryptoConfig import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME @@ -38,7 +39,6 @@ import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.crypto.tasks.createUniqueTxnId import org.matrix.android.sdk.internal.di.SessionId import org.matrix.android.sdk.internal.session.SessionScope -import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers import org.matrix.android.sdk.internal.worker.WorkerParamsFactory import timber.log.Timber import java.util.concurrent.Executors diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt index b8f1a9abea..441dfe4a5d 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt @@ -577,7 +577,8 @@ internal class MXOlmDevice @Inject constructor( session.keysClaimed = keysClaimed session.forwardingCurve25519KeyChain = forwardingCurve25519KeyChain - store.storeInboundGroupSessions(listOf(session)) + inboundGroupSessionStore.storeInBoundGroupSession(session, sessionId, senderKey) +// store.storeInboundGroupSessions(listOf(session)) return true } @@ -703,7 +704,7 @@ internal class MXOlmDevice @Inject constructor( timelineSet.add(messageIndexKey) } - inboundGroupSessionStore.storeInBoundGroupSession(session) + inboundGroupSessionStore.storeInBoundGroupSession(session, sessionId, senderKey) val payload = try { val adapter = MoshiProvider.providesMoshi().adapter(JSON_DICT_PARAMETERIZED_TYPE) val payloadString = convertFromUTF8(decryptResult.mDecryptedMessage) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingGossipingRequestManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingGossipingRequestManager.kt index ccdb5ab137..fd60e43260 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingGossipingRequestManager.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingGossipingRequestManager.kt @@ -16,17 +16,17 @@ package org.matrix.android.sdk.internal.crypto -import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyRequestBody -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import org.matrix.android.sdk.internal.di.SessionId -import org.matrix.android.sdk.internal.session.SessionScope -import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers -import org.matrix.android.sdk.internal.worker.WorkerParamsFactory import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers +import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyRequestBody +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.crypto.tasks.createUniqueTxnId import org.matrix.android.sdk.internal.crypto.util.RequestIdHelper +import org.matrix.android.sdk.internal.di.SessionId +import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.worker.WorkerParamsFactory import timber.log.Timber import javax.inject.Inject @@ -112,9 +112,8 @@ internal class OutgoingGossipingRequestManager @Inject constructor( * @param andResend true to resend the key request */ private fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody, andResend: Boolean) { - val req = cryptoStore.getOutgoingRoomKeyRequest(requestBody) - ?: // no request was made for this key - return Unit.also { + val req = cryptoStore.getOutgoingRoomKeyRequest(requestBody) // no request was made for this key + ?: return Unit.also { Timber.v("## CRYPTO - GOSSIP cancelRoomKeyRequest() Unknown request $requestBody") } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SendGossipRequestWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SendGossipRequestWorker.kt index 2e26720abb..b2ba189b65 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SendGossipRequestWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SendGossipRequestWorker.kt @@ -38,8 +38,8 @@ import timber.log.Timber import javax.inject.Inject internal class SendGossipRequestWorker(context: Context, - params: WorkerParameters) - : SessionSafeCoroutineWorker(context, params, Params::class.java) { + params: WorkerParameters) : + SessionSafeCoroutineWorker(context, params, Params::class.java) { @JsonClass(generateAdapter = true) internal data class Params( diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SendGossipWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SendGossipWorker.kt index c5c6d26f79..b96943e4ae 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SendGossipWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SendGossipWorker.kt @@ -38,8 +38,8 @@ import timber.log.Timber import javax.inject.Inject internal class SendGossipWorker(context: Context, - params: WorkerParameters) - : SessionSafeCoroutineWorker(context, params, Params::class.java) { + params: WorkerParameters) : + SessionSafeCoroutineWorker(context, params, Params::class.java) { @JsonClass(generateAdapter = true) internal data class Params( diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/EnsureOlmSessionsForDevicesAction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/EnsureOlmSessionsForDevicesAction.kt index 95b99c54e8..3979ff1fb4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/EnsureOlmSessionsForDevicesAction.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/EnsureOlmSessionsForDevicesAction.kt @@ -25,6 +25,8 @@ import org.matrix.android.sdk.internal.crypto.tasks.ClaimOneTimeKeysForUsersDevi import timber.log.Timber import javax.inject.Inject +private const val ONE_TIME_KEYS_RETRY_COUNT = 3 + internal class EnsureOlmSessionsForDevicesAction @Inject constructor( private val olmDevice: MXOlmDevice, private val oneTimeKeysForUsersDeviceTask: ClaimOneTimeKeysForUsersDeviceTask) { @@ -72,7 +74,7 @@ internal class EnsureOlmSessionsForDevicesAction @Inject constructor( Timber.i("## CRYPTO | claimOneTimeKeysForUsersDevices() : $usersDevicesToClaim") val claimParams = ClaimOneTimeKeysForUsersDeviceTask.Params(usersDevicesToClaim) - val oneTimeKeys = oneTimeKeysForUsersDeviceTask.execute(claimParams) + val oneTimeKeys = oneTimeKeysForUsersDeviceTask.executeRetry(claimParams, remainingRetry = ONE_TIME_KEYS_RETRY_COUNT) Timber.v("## CRYPTO | claimOneTimeKeysForUsersDevices() : keysClaimResponse.oneTimeKeys: $oneTimeKeys") for ((userId, deviceInfos) in devicesByUser) { for (deviceInfo in deviceInfos) { @@ -90,8 +92,8 @@ internal class EnsureOlmSessionsForDevicesAction @Inject constructor( oneTimeKey = key } if (oneTimeKey == null) { - Timber.w("## CRYPTO | ensureOlmSessionsForDevices() : No one-time keys " + oneTimeKeyAlgorithm - + " for device " + userId + " : " + deviceId) + Timber.w("## CRYPTO | ensureOlmSessionsForDevices() : No one-time keys " + oneTimeKeyAlgorithm + + " for device " + userId + " : " + deviceId) continue } // Update the result for this device in results @@ -126,15 +128,15 @@ internal class EnsureOlmSessionsForDevicesAction @Inject constructor( sessionId = olmDevice.createOutboundSession(deviceInfo.identityKey()!!, oneTimeKey.value) if (!sessionId.isNullOrEmpty()) { - Timber.v("## CRYPTO | verifyKeyAndStartSession() : Started new sessionid " + sessionId - + " for device " + deviceInfo + "(theirOneTimeKey: " + oneTimeKey.value + ")") + Timber.v("## CRYPTO | verifyKeyAndStartSession() : Started new sessionid " + sessionId + + " for device " + deviceInfo + "(theirOneTimeKey: " + oneTimeKey.value + ")") } else { // Possibly a bad key Timber.e("## CRYPTO | verifyKeyAndStartSession() : Error starting session with device $userId:$deviceId") } } else { - Timber.e("## CRYPTO | verifyKeyAndStartSession() : Unable to verify signature on one-time key for device " + userId - + ":" + deviceId + " Error " + errorMessage) + Timber.e("## CRYPTO | verifyKeyAndStartSession() : Unable to verify signature on one-time key for device " + userId + + ":" + deviceId + " Error " + errorMessage) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/EnsureOlmSessionsForUsersAction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/EnsureOlmSessionsForUsersAction.kt index a276394eaf..a3cfbd91f0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/EnsureOlmSessionsForUsersAction.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/EnsureOlmSessionsForUsersAction.kt @@ -38,9 +38,9 @@ internal class EnsureOlmSessionsForUsersAction @Inject constructor(private val o devices.filter { // Don't bother setting up session to ourself - it.identityKey() != olmDevice.deviceCurve25519Key + it.identityKey() != olmDevice.deviceCurve25519Key && // Don't bother setting up sessions with blocked users - && !(it.trustLevel?.isVerified() ?: false) + !(it.trustLevel?.isVerified() ?: false) } } return ensureOlmSessionsForDevicesAction.handle(devicesByUser) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt index 70d2022299..d7411ad0be 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt @@ -16,6 +16,9 @@ package org.matrix.android.sdk.internal.crypto.algorithms.megolm +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType @@ -39,9 +42,6 @@ import org.matrix.android.sdk.internal.crypto.model.rest.ForwardedRoomKeyContent import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyRequestBody import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask -import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch import timber.log.Timber internal class MXMegolmDecryption(private val userId: String, @@ -82,9 +82,9 @@ internal class MXMegolmDecryption(private val userId: String, val encryptedEventContent = event.content.toModel() ?: throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON) - if (encryptedEventContent.senderKey.isNullOrBlank() - || encryptedEventContent.sessionId.isNullOrBlank() - || encryptedEventContent.ciphertext.isNullOrBlank()) { + if (encryptedEventContent.senderKey.isNullOrBlank() || + encryptedEventContent.sessionId.isNullOrBlank() || + encryptedEventContent.ciphertext.isNullOrBlank()) { throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt index 95a4342dbf..29f9d193f8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt @@ -16,6 +16,8 @@ package org.matrix.android.sdk.internal.crypto.algorithms.megolm +import kotlinx.coroutines.CoroutineScope +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.internal.crypto.DeviceListManager import org.matrix.android.sdk.internal.crypto.MXOlmDevice import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager @@ -24,8 +26,6 @@ import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask import org.matrix.android.sdk.internal.di.UserId -import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers -import kotlinx.coroutines.CoroutineScope import javax.inject.Inject internal class MXMegolmDecryptionFactory @Inject constructor( diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt index 540280d8a2..031bb4e194 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt @@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.crypto.algorithms.megolm import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.events.model.Content import org.matrix.android.sdk.api.session.events.model.Event @@ -39,7 +40,6 @@ import org.matrix.android.sdk.internal.crypto.repository.WarnOnUnknownDeviceRepo import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask import org.matrix.android.sdk.internal.util.JsonCanonicalizer -import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers import org.matrix.android.sdk.internal.util.convertToUTF8 import timber.log.Timber @@ -155,11 +155,11 @@ internal class MXMegolmEncryption( private suspend fun ensureOutboundSession(devicesInRoom: MXUsersDevicesMap): MXOutboundSessionInfo { Timber.v("## CRYPTO | ensureOutboundSession start") var session = outboundSession - if (session == null + if (session == null || // Need to make a brand new session? - || session.needsRotation(sessionRotationPeriodMsgs, sessionRotationPeriodMs) + session.needsRotation(sessionRotationPeriodMsgs, sessionRotationPeriodMs) || // Determine if we have shared with anyone we shouldn't have - || session.sharedWithTooManyDevices(devicesInRoom)) { + session.sharedWithTooManyDevices(devicesInRoom)) { session = prepareNewSessionInRoom() outboundSession = session } @@ -380,8 +380,8 @@ internal class MXMegolmEncryption( // with them, which means that they will have announced any new devices via // an m.new_device. val keys = deviceListManager.downloadKeys(userIds, false) - val encryptToVerifiedDevicesOnly = cryptoStore.getGlobalBlacklistUnverifiedDevices() - || cryptoStore.getRoomsListBlacklistUnverifiedDevices().contains(roomId) + val encryptToVerifiedDevicesOnly = cryptoStore.getGlobalBlacklistUnverifiedDevices() || + cryptoStore.getRoomsListBlacklistUnverifiedDevices().contains(roomId) val devicesInRoom = DeviceInRoomInfo() val unknownDevices = MXUsersDevicesMap() @@ -446,10 +446,9 @@ internal class MXMegolmEncryption( val devicesByUser = mapOf(userId to listOf(deviceInfo)) val usersDeviceMap = ensureOlmSessionsForDevicesAction.handle(devicesByUser) val olmSessionResult = usersDeviceMap.getObject(userId, deviceId) - olmSessionResult?.sessionId - ?: // no session with this device, probably because there were no one-time keys. + olmSessionResult?.sessionId // no session with this device, probably because there were no one-time keys. // ensureOlmSessionsForDevicesAction has already done the logging, so just skip it. - return false.also { + ?: return false.also { Timber.w("## Crypto reshareKey: no session with this device, probably because there were no one-time keys") } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryptionFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryptionFactory.kt index 9f6312ea97..238d7eed88 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryptionFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryptionFactory.kt @@ -17,6 +17,7 @@ package org.matrix.android.sdk.internal.crypto.algorithms.megolm import kotlinx.coroutines.CoroutineScope +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.internal.crypto.DeviceListManager import org.matrix.android.sdk.internal.crypto.MXOlmDevice import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction @@ -27,7 +28,6 @@ import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask import org.matrix.android.sdk.internal.di.DeviceId import org.matrix.android.sdk.internal.di.UserId -import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers import javax.inject.Inject internal class MXMegolmEncryptionFactory @Inject constructor( diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryption.kt index 082b86c9da..f1bca4fbc6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryption.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryption.kt @@ -34,8 +34,8 @@ internal class MXOlmDecryption( // The olm device interface private val olmDevice: MXOlmDevice, // the matrix userId - private val userId: String) - : IMXDecrypting { + private val userId: String) : + IMXDecrypting { @Throws(MXCryptoError::class) override fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmEncryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmEncryption.kt index 65f78e11f0..63f2533ac3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmEncryption.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmEncryption.kt @@ -32,8 +32,8 @@ internal class MXOlmEncryption( private val cryptoStore: IMXCryptoStore, private val messageEncrypter: MessageEncrypter, private val deviceListManager: DeviceListManager, - private val ensureOlmSessionsForUsersAction: EnsureOlmSessionsForUsersAction) - : IMXEncrypting { + private val ensureOlmSessionsForUsersAction: EnsureOlmSessionsForUsersAction) : + IMXEncrypting { override suspend fun encryptEventContent(eventContent: Content, eventType: String, userIds: List): Content { // pick the list of recipients based on the membership list. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmEncryptionFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmEncryptionFactory.kt index 68a95e395b..50ce2d2bf3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmEncryptionFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmEncryptionFactory.kt @@ -16,12 +16,12 @@ package org.matrix.android.sdk.internal.crypto.algorithms.olm +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.internal.crypto.DeviceListManager import org.matrix.android.sdk.internal.crypto.MXOlmDevice import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForUsersAction import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers import javax.inject.Inject internal class MXOlmEncryptionFactory @Inject constructor(private val olmDevice: MXOlmDevice, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/ElementToDecrypt.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/ElementToDecrypt.kt index c071384df4..3d00e178a0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/ElementToDecrypt.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/ElementToDecrypt.kt @@ -17,8 +17,8 @@ package org.matrix.android.sdk.internal.crypto.attachments import android.os.Parcelable -import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo import kotlinx.parcelize.Parcelize +import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo fun EncryptedFileInfo.toElementToDecrypt(): ElementToDecrypt? { // Check the validity of some fields diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/ComputeTrustTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/ComputeTrustTask.kt index ee5aab977b..b470ab34bb 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/ComputeTrustTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/ComputeTrustTask.kt @@ -15,14 +15,14 @@ */ package org.matrix.android.sdk.internal.crypto.crosssigning +import kotlinx.coroutines.withContext +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.task.Task -import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers -import kotlinx.coroutines.withContext import javax.inject.Inject internal interface ComputeTrustTask : Task { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DefaultCrossSigningService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DefaultCrossSigningService.kt index 0289fadbd8..83de06a668 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DefaultCrossSigningService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DefaultCrossSigningService.kt @@ -22,6 +22,7 @@ import androidx.work.ExistingWorkPolicy import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService @@ -42,7 +43,6 @@ import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.task.TaskThread import org.matrix.android.sdk.internal.task.configureWith import org.matrix.android.sdk.internal.util.JsonCanonicalizer -import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers import org.matrix.android.sdk.internal.util.logLimit import org.matrix.android.sdk.internal.worker.WorkerParamsFactory import org.matrix.olm.OlmPkSigning @@ -529,13 +529,13 @@ internal class DefaultCrossSigningService @Inject constructor( } override fun canCrossSign(): Boolean { - return checkSelfTrust().isVerified() && cryptoStore.getCrossSigningPrivateKeys()?.selfSigned != null - && cryptoStore.getCrossSigningPrivateKeys()?.user != null + return checkSelfTrust().isVerified() && cryptoStore.getCrossSigningPrivateKeys()?.selfSigned != null && + cryptoStore.getCrossSigningPrivateKeys()?.user != null } override fun allPrivateKeysKnown(): Boolean { - return checkSelfTrust().isVerified() - && cryptoStore.getCrossSigningPrivateKeys()?.allKnown().orFalse() + return checkSelfTrust().isVerified() && + cryptoStore.getCrossSigningPrivateKeys()?.allKnown().orFalse() } override fun trustUser(otherUserId: String, callback: MatrixCallback) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt index 76b63b7798..3326d3707a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt @@ -51,8 +51,8 @@ import timber.log.Timber import javax.inject.Inject internal class UpdateTrustWorker(context: Context, - params: WorkerParameters) - : SessionSafeCoroutineWorker(context, params, Params::class.java) { + params: WorkerParameters) : + SessionSafeCoroutineWorker(context, params, Params::class.java) { @JsonClass(generateAdapter = true) internal data class Params( diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt index fbcf5cfdeb..b20168eaa3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt @@ -21,7 +21,12 @@ import android.os.Looper import androidx.annotation.UiThread import androidx.annotation.VisibleForTesting import androidx.annotation.WorkerThread +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.auth.data.Credentials import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.MatrixError @@ -40,6 +45,7 @@ import org.matrix.android.sdk.internal.crypto.keysbackup.model.KeysBackupVersion import org.matrix.android.sdk.internal.crypto.keysbackup.model.KeysBackupVersionTrustSignature import org.matrix.android.sdk.internal.crypto.keysbackup.model.MegolmBackupAuthData import org.matrix.android.sdk.internal.crypto.keysbackup.model.MegolmBackupCreationInfo +import org.matrix.android.sdk.internal.crypto.keysbackup.model.SignalableMegolmBackupAuthData import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.BackupKeysResult import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.CreateKeysBackupVersionBody import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeyBackupData @@ -78,13 +84,7 @@ import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.task.TaskThread import org.matrix.android.sdk.internal.task.configureWith import org.matrix.android.sdk.internal.util.JsonCanonicalizer -import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers import org.matrix.android.sdk.internal.util.awaitCallback -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import org.matrix.android.sdk.internal.crypto.keysbackup.model.SignalableMegolmBackupAuthData import org.matrix.olm.OlmException import org.matrix.olm.OlmPkDecryption import org.matrix.olm.OlmPkEncryption @@ -410,7 +410,7 @@ internal class DefaultKeysBackupService @Inject constructor( val keysBackupVersionTrust = KeysBackupVersionTrust() val authData = keysBackupVersion.getAuthDataAsMegolmBackupAuthData() - if (authData == null || authData.publicKey.isEmpty() || authData.signatures.isEmpty()) { + if (authData == null || authData.publicKey.isEmpty() || authData.signatures.isNullOrEmpty()) { Timber.v("getKeysBackupTrust: Key backup is absent or missing required data") return keysBackupVersionTrust } @@ -478,7 +478,7 @@ internal class DefaultKeysBackupService @Inject constructor( cryptoCoroutineScope.launch(coroutineDispatchers.main) { val updateKeysBackupVersionBody = withContext(coroutineDispatchers.crypto) { // Get current signatures, or create an empty set - val myUserSignatures = authData.signatures[userId].orEmpty().toMutableMap() + val myUserSignatures = authData.signatures?.get(userId).orEmpty().toMutableMap() if (trust) { // Add current device signature @@ -497,7 +497,7 @@ internal class DefaultKeysBackupService @Inject constructor( // Create an updated version of KeysVersionResult val newMegolmBackupAuthData = authData.copy() - val newSignatures = newMegolmBackupAuthData.signatures.toMutableMap() + val newSignatures = newMegolmBackupAuthData.signatures.orEmpty().toMutableMap() newSignatures[userId] = myUserSignatures val newMegolmBackupAuthDataWithNewSignature = newMegolmBackupAuthData.copy( @@ -860,8 +860,8 @@ internal class DefaultKeysBackupService @Inject constructor( } override fun onFailure(failure: Throwable) { - if (failure is Failure.ServerError - && failure.error.code == MatrixError.M_NOT_FOUND) { + if (failure is Failure.ServerError && + failure.error.code == MatrixError.M_NOT_FOUND) { // Workaround because the homeserver currently returns M_NOT_FOUND when there is no key backup callback.onSuccess(null) } else { @@ -883,8 +883,8 @@ internal class DefaultKeysBackupService @Inject constructor( } override fun onFailure(failure: Throwable) { - if (failure is Failure.ServerError - && failure.error.code == MatrixError.M_NOT_FOUND) { + if (failure is Failure.ServerError && + failure.error.code == MatrixError.M_NOT_FOUND) { // Workaround because the homeserver currently returns M_NOT_FOUND when there is no key backup callback.onSuccess(null) } else { @@ -1042,8 +1042,8 @@ internal class DefaultKeysBackupService @Inject constructor( return null } - if (authData.privateKeySalt.isNullOrBlank() - || authData.privateKeyIterations == null) { + if (authData.privateKeySalt.isNullOrBlank() || + authData.privateKeyIterations == null) { Timber.w("recoveryKeyFromPassword: Salt and/or iterations not found in key backup auth data") return null diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupStateManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupStateManager.kt index 7c0c741a2c..78ef958bbf 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupStateManager.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupStateManager.kt @@ -44,16 +44,16 @@ internal class KeysBackupStateManager(private val uiHandler: Handler) { } val isEnabled: Boolean - get() = state == KeysBackupState.ReadyToBackUp - || state == KeysBackupState.WillBackUp - || state == KeysBackupState.BackingUp + get() = state == KeysBackupState.ReadyToBackUp || + state == KeysBackupState.WillBackUp || + state == KeysBackupState.BackingUp // True if unknown or bad state val isStucked: Boolean - get() = state == KeysBackupState.Unknown - || state == KeysBackupState.Disabled - || state == KeysBackupState.WrongBackUpVersion - || state == KeysBackupState.NotTrusted + get() = state == KeysBackupState.Unknown || + state == KeysBackupState.Disabled || + state == KeysBackupState.WrongBackUpVersion || + state == KeysBackupState.NotTrusted fun addListener(listener: KeysBackupStateListener) { synchronized(listeners) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/MegolmBackupAuthData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/MegolmBackupAuthData.kt index 54b92546e9..17c895762c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/MegolmBackupAuthData.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/MegolmBackupAuthData.kt @@ -51,7 +51,7 @@ data class MegolmBackupAuthData( * userId -> (deviceSignKeyId -> signature) */ @Json(name = "signatures") - val signatures: Map> + val signatures: Map>? = null ) { fun toJsonDict(): JsonDict { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/secrets/DefaultSharedSecretStorageService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/secrets/DefaultSharedSecretStorageService.kt index fb10cf4482..e6d8b5e84f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/secrets/DefaultSharedSecretStorageService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/secrets/DefaultSharedSecretStorageService.kt @@ -16,6 +16,9 @@ package org.matrix.android.sdk.internal.crypto.secrets +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.withContext +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.listeners.ProgressListener import org.matrix.android.sdk.api.session.accountdata.SessionAccountDataService @@ -42,9 +45,6 @@ import org.matrix.android.sdk.internal.crypto.keysbackup.util.computeRecoveryKey import org.matrix.android.sdk.internal.crypto.tools.HkdfSha256 import org.matrix.android.sdk.internal.crypto.tools.withOlmDecryption import org.matrix.android.sdk.internal.di.UserId -import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.withContext import org.matrix.olm.OlmPkMessage import java.security.SecureRandom import javax.crypto.Cipher @@ -359,8 +359,8 @@ internal class DefaultSharedSecretStorageService @Inject constructor( val keyInfo = (keyInfoResult as? KeyInfoResult.Success)?.keyInfo ?: return IntegrityResult.Error(SharedSecretStorageError.UnknownKey(keyId ?: "")) - if (keyInfo.content.algorithm != SSSS_ALGORITHM_AES_HMAC_SHA2 - && keyInfo.content.algorithm != SSSS_ALGORITHM_CURVE25519_AES_SHA2) { + if (keyInfo.content.algorithm != SSSS_ALGORITHM_AES_HMAC_SHA2 && + keyInfo.content.algorithm != SSSS_ALGORITHM_CURVE25519_AES_SHA2) { // Unsupported algorithm return IntegrityResult.Error( SharedSecretStorageError.UnsupportedAlgorithm(keyInfo.content.algorithm ?: "") diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt index 238d06738c..9b75f88f91 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt @@ -379,7 +379,8 @@ internal interface IMXCryptoStore { fun getOrAddOutgoingSecretShareRequest(secretName: String, recipients: Map>): OutgoingSecretRequest? - fun saveGossipingEvent(event: Event) + fun saveGossipingEvent(event: Event) = saveGossipingEvents(listOf(event)) + fun saveGossipingEvents(events: List) fun updateGossipingRequestState(request: IncomingShareRequestCommon, state: GossipingRequestState) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt index 860bba7404..40678a6ce6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt @@ -25,6 +25,7 @@ import io.realm.Realm import io.realm.RealmConfiguration import io.realm.Sort import io.realm.kotlin.where +import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.room.send.SendState @@ -100,6 +101,8 @@ import org.matrix.olm.OlmAccount import org.matrix.olm.OlmException import org.matrix.olm.OlmOutboundGroupSession import timber.log.Timber +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit import javax.inject.Inject import kotlin.collections.set @@ -137,8 +140,11 @@ internal class RealmCryptoStore @Inject constructor( newSessionListeners.remove(listener) } + private val monarchyWriteAsyncExecutor = Executors.newSingleThreadExecutor() + private val monarchy = Monarchy.Builder() .setRealmConfiguration(realmConfiguration) + .setWriteAsyncExecutor(monarchyWriteAsyncExecutor) .build() init { @@ -152,8 +158,8 @@ internal class RealmCryptoStore @Inject constructor( // Check credentials // The device id may not have been provided in credentials. // Check it only if provided, else trust the stored one. - if (currentMetadata.userId != userId - || (deviceId != null && deviceId != currentMetadata.deviceId)) { + if (currentMetadata.userId != userId || + (deviceId != null && deviceId != currentMetadata.deviceId)) { Timber.w("## open() : Credentials do not match, close this store and delete data") deleteAll = true currentMetadata = null @@ -178,9 +184,9 @@ internal class RealmCryptoStore @Inject constructor( override fun hasData(): Boolean { return doWithRealm(realmConfiguration) { - !it.isEmpty + !it.isEmpty && // Check if there is a MetaData object - && it.where().count() > 0 + it.where().count() > 0 } } @@ -199,6 +205,14 @@ internal class RealmCryptoStore @Inject constructor( } override fun close() { + // Ensure no async request will be run later + val tasks = monarchyWriteAsyncExecutor.shutdownNow() + Timber.w("Closing RealmCryptoStore, ${tasks.size} async task(s) cancelled") + tryOrNull("Interrupted") { + // Wait 1 minute max + monarchyWriteAsyncExecutor.awaitTermination(1, TimeUnit.MINUTES) + } + olmSessionsToRelease.forEach { it.value.olmSession.releaseSession() } @@ -1025,10 +1039,10 @@ internal class RealmCryptoStore @Inject constructor( }.mapNotNull { it.toOutgoingGossipingRequest() as? OutgoingRoomKeyRequest }.firstOrNull { - it.requestBody?.algorithm == requestBody.algorithm - && it.requestBody?.roomId == requestBody.roomId - && it.requestBody?.senderKey == requestBody.senderKey - && it.requestBody?.sessionId == requestBody.sessionId + it.requestBody?.algorithm == requestBody.algorithm && + it.requestBody?.roomId == requestBody.roomId && + it.requestBody?.senderKey == requestBody.senderKey && + it.requestBody?.sessionId == requestBody.sessionId } } @@ -1113,10 +1127,10 @@ internal class RealmCryptoStore @Inject constructor( .mapNotNull { it.toOutgoingGossipingRequest() as? OutgoingRoomKeyRequest }.firstOrNull { - it.requestBody?.algorithm == requestBody.algorithm - && it.requestBody?.sessionId == requestBody.sessionId - && it.requestBody?.senderKey == requestBody.senderKey - && it.requestBody?.roomId == requestBody.roomId + it.requestBody?.algorithm == requestBody.algorithm && + it.requestBody?.sessionId == requestBody.sessionId && + it.requestBody?.senderKey == requestBody.senderKey && + it.requestBody?.roomId == requestBody.roomId } if (existing == null) { @@ -1163,8 +1177,8 @@ internal class RealmCryptoStore @Inject constructor( } override fun saveGossipingEvents(events: List) { - val now = System.currentTimeMillis() monarchy.writeAsync { realm -> + val now = System.currentTimeMillis() events.forEach { event -> val ageLocalTs = event.unsignedData?.age?.let { now - it } ?: now val entity = GossipingEventEntity( @@ -1182,23 +1196,6 @@ internal class RealmCryptoStore @Inject constructor( } } - override fun saveGossipingEvent(event: Event) { - monarchy.writeAsync { realm -> - val now = System.currentTimeMillis() - val ageLocalTs = event.unsignedData?.age?.let { now - it } ?: now - val entity = GossipingEventEntity( - type = event.type, - sender = event.senderId, - ageLocalTs = ageLocalTs, - content = ContentMapper.map(event.content) - ).apply { - sendState = SendState.SYNCED - decryptionResultJson = MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java).toJson(event.mxDecryptionResult) - decryptionErrorCode = event.mCryptoError?.name - } - realm.insertOrUpdate(entity) - } - } // override fun getOutgoingRoomKeyRequestByState(states: Set): OutgoingRoomKeyRequest? { // val statesIndex = states.map { it.ordinal }.toTypedArray() // return doRealmQueryAndCopy(realmConfiguration) { realm -> diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreModule.kt index 6aae68c83e..a2f2f8e97a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreModule.kt @@ -16,6 +16,7 @@ package org.matrix.android.sdk.internal.crypto.store.db +import io.realm.annotations.RealmModule import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntity import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoMetadataEntity import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoRoomEntity @@ -27,13 +28,12 @@ import org.matrix.android.sdk.internal.crypto.store.db.model.KeysBackupDataEntit import org.matrix.android.sdk.internal.crypto.store.db.model.MyDeviceLastSeenInfoEntity import org.matrix.android.sdk.internal.crypto.store.db.model.OlmInboundGroupSessionEntity import org.matrix.android.sdk.internal.crypto.store.db.model.OlmSessionEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.OutboundGroupSessionInfoEntity import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingGossipingRequestEntity import org.matrix.android.sdk.internal.crypto.store.db.model.SharedSessionEntity import org.matrix.android.sdk.internal.crypto.store.db.model.TrustLevelEntity import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntity import org.matrix.android.sdk.internal.crypto.store.db.model.WithHeldSessionEntity -import io.realm.annotations.RealmModule -import org.matrix.android.sdk.internal.crypto.store.db.model.OutboundGroupSessionInfoEntity /** * Realm module for Crypto store classes diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/mapper/CrossSigningKeysMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/mapper/CrossSigningKeysMapper.kt index 9e73985592..c15414a8dd 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/mapper/CrossSigningKeysMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/mapper/CrossSigningKeysMapper.kt @@ -18,10 +18,10 @@ package org.matrix.android.sdk.internal.crypto.store.db.mapper import com.squareup.moshi.Moshi import com.squareup.moshi.Types +import io.realm.RealmList import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustLevel import org.matrix.android.sdk.internal.crypto.model.CryptoCrossSigningKey import org.matrix.android.sdk.internal.crypto.store.db.model.KeyInfoEntity -import io.realm.RealmList import timber.log.Timber import javax.inject.Inject diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoMetadataEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoMetadataEntity.kt index 5166f6c31f..35ae86db8b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoMetadataEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoMetadataEntity.kt @@ -16,10 +16,10 @@ package org.matrix.android.sdk.internal.crypto.store.db.model -import org.matrix.android.sdk.internal.crypto.store.db.deserializeFromRealm -import org.matrix.android.sdk.internal.crypto.store.db.serializeForRealm import io.realm.RealmObject import io.realm.annotations.PrimaryKey +import org.matrix.android.sdk.internal.crypto.store.db.deserializeFromRealm +import org.matrix.android.sdk.internal.crypto.store.db.serializeForRealm import org.matrix.olm.OlmAccount internal open class CryptoMetadataEntity( diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoRoomEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoRoomEntity.kt index e226f3eaa8..711b698464 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoRoomEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoRoomEntity.kt @@ -28,8 +28,8 @@ internal open class CryptoRoomEntity( // to avoid re-create and re-share at each startup (if rotation not needed..) // This is specific to megolm but not sure how to model it better var outboundSessionInfo: OutboundGroupSessionInfoEntity? = null - ) - : RealmObject() { + ) : + RealmObject() { companion object } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/GossipingEventEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/GossipingEventEntity.kt index b8675d0823..75094f01db 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/GossipingEventEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/GossipingEventEntity.kt @@ -17,6 +17,8 @@ package org.matrix.android.sdk.internal.crypto.store.db.model import com.squareup.moshi.JsonDataException +import io.realm.RealmObject +import io.realm.annotations.Index import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.room.send.SendState @@ -24,8 +26,6 @@ import org.matrix.android.sdk.internal.crypto.MXEventDecryptionResult import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult import org.matrix.android.sdk.internal.database.mapper.ContentMapper import org.matrix.android.sdk.internal.di.MoshiProvider -import io.realm.RealmObject -import io.realm.annotations.Index import timber.log.Timber /** diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/IncomingGossipingRequestEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/IncomingGossipingRequestEntity.kt index df45568d18..4457a44cb2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/IncomingGossipingRequestEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/IncomingGossipingRequestEntity.kt @@ -16,6 +16,8 @@ package org.matrix.android.sdk.internal.crypto.store.db.model +import io.realm.RealmObject +import io.realm.annotations.Index import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.internal.crypto.GossipRequestType import org.matrix.android.sdk.internal.crypto.GossipingRequestState @@ -23,8 +25,6 @@ import org.matrix.android.sdk.internal.crypto.IncomingRoomKeyRequest import org.matrix.android.sdk.internal.crypto.IncomingSecretShareRequest import org.matrix.android.sdk.internal.crypto.IncomingShareRequestCommon import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyRequestBody -import io.realm.RealmObject -import io.realm.annotations.Index internal open class IncomingGossipingRequestEntity(@Index var requestId: String? = "", @Index var typeStr: String? = null, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OlmInboundGroupSessionEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OlmInboundGroupSessionEntity.kt index d0e16bbe11..f330e8822a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OlmInboundGroupSessionEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OlmInboundGroupSessionEntity.kt @@ -16,11 +16,11 @@ package org.matrix.android.sdk.internal.crypto.store.db.model +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2 import org.matrix.android.sdk.internal.crypto.store.db.deserializeFromRealm import org.matrix.android.sdk.internal.crypto.store.db.serializeForRealm -import io.realm.RealmObject -import io.realm.annotations.PrimaryKey import timber.log.Timber internal fun OlmInboundGroupSessionEntity.Companion.createPrimaryKey(sessionId: String?, senderKey: String?) = "$sessionId|$senderKey" @@ -33,8 +33,8 @@ internal open class OlmInboundGroupSessionEntity( // olmInboundGroupSessionData contains Json var olmInboundGroupSessionData: String? = null, // Indicate if the key has been backed up to the homeserver - var backedUp: Boolean = false) - : RealmObject() { + var backedUp: Boolean = false) : + RealmObject() { fun getInboundGroupSession(): OlmInboundGroupSessionWrapper2? { return try { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OlmSessionEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OlmSessionEntity.kt index 8f41057807..0b69311c57 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OlmSessionEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OlmSessionEntity.kt @@ -16,10 +16,10 @@ package org.matrix.android.sdk.internal.crypto.store.db.model -import org.matrix.android.sdk.internal.crypto.store.db.deserializeFromRealm -import org.matrix.android.sdk.internal.crypto.store.db.serializeForRealm import io.realm.RealmObject import io.realm.annotations.PrimaryKey +import org.matrix.android.sdk.internal.crypto.store.db.deserializeFromRealm +import org.matrix.android.sdk.internal.crypto.store.db.serializeForRealm import org.matrix.olm.OlmSession internal fun OlmSessionEntity.Companion.createPrimaryKey(sessionId: String, deviceKey: String) = "$sessionId|$deviceKey" @@ -29,8 +29,8 @@ internal open class OlmSessionEntity(@PrimaryKey var primaryKey: String = "", var sessionId: String? = null, var deviceKey: String? = null, var olmSessionData: String? = null, - var lastReceivedMessageTs: Long = 0) - : RealmObject() { + var lastReceivedMessageTs: Long = 0) : + RealmObject() { fun getOlmSession(): OlmSession? { return deserializeFromRealm(olmSessionData) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OutgoingGossipingRequestEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OutgoingGossipingRequestEntity.kt index 442dda1d71..a19547fddc 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OutgoingGossipingRequestEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OutgoingGossipingRequestEntity.kt @@ -18,6 +18,8 @@ package org.matrix.android.sdk.internal.crypto.store.db.model import com.squareup.moshi.JsonAdapter import com.squareup.moshi.Types +import io.realm.RealmObject +import io.realm.annotations.Index import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.internal.crypto.GossipRequestType import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequest @@ -26,8 +28,6 @@ import org.matrix.android.sdk.internal.crypto.OutgoingRoomKeyRequest import org.matrix.android.sdk.internal.crypto.OutgoingSecretRequest import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyRequestBody import org.matrix.android.sdk.internal.di.MoshiProvider -import io.realm.RealmObject -import io.realm.annotations.Index internal open class OutgoingGossipingRequestEntity( @Index var requestId: String? = null, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/WithHeldSessionEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/WithHeldSessionEntity.kt index 2864ab768d..6d7889053b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/WithHeldSessionEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/WithHeldSessionEntity.kt @@ -16,9 +16,9 @@ package org.matrix.android.sdk.internal.crypto.store.db.model -import org.matrix.android.sdk.internal.crypto.model.event.WithHeldCode import io.realm.RealmObject import io.realm.annotations.Index +import org.matrix.android.sdk.internal.crypto.model.event.WithHeldCode /** * When an encrypted message is sent in a room, the megolm key might not be sent to all devices present in the room. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/query/CrossSigningInfoEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/query/CrossSigningInfoEntityQueries.kt index eea2f6f31b..05eed9256e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/query/CrossSigningInfoEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/query/CrossSigningInfoEntityQueries.kt @@ -16,11 +16,11 @@ package org.matrix.android.sdk.internal.crypto.store.db.query -import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntity -import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntityFields import io.realm.Realm import io.realm.kotlin.createObject import io.realm.kotlin.where +import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntityFields internal fun CrossSigningInfoEntity.Companion.getOrCreate(realm: Realm, userId: String): CrossSigningInfoEntity { return realm.where() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/query/CryptoRoomEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/query/CryptoRoomEntityQueries.kt index 5ebf8b1ed5..5750cc1f67 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/query/CryptoRoomEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/query/CryptoRoomEntityQueries.kt @@ -16,11 +16,11 @@ package org.matrix.android.sdk.internal.crypto.store.db.query -import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoRoomEntity -import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoRoomEntityFields import io.realm.Realm import io.realm.kotlin.createObject import io.realm.kotlin.where +import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoRoomEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoRoomEntityFields /** * Get or create a room diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/query/DeviceInfoEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/query/DeviceInfoEntityQueries.kt index 1d5ca2d3cc..c9523d63ce 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/query/DeviceInfoEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/query/DeviceInfoEntityQueries.kt @@ -16,12 +16,12 @@ package org.matrix.android.sdk.internal.crypto.store.db.query -import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntity -import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntityFields -import org.matrix.android.sdk.internal.crypto.store.db.model.createPrimaryKey import io.realm.Realm import io.realm.kotlin.createObject import io.realm.kotlin.where +import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.createPrimaryKey /** * Get or create a device info diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/query/SharedSessionQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/query/SharedSessionQueries.kt index 39117512bb..2784e58425 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/query/SharedSessionQueries.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/query/SharedSessionQueries.kt @@ -29,8 +29,7 @@ internal fun SharedSessionEntity.Companion.get(realm: Realm, sessionId: String, userId: String, deviceId: String, - deviceIdentityKey: String?) - : SharedSessionEntity? { + deviceIdentityKey: String?): SharedSessionEntity? { return realm.where() .equalTo(SharedSessionEntityFields.ROOM_ID, roomId) .equalTo(SharedSessionEntityFields.SESSION_ID, sessionId) @@ -41,8 +40,7 @@ internal fun SharedSessionEntity.Companion.get(realm: Realm, .findFirst() } -internal fun SharedSessionEntity.Companion.get(realm: Realm, roomId: String?, sessionId: String) - : RealmResults { +internal fun SharedSessionEntity.Companion.get(realm: Realm, roomId: String?, sessionId: String): RealmResults { return realm.where() .equalTo(SharedSessionEntityFields.ROOM_ID, roomId) .equalTo(SharedSessionEntityFields.SESSION_ID, sessionId) @@ -55,8 +53,7 @@ internal fun SharedSessionEntity.Companion.create(realm: Realm, roomId: String?, userId: String, deviceId: String, deviceIdentityKey: String, - chainIndex: Int) - : SharedSessionEntity { + chainIndex: Int): SharedSessionEntity { return realm.createObject().apply { this.roomId = roomId this.algorithm = MXCRYPTO_ALGORITHM_MEGOLM diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/query/WithHeldSessionQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/query/WithHeldSessionQueries.kt index 3c6c594a70..b8a3e36137 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/query/WithHeldSessionQueries.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/query/WithHeldSessionQueries.kt @@ -16,12 +16,12 @@ package org.matrix.android.sdk.internal.crypto.store.db.query -import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM -import org.matrix.android.sdk.internal.crypto.store.db.model.WithHeldSessionEntity -import org.matrix.android.sdk.internal.crypto.store.db.model.WithHeldSessionEntityFields import io.realm.Realm import io.realm.kotlin.createObject import io.realm.kotlin.where +import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM +import org.matrix.android.sdk.internal.crypto.store.db.model.WithHeldSessionEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.WithHeldSessionEntityFields internal fun WithHeldSessionEntity.Companion.get(realm: Realm, roomId: String, sessionId: String): WithHeldSessionEntity? { return realm.where() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt index bdb8e8d137..ca04bac5d5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt @@ -16,11 +16,11 @@ package org.matrix.android.sdk.internal.crypto.tasks +import org.matrix.android.sdk.api.auth.UIABaseAuth import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor import org.matrix.android.sdk.internal.auth.registration.handleUIA import org.matrix.android.sdk.internal.crypto.api.CryptoApi import org.matrix.android.sdk.internal.crypto.model.rest.DeleteDeviceParams -import org.matrix.android.sdk.api.auth.UIABaseAuth import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.task.Task @@ -46,8 +46,8 @@ internal class DefaultDeleteDeviceTask @Inject constructor( cryptoApi.deleteDevice(params.deviceId, DeleteDeviceParams(params.userAuthParam?.asMap())) } } catch (throwable: Throwable) { - if (params.userInteractiveAuthInterceptor == null - || !handleUIA( + if (params.userInteractiveAuthInterceptor == null || + !handleUIA( failure = throwable, interceptor = params.userInteractiveAuthInterceptor, retryBlock = { authUpdate -> diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/InitializeCrossSigningTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/InitializeCrossSigningTask.kt index 1d40e5defd..e2fd54f0d8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/InitializeCrossSigningTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/InitializeCrossSigningTask.kt @@ -125,8 +125,8 @@ internal class DefaultInitializeCrossSigningTask @Inject constructor( try { uploadSigningKeysTask.execute(uploadSigningKeysParams) } catch (failure: Throwable) { - if (params.interactiveAuthInterceptor == null - || !handleUIA( + if (params.interactiveAuthInterceptor == null || + !handleUIA( failure = failure, interceptor = params.interactiveAuthInterceptor, retryBlock = { authUpdate -> diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendEventTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendEventTask.kt index e1e297767b..e40db6af67 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendEventTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendEventTask.kt @@ -34,7 +34,7 @@ internal interface SendEventTask : Task { internal class DefaultSendEventTask @Inject constructor( private val localEchoRepository: LocalEchoRepository, - private val encryptEventTask: DefaultEncryptEventTask, + private val encryptEventTask: EncryptEventTask, private val loadRoomMembersTask: LoadRoomMembersTask, private val roomAPI: RoomAPI, private val globalErrorReceiver: GlobalErrorReceiver) : SendEventTask { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendVerificationMessageTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendVerificationMessageTask.kt index 7fa48c3da1..c4a6ba27d6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendVerificationMessageTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendVerificationMessageTask.kt @@ -34,7 +34,7 @@ internal interface SendVerificationMessageTask : Task, otherUserId: String, roomId: String, localId: String?) - : PendingVerificationRequest { + override fun requestKeyVerificationInDMs(methods: List, + otherUserId: String, + roomId: String, + localId: String?): PendingVerificationRequest { Timber.i("## SAS Requesting verification to user: $otherUserId in room $roomId") val requestsForUser = pendingRequests.getOrPut(otherUserId) { mutableListOf() } @@ -1278,8 +1280,8 @@ internal class DefaultVerificationService @Inject constructor( private fun updatePendingRequest(updated: PendingVerificationRequest) { val requestsForUser = pendingRequests.getOrPut(updated.otherUserId) { mutableListOf() } val index = requestsForUser.indexOfFirst { - it.transactionId == updated.transactionId - || it.transactionId == null && it.localId == updated.localId + it.transactionId == updated.transactionId || + it.transactionId == null && it.localId == updated.localId } if (index != -1) { requestsForUser.removeAt(index) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SendVerificationMessageWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SendVerificationMessageWorker.kt index 538d7b56e9..481ce85f70 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SendVerificationMessageWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SendVerificationMessageWorker.kt @@ -34,8 +34,8 @@ import javax.inject.Inject * Possible next worker : None */ internal class SendVerificationMessageWorker(context: Context, - params: WorkerParameters) - : SessionSafeCoroutineWorker(context, params, Params::class.java) { + params: WorkerParameters) : + SessionSafeCoroutineWorker(context, params, Params::class.java) { @JsonClass(generateAdapter = true) internal data class Params( diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoStart.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoStart.kt index 21a6ba41b1..f727aff39d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoStart.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoStart.kt @@ -73,8 +73,8 @@ internal interface VerificationInfoStart : VerificationInfo) - : VerificationInfoAccept = MessageVerificationAcceptContent.create( + shortAuthenticationStrings: List): VerificationInfoAccept = + MessageVerificationAcceptContent.create( tid, keyAgreementProtocol, hash, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/EventInsertLiveObserver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/EventInsertLiveObserver.kt index 758c7aa5b9..115025cc7d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/EventInsertLiveObserver.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/EventInsertLiveObserver.kt @@ -31,8 +31,8 @@ import timber.log.Timber import javax.inject.Inject internal class EventInsertLiveObserver @Inject constructor(@SessionDatabase realmConfiguration: RealmConfiguration, - private val processors: Set<@JvmSuppressWildcards EventInsertLiveProcessor>) - : RealmLiveEntityObserver(realmConfiguration) { + private val processors: Set<@JvmSuppressWildcards EventInsertLiveProcessor>) : + RealmLiveEntityObserver(realmConfiguration) { override val query = Monarchy.Query { it.where(EventInsertEntity::class.java).equalTo(EventInsertEntityFields.CAN_BE_PROCESSED, true) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmKeysUtils.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmKeysUtils.kt index d5ff7a0f84..7c622146d3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmKeysUtils.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmKeysUtils.kt @@ -19,9 +19,9 @@ import android.content.Context import android.util.Base64 import androidx.core.content.edit import io.realm.Realm +import io.realm.RealmConfiguration import org.matrix.android.sdk.BuildConfig import org.matrix.android.sdk.internal.session.securestorage.SecretStoringUtils -import io.realm.RealmConfiguration import timber.log.Timber import java.security.SecureRandom import javax.inject.Inject diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmLiveEntityObserver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmLiveEntityObserver.kt index c602ed7075..50eb086f9a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmLiveEntityObserver.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmLiveEntityObserver.kt @@ -17,8 +17,6 @@ package org.matrix.android.sdk.internal.database import com.zhuinden.monarchy.Monarchy -import org.matrix.android.sdk.api.session.SessionLifecycleObserver -import org.matrix.android.sdk.internal.util.createBackgroundHandler import io.realm.Realm import io.realm.RealmChangeListener import io.realm.RealmConfiguration @@ -29,13 +27,15 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.android.asCoroutineDispatcher import kotlinx.coroutines.cancelChildren import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.SessionLifecycleObserver +import org.matrix.android.sdk.internal.util.createBackgroundHandler import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicReference internal interface LiveEntityObserver : SessionLifecycleObserver -internal abstract class RealmLiveEntityObserver(protected val realmConfiguration: RealmConfiguration) - : LiveEntityObserver, RealmChangeListener> { +internal abstract class RealmLiveEntityObserver(protected val realmConfiguration: RealmConfiguration) : + LiveEntityObserver, RealmChangeListener> { private companion object { val BACKGROUND_HANDLER = createBackgroundHandler("LIVE_ENTITY_BACKGROUND") diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionProvider.kt index 52fbabb49f..8c62c345d0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionProvider.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionProvider.kt @@ -21,8 +21,8 @@ import androidx.annotation.MainThread import com.zhuinden.monarchy.Monarchy import io.realm.Realm import org.matrix.android.sdk.api.session.Session -import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.api.session.SessionLifecycleObserver +import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.session.SessionScope import javax.inject.Inject import kotlin.concurrent.getOrSet @@ -32,8 +32,8 @@ import kotlin.concurrent.getOrSet * instance. This does check each time if you are on the main thread or not and returns the appropriate realm instance. */ @SessionScope -internal class RealmSessionProvider @Inject constructor(@SessionDatabase private val monarchy: Monarchy) - : SessionLifecycleObserver { +internal class RealmSessionProvider @Inject constructor(@SessionDatabase private val monarchy: Monarchy) : + SessionLifecycleObserver { private val realmThreadLocal = ThreadLocal() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt index 5aa6914647..9934c2570b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt @@ -36,19 +36,34 @@ import org.matrix.android.sdk.internal.database.model.PendingThreePidEntityField import org.matrix.android.sdk.internal.database.model.PreviewUrlCacheEntityFields import org.matrix.android.sdk.internal.database.model.RoomAccountDataEntityFields import org.matrix.android.sdk.internal.database.model.RoomEntityFields +import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFields import org.matrix.android.sdk.internal.database.model.RoomMembersLoadStatusType import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields import org.matrix.android.sdk.internal.database.model.RoomTagEntityFields import org.matrix.android.sdk.internal.database.model.SpaceChildSummaryEntityFields import org.matrix.android.sdk.internal.database.model.SpaceParentSummaryEntityFields import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields +import org.matrix.android.sdk.internal.database.model.presence.UserPresenceEntityFields import org.matrix.android.sdk.internal.di.MoshiProvider import org.matrix.android.sdk.internal.query.process +import org.matrix.android.sdk.internal.util.Normalizer import timber.log.Timber +import javax.inject.Inject -internal object RealmSessionStoreMigration : RealmMigration { +internal class RealmSessionStoreMigration @Inject constructor( + private val normalizer: Normalizer +) : RealmMigration { - const val SESSION_STORE_SCHEMA_VERSION = 18L + companion object { + const val SESSION_STORE_SCHEMA_VERSION = 20L + } + + /** + * Forces all RealmSessionStoreMigration instances to be equal + * Avoids Realm throwing when multiple instances of the migration are set + */ + override fun equals(other: Any?) = other is RealmSessionStoreMigration + override fun hashCode() = 1000 override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) { Timber.v("Migrating Realm Session from $oldVersion to $newVersion") @@ -71,6 +86,8 @@ internal object RealmSessionStoreMigration : RealmMigration { if (oldVersion <= 15) migrateTo16(realm) if (oldVersion <= 16) migrateTo17(realm) if (oldVersion <= 17) migrateTo18(realm) + if (oldVersion <= 18) migrateTo19(realm) + if (oldVersion <= 19) migrateTo20(realm) } private fun migrateTo1(realm: DynamicRealm) { @@ -343,6 +360,41 @@ internal object RealmSessionStoreMigration : RealmMigration { private fun migrateTo18(realm: DynamicRealm) { Timber.d("Step 17 -> 18") + realm.schema.create("UserPresenceEntity") + ?.addField(UserPresenceEntityFields.USER_ID, String::class.java) + ?.addPrimaryKey(UserPresenceEntityFields.USER_ID) + ?.setRequired(UserPresenceEntityFields.USER_ID, true) + ?.addField(UserPresenceEntityFields.PRESENCE_STR, String::class.java) + ?.addField(UserPresenceEntityFields.LAST_ACTIVE_AGO, Long::class.java) + ?.setNullable(UserPresenceEntityFields.LAST_ACTIVE_AGO, true) + ?.addField(UserPresenceEntityFields.STATUS_MESSAGE, String::class.java) + ?.addField(UserPresenceEntityFields.IS_CURRENTLY_ACTIVE, Boolean::class.java) + ?.setNullable(UserPresenceEntityFields.IS_CURRENTLY_ACTIVE, true) + ?.addField(UserPresenceEntityFields.AVATAR_URL, String::class.java) + ?.addField(UserPresenceEntityFields.DISPLAY_NAME, String::class.java) + + val userPresenceEntity = realm.schema.get("UserPresenceEntity") ?: return + realm.schema.get("RoomSummaryEntity") + ?.addRealmObjectField(RoomSummaryEntityFields.DIRECT_USER_PRESENCE.`$`, userPresenceEntity) + + realm.schema.get("RoomMemberSummaryEntity") + ?.addRealmObjectField(RoomMemberSummaryEntityFields.USER_PRESENCE_ENTITY.`$`, userPresenceEntity) + } + + private fun migrateTo19(realm: DynamicRealm) { + Timber.d("Step 18 -> 19") + realm.schema.get("RoomSummaryEntity") + ?.addField(RoomSummaryEntityFields.NORMALIZED_DISPLAY_NAME, String::class.java) + ?.transform { + it.getString(RoomSummaryEntityFields.DISPLAY_NAME)?.let { displayName -> + val normalised = normalizer.normalize(displayName) + it.set(RoomSummaryEntityFields.NORMALIZED_DISPLAY_NAME, normalised) + } + } + } + + private fun migrateTo20(realm: DynamicRealm) { + Timber.d("Step 19 -> 20") realm.schema.get("ChunkEntity")?.apply { if (hasField("numberOfTimelineEvents")) { removeField("numberOfTimelineEvents") @@ -362,4 +414,5 @@ internal object RealmSessionStoreMigration : RealmMigration { } } } + } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/SessionRealmConfigurationFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/SessionRealmConfigurationFactory.kt index 1771c5b202..04ca26a943 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/SessionRealmConfigurationFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/SessionRealmConfigurationFactory.kt @@ -40,6 +40,7 @@ private const val REALM_NAME = "disk_store.realm" */ internal class SessionRealmConfigurationFactory @Inject constructor( private val realmKeysUtils: RealmKeysUtils, + private val realmSessionStoreMigration: RealmSessionStoreMigration, @SessionFilesDirectory val directory: File, @SessionId val sessionId: String, @UserMd5 val userMd5: String, @@ -71,7 +72,7 @@ internal class SessionRealmConfigurationFactory @Inject constructor( .allowWritesOnUiThread(true) .modules(SessionRealmModule()) .schemaVersion(RealmSessionStoreMigration.SESSION_STORE_SCHEMA_VERSION) - .migration(RealmSessionStoreMigration) + .migration(realmSessionStoreMigration) .build() // Try creating a realm instance and if it succeeds we can clear the flag diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt index 5e4e5ff3b6..482f4ad842 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt @@ -16,6 +16,9 @@ package org.matrix.android.sdk.internal.database.helper +import io.realm.Realm +import io.realm.Sort +import io.realm.kotlin.createObject import org.matrix.android.sdk.api.session.room.model.RoomMemberContent import org.matrix.android.sdk.internal.database.model.ChunkEntity import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntityFields @@ -33,9 +36,6 @@ import org.matrix.android.sdk.internal.database.query.getOrCreate import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.extensions.assertIsManaged import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection -import io.realm.Realm -import io.realm.Sort -import io.realm.kotlin.createObject import timber.log.Timber internal fun ChunkEntity.merge(roomId: String, chunkToMerge: ChunkEntity, direction: PaginationDirection) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/TimelineEventEntityHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/TimelineEventEntityHelper.kt index 90e867749e..3993e8e799 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/TimelineEventEntityHelper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/TimelineEventEntityHelper.kt @@ -16,9 +16,9 @@ package org.matrix.android.sdk.internal.database.helper +import io.realm.Realm import org.matrix.android.sdk.internal.database.model.TimelineEventEntity import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields -import io.realm.Realm internal fun TimelineEventEntity.Companion.nextId(realm: Realm): Long { val currentIdNum = realm.where(TimelineEventEntity::class.java).max(TimelineEventEntityFields.LOCAL_ID) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/IsUselessResolver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/IsUselessResolver.kt index 5d7afc50d6..d704ecac8e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/IsUselessResolver.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/IsUselessResolver.kt @@ -29,8 +29,8 @@ internal object IsUselessResolver { return when (event.type) { EventType.STATE_ROOM_MEMBER -> { // Call toContent(), to filter out null value - event.content != null - && event.content.toContent() == event.resolvedPrevContent()?.toContent() + event.content != null && + event.content.toContent() == event.resolvedPrevContent()?.toContent() } else -> false } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/PollResponseAggregatedSummaryEntityMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/PollResponseAggregatedSummaryEntityMapper.kt index b26e7e88e3..00998af9bb 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/PollResponseAggregatedSummaryEntityMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/PollResponseAggregatedSummaryEntityMapper.kt @@ -16,11 +16,11 @@ package org.matrix.android.sdk.internal.database.mapper +import io.realm.RealmList import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.model.PollResponseAggregatedSummary import org.matrix.android.sdk.internal.database.model.PollResponseAggregatedSummaryEntity -import io.realm.RealmList internal object PollResponseAggregatedSummaryEntityMapper { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomMemberSummaryMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomMemberSummaryMapper.kt index 2365a39567..efd9b68011 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomMemberSummaryMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomMemberSummaryMapper.kt @@ -18,12 +18,14 @@ package org.matrix.android.sdk.internal.database.mapper import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity +import org.matrix.android.sdk.internal.database.model.presence.toUserPresence internal object RoomMemberSummaryMapper { fun map(roomMemberSummaryEntity: RoomMemberSummaryEntity): RoomMemberSummary { return RoomMemberSummary( userId = roomMemberSummaryEntity.userId, + userPresence = roomMemberSummaryEntity.userPresenceEntity?.toUserPresence(), avatarUrl = roomMemberSummaryEntity.avatarUrl, displayName = roomMemberSummaryEntity.displayName, membership = roomMemberSummaryEntity.membership diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt index 0cf431c340..3a15e0acf0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt @@ -21,12 +21,13 @@ import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo import org.matrix.android.sdk.api.session.room.model.SpaceParentInfo import org.matrix.android.sdk.api.session.room.model.tag.RoomTag +import org.matrix.android.sdk.api.session.typing.TypingUsersTracker import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity -import org.matrix.android.sdk.internal.session.typing.DefaultTypingUsersTracker +import org.matrix.android.sdk.internal.database.model.presence.toUserPresence import javax.inject.Inject internal class RoomSummaryMapper @Inject constructor(private val timelineEventMapper: TimelineEventMapper, - private val typingUsersTracker: DefaultTypingUsersTracker) { + private val typingUsersTracker: TypingUsersTracker) { fun map(roomSummaryEntity: RoomSummaryEntity): RoomSummary { val tags = roomSummaryEntity.tags().map { @@ -41,13 +42,14 @@ internal class RoomSummaryMapper @Inject constructor(private val timelineEventMa return RoomSummary( roomId = roomSummaryEntity.roomId, - displayName = roomSummaryEntity.displayName ?: "", + displayName = roomSummaryEntity.displayName() ?: "", name = roomSummaryEntity.name ?: "", topic = roomSummaryEntity.topic ?: "", avatarUrl = roomSummaryEntity.avatarUrl ?: "", joinRules = roomSummaryEntity.joinRules, isDirect = roomSummaryEntity.isDirect, directUserId = roomSummaryEntity.directUserId, + directUserPresence = roomSummaryEntity.directUserPresence?.toUserPresence(), latestPreviewableEvent = latestEvent, joinedMembersCount = roomSummaryEntity.joinedMembersCount, invitedMembersCount = roomSummaryEntity.invitedMembersCount, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt index 4dc8712afb..bcd30cb54b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt @@ -16,12 +16,12 @@ package org.matrix.android.sdk.internal.database.model +import io.realm.RealmObject +import io.realm.annotations.Index import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.internal.crypto.MXEventDecryptionResult import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult import org.matrix.android.sdk.internal.di.MoshiProvider -import io.realm.RealmObject -import io.realm.annotations.Index import org.matrix.android.sdk.internal.extensions.assertIsManaged internal open class EventEntity(@Index var eventId: String = "", diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/GroupEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/GroupEntity.kt index 25a041e3d0..527f782359 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/GroupEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/GroupEntity.kt @@ -16,16 +16,16 @@ package org.matrix.android.sdk.internal.database.model -import org.matrix.android.sdk.api.session.room.model.Membership import io.realm.RealmObject import io.realm.annotations.PrimaryKey +import org.matrix.android.sdk.api.session.room.model.Membership /** * This class is used to store group info (groupId and membership) from the sync response. * Then GetGroupDataTask is called regularly to fetch group information from the homeserver. */ -internal open class GroupEntity(@PrimaryKey var groupId: String = "") - : RealmObject() { +internal open class GroupEntity(@PrimaryKey var groupId: String = "") : + RealmObject() { private var membershipStr: String = Membership.NONE.name var membership: Membership diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/GroupSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/GroupSummaryEntity.kt index 8982436ccc..4ba45dcda2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/GroupSummaryEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/GroupSummaryEntity.kt @@ -16,10 +16,10 @@ package org.matrix.android.sdk.internal.database.model -import org.matrix.android.sdk.api.session.room.model.Membership import io.realm.RealmList import io.realm.RealmObject import io.realm.annotations.PrimaryKey +import org.matrix.android.sdk.api.session.room.model.Membership internal open class GroupSummaryEntity(@PrimaryKey var groupId: String = "", var displayName: String = "", diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PushRulesEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PushRulesEntity.kt index 571bc71c27..4125d90891 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PushRulesEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PushRulesEntity.kt @@ -15,9 +15,9 @@ */ package org.matrix.android.sdk.internal.database.model -import org.matrix.android.sdk.api.pushrules.RuleKind import io.realm.RealmList import io.realm.RealmObject +import org.matrix.android.sdk.api.pushrules.RuleKind import org.matrix.android.sdk.internal.extensions.clearWith internal open class PushRulesEntity( diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomEntity.kt index 65483e05bf..2997d5d7d8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomEntity.kt @@ -16,10 +16,10 @@ package org.matrix.android.sdk.internal.database.model -import org.matrix.android.sdk.api.session.room.model.Membership import io.realm.RealmList import io.realm.RealmObject import io.realm.annotations.PrimaryKey +import org.matrix.android.sdk.api.session.room.model.Membership internal open class RoomEntity(@PrimaryKey var roomId: String = "", var chunks: RealmList = RealmList(), diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomMemberSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomMemberSummaryEntity.kt index e970fab397..a8a76d1681 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomMemberSummaryEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomMemberSummaryEntity.kt @@ -16,10 +16,12 @@ package org.matrix.android.sdk.internal.database.model -import org.matrix.android.sdk.api.session.room.model.Membership import io.realm.RealmObject import io.realm.annotations.Index import io.realm.annotations.PrimaryKey +import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.api.util.MatrixItem +import org.matrix.android.sdk.internal.database.model.presence.UserPresenceEntity internal open class RoomMemberSummaryEntity(@PrimaryKey var primaryKey: String = "", @Index var userId: String = "", @@ -39,7 +41,12 @@ internal open class RoomMemberSummaryEntity(@PrimaryKey var primaryKey: String = membershipStr = value.name } - fun getBestName() = displayName?.takeIf { it.isNotBlank() } ?: userId + var userPresenceEntity: UserPresenceEntity? = null + set(value) { + if (value != field) field = value + } + + fun toMatrixItem() = MatrixItem.UserItem(userId, displayName, avatarUrl) companion object } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomSummaryEntity.kt index 64dc08e827..67672f03ad 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomSummaryEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomSummaryEntity.kt @@ -27,6 +27,8 @@ import org.matrix.android.sdk.api.session.room.model.RoomJoinRules import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.VersioningState import org.matrix.android.sdk.api.session.room.model.tag.RoomTag +import org.matrix.android.sdk.internal.database.model.presence.UserPresenceEntity +import org.matrix.android.sdk.internal.session.room.membership.RoomName internal open class RoomSummaryEntity( @PrimaryKey var roomId: String = "", @@ -35,10 +37,24 @@ internal open class RoomSummaryEntity( var children: RealmList = RealmList() ) : RealmObject() { - var displayName: String? = "" - set(value) { - if (value != field) field = value + private var displayName: String? = "" + + fun displayName() = displayName + + fun setDisplayName(roomName: RoomName) { + if (roomName.name != displayName) { + displayName = roomName.name + normalizedDisplayName = roomName.normalizedName } + } + + /** + * Workaround for Realm only supporting Latin-1 character sets when sorting + * or filtering by case + * See https://github.com/realm/realm-core/issues/777 + */ + private var normalizedDisplayName: String? = "" + var avatarUrl: String? = "" set(value) { if (value != field) field = value @@ -204,6 +220,11 @@ internal open class RoomSummaryEntity( if (value != field) field = value } + var directUserPresence: UserPresenceEntity? = null + set(value) { + if (value != field) field = value + } + var hasFailedSending: Boolean = false set(value) { if (value != field) field = value @@ -278,5 +299,6 @@ internal open class RoomSummaryEntity( roomEncryptionTrustLevelStr = value?.name } } + companion object } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt index 19472e21d9..c090777972 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt @@ -17,6 +17,7 @@ package org.matrix.android.sdk.internal.database.model import io.realm.annotations.RealmModule +import org.matrix.android.sdk.internal.database.model.presence.UserPresenceEntity /** * Realm module for Session @@ -64,6 +65,7 @@ import io.realm.annotations.RealmModule WellknownIntegrationManagerConfigEntity::class, RoomAccountDataEntity::class, SpaceChildSummaryEntity::class, - SpaceParentSummaryEntity::class + SpaceParentSummaryEntity::class, + UserPresenceEntity::class ]) internal class SessionRealmModule diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/presence/UserPresenceEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/presence/UserPresenceEntity.kt new file mode 100644 index 0000000000..5713337ec5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/presence/UserPresenceEntity.kt @@ -0,0 +1,51 @@ +/* + * 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.internal.database.model.presence + +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey +import org.matrix.android.sdk.api.session.presence.model.PresenceEnum +import org.matrix.android.sdk.api.session.presence.model.UserPresence + +internal open class UserPresenceEntity(@PrimaryKey var userId: String = "", + var lastActiveAgo: Long? = null, + var statusMessage: String? = null, + var isCurrentlyActive: Boolean? = null, + var avatarUrl: String? = null, + var displayName: String? = null +) : RealmObject() { + + var presence: PresenceEnum + get() { + return PresenceEnum.valueOf(presenceStr) + } + set(value) { + presenceStr = value.name + } + + private var presenceStr: String = PresenceEnum.UNAVAILABLE.name + + companion object +} + +internal fun UserPresenceEntity.toUserPresence() = + UserPresence( + lastActiveAgo, + statusMessage, + isCurrentlyActive, + presence + ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/BreadcrumbsEntityQuery.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/BreadcrumbsEntityQuery.kt index 0463d52fff..3b24ff5f9d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/BreadcrumbsEntityQuery.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/BreadcrumbsEntityQuery.kt @@ -16,10 +16,10 @@ package org.matrix.android.sdk.internal.database.query -import org.matrix.android.sdk.internal.database.model.BreadcrumbsEntity import io.realm.Realm import io.realm.kotlin.createObject import io.realm.kotlin.where +import org.matrix.android.sdk.internal.database.model.BreadcrumbsEntity internal fun BreadcrumbsEntity.Companion.get(realm: Realm): BreadcrumbsEntity? { return realm.where().findFirst() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ChunkEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ChunkEntityQueries.kt index f7d2823303..156a8dd767 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ChunkEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ChunkEntityQueries.kt @@ -16,13 +16,13 @@ package org.matrix.android.sdk.internal.database.query -import org.matrix.android.sdk.internal.database.model.ChunkEntity -import org.matrix.android.sdk.internal.database.model.ChunkEntityFields import io.realm.Realm import io.realm.RealmQuery import io.realm.RealmResults import io.realm.kotlin.createObject import io.realm.kotlin.where +import org.matrix.android.sdk.internal.database.model.ChunkEntity +import org.matrix.android.sdk.internal.database.model.ChunkEntityFields internal fun ChunkEntity.Companion.where(realm: Realm, roomId: String): RealmQuery { return realm.where() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/CurrentStateEventEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/CurrentStateEventEntityQueries.kt index 9a3622e2dc..716783f2ba 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/CurrentStateEventEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/CurrentStateEventEntityQueries.kt @@ -17,33 +17,46 @@ package org.matrix.android.sdk.internal.database.query -import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity -import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntityFields import io.realm.Realm import io.realm.RealmQuery import io.realm.kotlin.createObject +import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity +import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntityFields -internal fun CurrentStateEventEntity.Companion.whereType(realm: Realm, roomId: String, type: String): RealmQuery { +internal fun CurrentStateEventEntity.Companion.whereType(realm: Realm, + roomId: String, + type: String): RealmQuery { return realm.where(CurrentStateEventEntity::class.java) .equalTo(CurrentStateEventEntityFields.ROOM_ID, roomId) .equalTo(CurrentStateEventEntityFields.TYPE, type) } -internal fun CurrentStateEventEntity.Companion.whereStateKey(realm: Realm, roomId: String, type: String, stateKey: String) - : RealmQuery { +internal fun CurrentStateEventEntity.Companion.whereStateKey(realm: Realm, + roomId: String, + type: String, + stateKey: String): RealmQuery { return whereType(realm = realm, roomId = roomId, type = type) .equalTo(CurrentStateEventEntityFields.STATE_KEY, stateKey) } -internal fun CurrentStateEventEntity.Companion.getOrNull(realm: Realm, roomId: String, stateKey: String, type: String): CurrentStateEventEntity? { +internal fun CurrentStateEventEntity.Companion.getOrNull(realm: Realm, + roomId: String, + stateKey: String, + type: String): CurrentStateEventEntity? { return whereStateKey(realm = realm, roomId = roomId, type = type, stateKey = stateKey).findFirst() } -internal fun CurrentStateEventEntity.Companion.getOrCreate(realm: Realm, roomId: String, stateKey: String, type: String): CurrentStateEventEntity { +internal fun CurrentStateEventEntity.Companion.getOrCreate(realm: Realm, + roomId: String, + stateKey: String, + type: String): CurrentStateEventEntity { return getOrNull(realm = realm, roomId = roomId, stateKey = stateKey, type = type) ?: create(realm, roomId, stateKey, type) } -private fun create(realm: Realm, roomId: String, stateKey: String, type: String): CurrentStateEventEntity { +private fun create(realm: Realm, + roomId: String, + stateKey: String, + type: String): CurrentStateEventEntity { return realm.createObject().apply { this.type = type this.roomId = roomId diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventAnnotationsSummaryEntityQuery.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventAnnotationsSummaryEntityQuery.kt index c3cae3d268..14cb7e22da 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventAnnotationsSummaryEntityQuery.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventAnnotationsSummaryEntityQuery.kt @@ -16,12 +16,12 @@ package org.matrix.android.sdk.internal.database.query -import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity -import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntityFields -import org.matrix.android.sdk.internal.database.model.TimelineEventEntity import io.realm.Realm import io.realm.RealmQuery import io.realm.kotlin.where +import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity +import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntityFields +import org.matrix.android.sdk.internal.database.model.TimelineEventEntity internal fun EventAnnotationsSummaryEntity.Companion.where(realm: Realm, roomId: String, eventId: String): RealmQuery { return realm.where() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventEntityQueries.kt index 57e24cf88f..240b2a0691 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventEntityQueries.kt @@ -16,15 +16,15 @@ package org.matrix.android.sdk.internal.database.query -import org.matrix.android.sdk.internal.database.model.EventEntity -import org.matrix.android.sdk.internal.database.model.EventEntityFields -import org.matrix.android.sdk.internal.database.model.EventInsertEntity -import org.matrix.android.sdk.internal.database.model.EventInsertType import io.realm.Realm import io.realm.RealmList import io.realm.RealmQuery import io.realm.kotlin.where import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.internal.database.model.EventEntity +import org.matrix.android.sdk.internal.database.model.EventEntityFields +import org.matrix.android.sdk.internal.database.model.EventInsertEntity +import org.matrix.android.sdk.internal.database.model.EventInsertType internal fun EventEntity.copyToRealmOrIgnore(realm: Realm, insertType: EventInsertType): EventEntity { val eventEntity = realm.where() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/FilterEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/FilterEntityQueries.kt index c76e606805..3968169e08 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/FilterEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/FilterEntityQueries.kt @@ -16,11 +16,11 @@ package org.matrix.android.sdk.internal.database.query -import org.matrix.android.sdk.internal.database.model.FilterEntity -import org.matrix.android.sdk.internal.session.filter.FilterFactory import io.realm.Realm import io.realm.kotlin.createObject import io.realm.kotlin.where +import org.matrix.android.sdk.internal.database.model.FilterEntity +import org.matrix.android.sdk.internal.session.filter.FilterFactory /** * Get the current filter diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/GroupEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/GroupEntityQueries.kt index 9a1f2b3782..020592d1dd 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/GroupEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/GroupEntityQueries.kt @@ -16,13 +16,13 @@ package org.matrix.android.sdk.internal.database.query +import io.realm.Realm +import io.realm.RealmQuery +import io.realm.kotlin.where import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.internal.database.model.GroupEntity import org.matrix.android.sdk.internal.database.model.GroupEntityFields import org.matrix.android.sdk.internal.query.process -import io.realm.Realm -import io.realm.RealmQuery -import io.realm.kotlin.where internal fun GroupEntity.Companion.where(realm: Realm, groupId: String): RealmQuery { return realm.where() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/GroupSummaryEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/GroupSummaryEntityQueries.kt index fbfd8bd19e..8131598d95 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/GroupSummaryEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/GroupSummaryEntityQueries.kt @@ -16,12 +16,12 @@ package org.matrix.android.sdk.internal.database.query -import org.matrix.android.sdk.internal.database.model.GroupSummaryEntity -import org.matrix.android.sdk.internal.database.model.GroupSummaryEntityFields import io.realm.Realm import io.realm.RealmQuery import io.realm.kotlin.createObject import io.realm.kotlin.where +import org.matrix.android.sdk.internal.database.model.GroupSummaryEntity +import org.matrix.android.sdk.internal.database.model.GroupSummaryEntityFields internal fun GroupSummaryEntity.Companion.where(realm: Realm, groupId: String? = null): RealmQuery { val query = realm.where() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/HomeServerCapabilitiesQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/HomeServerCapabilitiesQueries.kt index b0b4f5a83d..4f8ac20e94 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/HomeServerCapabilitiesQueries.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/HomeServerCapabilitiesQueries.kt @@ -16,10 +16,10 @@ package org.matrix.android.sdk.internal.database.query -import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntity import io.realm.Realm import io.realm.kotlin.createObject import io.realm.kotlin.where +import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntity /** * Get the current HomeServerCapabilitiesEntity, return null if it does not exist diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/PushersQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/PushersQueries.kt index 359b256844..1f6b210252 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/PushersQueries.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/PushersQueries.kt @@ -15,6 +15,9 @@ */ package org.matrix.android.sdk.internal.database.query +import io.realm.Realm +import io.realm.RealmQuery +import io.realm.kotlin.where import org.matrix.android.sdk.api.pushrules.RuleKind import org.matrix.android.sdk.internal.database.model.PushRuleEntity import org.matrix.android.sdk.internal.database.model.PushRuleEntityFields @@ -22,9 +25,6 @@ import org.matrix.android.sdk.internal.database.model.PushRulesEntity import org.matrix.android.sdk.internal.database.model.PushRulesEntityFields import org.matrix.android.sdk.internal.database.model.PusherEntity import org.matrix.android.sdk.internal.database.model.PusherEntityFields -import io.realm.Realm -import io.realm.RealmQuery -import io.realm.kotlin.where internal fun PusherEntity.Companion.where(realm: Realm, pushKey: String? = null): RealmQuery { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadMarkerEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadMarkerEntityQueries.kt index 35fb2b068b..d80fe86aae 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadMarkerEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadMarkerEntityQueries.kt @@ -16,12 +16,12 @@ package org.matrix.android.sdk.internal.database.query -import org.matrix.android.sdk.internal.database.model.ReadMarkerEntity -import org.matrix.android.sdk.internal.database.model.ReadMarkerEntityFields import io.realm.Realm import io.realm.RealmQuery import io.realm.kotlin.createObject import io.realm.kotlin.where +import org.matrix.android.sdk.internal.database.model.ReadMarkerEntity +import org.matrix.android.sdk.internal.database.model.ReadMarkerEntityFields internal fun ReadMarkerEntity.Companion.where(realm: Realm, roomId: String): RealmQuery { return realm.where() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadQueries.kt index 5423025823..60096777d9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadQueries.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadQueries.kt @@ -15,13 +15,13 @@ */ package org.matrix.android.sdk.internal.database.query +import io.realm.Realm +import io.realm.RealmConfiguration import org.matrix.android.sdk.api.session.events.model.LocalEcho import org.matrix.android.sdk.internal.database.model.ChunkEntity import org.matrix.android.sdk.internal.database.model.ReadMarkerEntity import org.matrix.android.sdk.internal.database.model.ReadReceiptEntity import org.matrix.android.sdk.internal.database.model.TimelineEventEntity -import io.realm.Realm -import io.realm.RealmConfiguration internal fun isEventRead(realmConfiguration: RealmConfiguration, userId: String?, @@ -39,19 +39,18 @@ internal fun isEventRead(realmConfiguration: RealmConfiguration, val liveChunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomId) ?: return@use val eventToCheck = liveChunk.timelineEvents.find(eventId) isEventRead = when { - eventToCheck == null -> { - // This can happen in case of fast lane Event - false - } + eventToCheck == null -> hasReadMissingEvent( + realm = realm, + latestChunkEntity = liveChunk, + roomId = roomId, + userId = userId, + eventId = eventId + ) eventToCheck.root?.sender == userId -> true else -> { - val readReceipt = ReadReceiptEntity.where(realm, roomId, userId).findFirst() - ?: return@use - val readReceiptIndex = liveChunk.timelineEvents.find(readReceipt.eventId)?.displayIndex - ?: Int.MIN_VALUE - val eventToCheckIndex = eventToCheck.displayIndex - - eventToCheckIndex <= readReceiptIndex + val readReceipt = ReadReceiptEntity.where(realm, roomId, userId).findFirst() ?: return@use + val readReceiptIndex = liveChunk.timelineEvents.find(readReceipt.eventId)?.displayIndex ?: Int.MIN_VALUE + eventToCheck.displayIndex <= readReceiptIndex } } } @@ -59,6 +58,24 @@ internal fun isEventRead(realmConfiguration: RealmConfiguration, return isEventRead } +/** + * Missing events can be caused by the latest timeline chunk no longer contain an older event or + * by fast lane eagerly displaying events before the database has finished updating + */ +private fun hasReadMissingEvent(realm: Realm, latestChunkEntity: ChunkEntity, roomId: String, userId: String, eventId: String): Boolean { + return realm.doesEventExistInChunkHistory(eventId) && realm.hasReadReceiptInLatestChunk(latestChunkEntity, roomId, userId) +} + +private fun Realm.doesEventExistInChunkHistory(eventId: String): Boolean { + return ChunkEntity.findIncludingEvent(this, eventId) != null +} + +private fun Realm.hasReadReceiptInLatestChunk(latestChunkEntity: ChunkEntity, roomId: String, userId: String): Boolean { + return ReadReceiptEntity.where(this, roomId = roomId, userId = userId).findFirst()?.let { + latestChunkEntity.timelineEvents.find(it.eventId) + } != null +} + internal fun isReadMarkerMoreRecent(realmConfiguration: RealmConfiguration, roomId: String?, eventId: String?): Boolean { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadReceiptEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadReceiptEntityQueries.kt index 1a5e8fcf89..b180c06e2c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadReceiptEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadReceiptEntityQueries.kt @@ -16,12 +16,12 @@ package org.matrix.android.sdk.internal.database.query -import org.matrix.android.sdk.internal.database.model.ReadReceiptEntity -import org.matrix.android.sdk.internal.database.model.ReadReceiptEntityFields import io.realm.Realm import io.realm.RealmQuery import io.realm.kotlin.createObject import io.realm.kotlin.where +import org.matrix.android.sdk.internal.database.model.ReadReceiptEntity +import org.matrix.android.sdk.internal.database.model.ReadReceiptEntityFields internal fun ReadReceiptEntity.Companion.where(realm: Realm, roomId: String, userId: String): RealmQuery { return realm.where() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadReceiptsSummaryEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadReceiptsSummaryEntityQueries.kt index 97f8418403..a35df57b7b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadReceiptsSummaryEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadReceiptsSummaryEntityQueries.kt @@ -16,11 +16,11 @@ package org.matrix.android.sdk.internal.database.query -import org.matrix.android.sdk.internal.database.model.ReadReceiptsSummaryEntity -import org.matrix.android.sdk.internal.database.model.ReadReceiptsSummaryEntityFields import io.realm.Realm import io.realm.RealmQuery import io.realm.kotlin.where +import org.matrix.android.sdk.internal.database.model.ReadReceiptsSummaryEntity +import org.matrix.android.sdk.internal.database.model.ReadReceiptsSummaryEntityFields internal fun ReadReceiptsSummaryEntity.Companion.where(realm: Realm, eventId: String): RealmQuery { return realm.where() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReferencesAggregatedSummaryEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReferencesAggregatedSummaryEntityQueries.kt index 8b3929cd60..14beb2d853 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReferencesAggregatedSummaryEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReferencesAggregatedSummaryEntityQueries.kt @@ -16,11 +16,11 @@ package org.matrix.android.sdk.internal.database.query -import org.matrix.android.sdk.internal.database.model.ReferencesAggregatedSummaryEntity -import org.matrix.android.sdk.internal.database.model.ReferencesAggregatedSummaryEntityFields import io.realm.Realm import io.realm.RealmQuery import io.realm.kotlin.where +import org.matrix.android.sdk.internal.database.model.ReferencesAggregatedSummaryEntity +import org.matrix.android.sdk.internal.database.model.ReferencesAggregatedSummaryEntityFields internal fun ReferencesAggregatedSummaryEntity.Companion.where(realm: Realm, eventId: String): RealmQuery { val query = realm.where() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomEntityQueries.kt index a551f97379..08bb9e7ff3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomEntityQueries.kt @@ -16,13 +16,13 @@ package org.matrix.android.sdk.internal.database.query +import io.realm.Realm +import io.realm.RealmQuery +import io.realm.kotlin.where import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.internal.database.model.EventEntity import org.matrix.android.sdk.internal.database.model.RoomEntity import org.matrix.android.sdk.internal.database.model.RoomEntityFields -import io.realm.Realm -import io.realm.RealmQuery -import io.realm.kotlin.where internal fun RoomEntity.Companion.where(realm: Realm, roomId: String): RealmQuery { return realm.where() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomMemberEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomMemberEntityQueries.kt index 0747b12665..1ea06b9dfb 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomMemberEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomMemberEntityQueries.kt @@ -16,11 +16,12 @@ package org.matrix.android.sdk.internal.database.query -import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity -import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFields import io.realm.Realm import io.realm.RealmQuery import io.realm.kotlin.where +import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity +import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFields +import org.matrix.android.sdk.internal.database.model.presence.UserPresenceEntity internal fun RoomMemberSummaryEntity.Companion.where(realm: Realm, roomId: String, userId: String? = null): RealmQuery { val query = realm @@ -32,3 +33,13 @@ internal fun RoomMemberSummaryEntity.Companion.where(realm: Realm, roomId: Strin } return query } + +internal fun RoomMemberSummaryEntity.Companion.updateUserPresence(realm: Realm, userId: String, userPresenceEntity: UserPresenceEntity) { + realm.where() + .equalTo(RoomMemberSummaryEntityFields.USER_ID, userId) + .isNull(RoomMemberSummaryEntityFields.USER_PRESENCE_ENTITY.`$`) + .findAll() + .map { + it.userPresenceEntity = userPresenceEntity + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomSummaryEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomSummaryEntityQueries.kt index 2af5dcf0ae..d1b05a4932 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomSummaryEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomSummaryEntityQueries.kt @@ -16,13 +16,14 @@ package org.matrix.android.sdk.internal.database.query -import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity -import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields import io.realm.Realm import io.realm.RealmQuery import io.realm.RealmResults import io.realm.kotlin.createObject import io.realm.kotlin.where +import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity +import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields +import org.matrix.android.sdk.internal.database.model.presence.UserPresenceEntity internal fun RoomSummaryEntity.Companion.where(realm: Realm, roomId: String? = null): RealmQuery { val query = realm.where() @@ -67,3 +68,11 @@ internal fun RoomSummaryEntity.Companion.isDirect(realm: Realm, roomId: String): .findAll() .isNotEmpty() } + +internal fun RoomSummaryEntity.Companion.updateDirectUserPresence(realm: Realm, directUserId: String, userPresenceEntity: UserPresenceEntity) { + RoomSummaryEntity.where(realm) + .equalTo(RoomSummaryEntityFields.IS_DIRECT, true) + .equalTo(RoomSummaryEntityFields.DIRECT_USER_ID, directUserId) + .findFirst() + ?.directUserPresence = userPresenceEntity +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ScalarTokenQuery.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ScalarTokenQuery.kt index 53fd525092..a7d85d676c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ScalarTokenQuery.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ScalarTokenQuery.kt @@ -16,11 +16,11 @@ package org.matrix.android.sdk.internal.database.query -import org.matrix.android.sdk.internal.database.model.ScalarTokenEntity -import org.matrix.android.sdk.internal.database.model.ScalarTokenEntityFields import io.realm.Realm import io.realm.RealmQuery import io.realm.kotlin.where +import org.matrix.android.sdk.internal.database.model.ScalarTokenEntity +import org.matrix.android.sdk.internal.database.model.ScalarTokenEntityFields internal fun ScalarTokenEntity.Companion.where(realm: Realm, serverUrl: String): RealmQuery { return realm diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventEntityQueries.kt index 148232cf94..aa1ce41bb7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventEntityQueries.kt @@ -119,8 +119,7 @@ internal fun RealmList.find(eventId: String): TimelineEvent internal fun TimelineEventEntity.Companion.findAllInRoomWithSendStates(realm: Realm, roomId: String, - sendStates: List) - : RealmResults { + sendStates: List): RealmResults { return whereRoomId(realm, roomId) .filterSendStates(sendStates) .findAll() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/UserDraftsEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/UserDraftsEntityQueries.kt index 4af4da0a22..aa9d409a2a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/UserDraftsEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/UserDraftsEntityQueries.kt @@ -16,11 +16,11 @@ package org.matrix.android.sdk.internal.database.query -import org.matrix.android.sdk.internal.database.model.UserDraftsEntity -import org.matrix.android.sdk.internal.database.model.UserDraftsEntityFields import io.realm.Realm import io.realm.RealmQuery import io.realm.kotlin.where +import org.matrix.android.sdk.internal.database.model.UserDraftsEntity +import org.matrix.android.sdk.internal.database.model.UserDraftsEntityFields internal fun UserDraftsEntity.Companion.where(realm: Realm, roomId: String? = null): RealmQuery { val query = realm.where() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/UserEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/UserEntityQueries.kt index 6a5528e3db..3159f89311 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/UserEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/UserEntityQueries.kt @@ -16,11 +16,11 @@ package org.matrix.android.sdk.internal.database.query -import org.matrix.android.sdk.internal.database.model.UserEntity -import org.matrix.android.sdk.internal.database.model.UserEntityFields import io.realm.Realm import io.realm.RealmQuery import io.realm.kotlin.where +import org.matrix.android.sdk.internal.database.model.UserEntity +import org.matrix.android.sdk.internal.database.model.UserEntityFields internal fun UserEntity.Companion.where(realm: Realm, userId: String): RealmQuery { return realm diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/UserPresenceEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/UserPresenceEntityQueries.kt new file mode 100644 index 0000000000..22790b6f4f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/UserPresenceEntityQueries.kt @@ -0,0 +1,29 @@ +/* + * 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.internal.database.query + +import io.realm.Realm +import io.realm.RealmQuery +import io.realm.kotlin.where +import org.matrix.android.sdk.internal.database.model.presence.UserPresenceEntity +import org.matrix.android.sdk.internal.database.model.presence.UserPresenceEntityFields + +internal fun UserPresenceEntity.Companion.where(realm: Realm, userId: String): RealmQuery { + return realm + .where() + .equalTo(UserPresenceEntityFields.USER_ID, userId) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixComponent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixComponent.kt index 5bc519e960..81a067f2c0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixComponent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixComponent.kt @@ -24,6 +24,7 @@ import dagger.Component import okhttp3.OkHttpClient import org.matrix.android.sdk.api.Matrix import org.matrix.android.sdk.api.MatrixConfiguration +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.auth.AuthenticationService import org.matrix.android.sdk.api.auth.HomeServerHistoryService import org.matrix.android.sdk.api.raw.RawService @@ -35,7 +36,6 @@ import org.matrix.android.sdk.internal.session.MockHttpInterceptor import org.matrix.android.sdk.internal.session.TestInterceptor import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.util.BackgroundDetectionObserver -import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers import org.matrix.android.sdk.internal.util.system.SystemModule import org.matrix.olm.OlmManager import java.io.File diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixModule.kt index b58fb3e683..9cab307c61 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixModule.kt @@ -20,11 +20,11 @@ import android.content.Context import android.content.res.Resources import dagger.Module import dagger.Provides -import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers -import org.matrix.android.sdk.internal.util.createBackgroundHandler import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.android.asCoroutineDispatcher import kotlinx.coroutines.asCoroutineDispatcher +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers +import org.matrix.android.sdk.internal.util.createBackgroundHandler import org.matrix.olm.OlmManager import java.io.File import java.util.concurrent.Executors diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/NetworkModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/NetworkModule.kt index 0d0892b608..ad34a4d8a6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/NetworkModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/NetworkModule.kt @@ -20,15 +20,17 @@ import com.facebook.stetho.okhttp3.StethoInterceptor import com.squareup.moshi.Moshi import dagger.Module import dagger.Provides +import okhttp3.ConnectionSpec +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor import org.matrix.android.sdk.BuildConfig import org.matrix.android.sdk.api.MatrixConfiguration +import org.matrix.android.sdk.internal.network.ApiInterceptor import org.matrix.android.sdk.internal.network.TimeOutInterceptor import org.matrix.android.sdk.internal.network.UserAgentInterceptor import org.matrix.android.sdk.internal.network.interceptors.CurlLoggingInterceptor import org.matrix.android.sdk.internal.network.interceptors.FormattedJsonHttpLogger -import okhttp3.OkHttpClient -import okhttp3.logging.HttpLoggingInterceptor -import org.matrix.android.sdk.internal.network.ApiInterceptor +import java.util.Collections import java.util.concurrent.TimeUnit @Module @@ -66,6 +68,8 @@ internal object NetworkModule { httpLoggingInterceptor: HttpLoggingInterceptor, curlLoggingInterceptor: CurlLoggingInterceptor, apiInterceptor: ApiInterceptor): OkHttpClient { + val spec = ConnectionSpec.Builder(matrixConfiguration.connectionSpec).build() + return OkHttpClient.Builder() .connectTimeout(30, TimeUnit.SECONDS) .readTimeout(60, TimeUnit.SECONDS) @@ -87,6 +91,7 @@ internal object NetworkModule { proxy(it) } } + .connectionSpecs(Collections.singletonList(spec)) .build() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/LiveData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/LiveData.kt index 718e7869dd..fecbb874d0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/LiveData.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/LiveData.kt @@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.extensions import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LiveData +import androidx.lifecycle.MediatorLiveData import androidx.lifecycle.Observer inline fun LiveData.observeK(owner: LifecycleOwner, crossinline observer: (T?) -> Unit) { @@ -27,3 +28,25 @@ inline fun LiveData.observeK(owner: LifecycleOwner, crossinline observer: inline fun LiveData.observeNotNull(owner: LifecycleOwner, crossinline observer: (T) -> Unit) { this.observe(owner, Observer { it?.run(observer) }) } + +fun combineLatest(source1: LiveData, source2: LiveData, mapper: (T1, T2) -> R): LiveData { + val combined = MediatorLiveData() + var source1Result: T1? = null + var source2Result: T2? = null + + fun notify() { + if (source1Result != null && source2Result != null) { + combined.value = mapper(source1Result!!, source2Result!!) + } + } + + combined.addSource(source1) { + source1Result = it + notify() + } + combined.addSource(source2) { + source2Result = it + notify() + } + return combined +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/DefaultLegacySessionImporter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/DefaultLegacySessionImporter.kt index ad2aff4c9d..445b6be8e8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/DefaultLegacySessionImporter.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/DefaultLegacySessionImporter.kt @@ -17,6 +17,9 @@ package org.matrix.android.sdk.internal.legacy import android.content.Context +import io.realm.Realm +import io.realm.RealmConfiguration +import kotlinx.coroutines.runBlocking import org.matrix.android.sdk.api.auth.data.Credentials import org.matrix.android.sdk.api.auth.data.DiscoveryInformation import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig @@ -30,9 +33,6 @@ import org.matrix.android.sdk.internal.database.RealmKeysUtils import org.matrix.android.sdk.internal.legacy.riot.LoginStorage import org.matrix.android.sdk.internal.network.ssl.Fingerprint import org.matrix.android.sdk.internal.util.md5 -import io.realm.Realm -import io.realm.RealmConfiguration -import kotlinx.coroutines.runBlocking import timber.log.Timber import java.io.File import javax.inject.Inject diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/WellKnown.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/WellKnown.kt index 56d372faa5..17fd0925f8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/WellKnown.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/WellKnown.kt @@ -75,9 +75,9 @@ class WellKnown { (config as? Map<*, *>)?.let { map -> val apiUrl = map["api_url"] as? String val uiUrl = map["ui_url"] as? String ?: apiUrl - if (apiUrl != null - && apiUrl.startsWith("https://") - && uiUrl!!.startsWith("https://")) { + if (apiUrl != null && + apiUrl.startsWith("https://") && + uiUrl!!.startsWith("https://")) { managers.add(WellKnownManagerConfig( apiUrl = apiUrl, uiUrl = uiUrl diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/AccessTokenInterceptor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/AccessTokenInterceptor.kt index b11fb6a5ee..a34606a6bb 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/AccessTokenInterceptor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/AccessTokenInterceptor.kt @@ -16,9 +16,9 @@ package org.matrix.android.sdk.internal.network -import org.matrix.android.sdk.internal.network.token.AccessTokenProvider import okhttp3.Interceptor import okhttp3.Response +import org.matrix.android.sdk.internal.network.token.AccessTokenProvider internal class AccessTokenInterceptor(private val accessTokenProvider: AccessTokenProvider) : Interceptor { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkConnectivityChecker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkConnectivityChecker.kt index c149ed2591..e32f6be6fc 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkConnectivityChecker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkConnectivityChecker.kt @@ -17,10 +17,10 @@ package org.matrix.android.sdk.internal.network import androidx.annotation.WorkerThread +import kotlinx.coroutines.runBlocking import org.matrix.android.sdk.internal.session.SessionScope import org.matrix.android.sdk.internal.session.homeserver.HomeServerPinger import org.matrix.android.sdk.internal.util.BackgroundDetectionObserver -import kotlinx.coroutines.runBlocking import java.util.Collections import java.util.concurrent.atomic.AtomicBoolean import javax.inject.Inject @@ -43,8 +43,8 @@ interface NetworkConnectivityChecker { @SessionScope internal class DefaultNetworkConnectivityChecker @Inject constructor(private val homeServerPinger: HomeServerPinger, private val backgroundDetectionObserver: BackgroundDetectionObserver, - private val networkCallbackStrategy: NetworkCallbackStrategy) - : NetworkConnectivityChecker { + private val networkCallbackStrategy: NetworkCallbackStrategy) : + NetworkConnectivityChecker { private val hasInternetAccess = AtomicBoolean(true) private val listeners = Collections.synchronizedSet(LinkedHashSet()) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/Request.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/Request.kt index e045cebd3e..927d9f7dd2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/Request.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/Request.kt @@ -74,10 +74,10 @@ internal suspend inline fun executeRequest(globalErrorReceiver: GlobalErr currentRetryCount++ - if (exception is Failure.ServerError - && exception.httpCode == 429 - && exception.error.code == MatrixError.M_LIMIT_EXCEEDED - && currentRetryCount < maxRetriesCount) { + if (exception is Failure.ServerError && + exception.httpCode == 429 && + exception.error.code == MatrixError.M_LIMIT_EXCEEDED && + currentRetryCount < maxRetriesCount) { // 429, we can retry delay(exception.getRetryDelay(1_000)) } else if (canRetry && currentRetryCount < maxRetriesCount && exception.shouldBeRetried()) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/RequestExecutor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/RequestExecutor.kt new file mode 100644 index 0000000000..5cd2d88000 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/RequestExecutor.kt @@ -0,0 +1,36 @@ +/* + * 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.internal.network +import org.matrix.android.sdk.internal.network.executeRequest as internalExecuteRequest + +internal interface RequestExecutor { + suspend fun executeRequest(globalErrorReceiver: GlobalErrorReceiver?, + canRetry: Boolean = false, + maxDelayBeforeRetry: Long = 32_000L, + maxRetriesCount: Int = 4, + requestBlock: suspend () -> DATA): DATA +} + +internal object DefaultRequestExecutor : RequestExecutor { + override suspend fun executeRequest(globalErrorReceiver: GlobalErrorReceiver?, + canRetry: Boolean, + maxDelayBeforeRetry: Long, + maxRetriesCount: Int, + requestBlock: suspend () -> DATA): DATA { + return internalExecuteRequest(globalErrorReceiver, canRetry, maxDelayBeforeRetry, maxRetriesCount, requestBlock) + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/RequestModule.kt similarity index 69% rename from vector/src/main/java/im/vector/app/features/home/room/list/RoomListModule.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/RequestModule.kt index 5dcef663e6..7b2bb9fcba 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/RequestModule.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019 New Vector Ltd + * 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. @@ -14,14 +14,16 @@ * limitations under the License. */ -package im.vector.app.features.home.room.list +package org.matrix.android.sdk.internal.network -import dagger.Binds import dagger.Module +import dagger.Provides @Module -abstract class RoomListModule { +internal object RequestModule { - @Binds - abstract fun providesRoomListViewModelFactory(factory: RoomListViewModelFactory): RoomListViewModel.Factory + @Provides + fun providesRequestExecutor(): RequestExecutor { + return DefaultRequestExecutor + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/RetrofitExtensions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/RetrofitExtensions.kt index 8a03102527..60055be9ec 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/RetrofitExtensions.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/RetrofitExtensions.kt @@ -91,8 +91,8 @@ private fun toFailure(errorBody: ResponseBody?, httpCode: Int, globalErrorReceiv matrixError.code == MatrixError.M_CONSENT_NOT_GIVEN && !matrixError.consentUri.isNullOrBlank() -> { globalErrorReceiver?.handleGlobalError(GlobalError.ConsentNotGivenError(matrixError.consentUri)) } - httpCode == HttpURLConnection.HTTP_UNAUTHORIZED /* 401 */ - && matrixError.code == MatrixError.M_UNKNOWN_TOKEN -> { + httpCode == HttpURLConnection.HTTP_UNAUTHORIZED && /* 401 */ + matrixError.code == MatrixError.M_UNKNOWN_TOKEN -> { globalErrorReceiver?.handleGlobalError(GlobalError.InvalidToken(matrixError.isSoftLogout.orFalse())) } matrixError.code == MatrixError.ORG_MATRIX_EXPIRED_ACCOUNT -> { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/UserAgentHolder.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/UserAgentHolder.kt index 57eab6a8dd..00e15c283e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/UserAgentHolder.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/UserAgentHolder.kt @@ -73,8 +73,8 @@ internal class UserAgentHolder @Inject constructor(private val context: Context, // if there is no user agent or cannot parse it if (null == systemUserAgent || systemUserAgent.lastIndexOf(")") == -1 || !systemUserAgent.contains("(")) { - userAgent = (appName + "/" + appVersion + " ( Flavour " + flavorDescription - + "; MatrixAndroidSdk2 " + BuildConfig.SDK_VERSION + ")") + userAgent = (appName + "/" + appVersion + " ( Flavour " + flavorDescription + + "; MatrixAndroidSdk2 " + BuildConfig.SDK_VERSION + ")") } else { // update userAgent = appName + "/" + appVersion + " " + diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/httpclient/OkHttpClientUtil.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/httpclient/OkHttpClientUtil.kt index b4a2d191e2..3920c3b527 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/httpclient/OkHttpClientUtil.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/httpclient/OkHttpClientUtil.kt @@ -16,12 +16,12 @@ package org.matrix.android.sdk.internal.network.httpclient +import okhttp3.OkHttpClient import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig import org.matrix.android.sdk.internal.network.AccessTokenInterceptor import org.matrix.android.sdk.internal.network.interceptors.CurlLoggingInterceptor import org.matrix.android.sdk.internal.network.ssl.CertUtil import org.matrix.android.sdk.internal.network.token.AccessTokenProvider -import okhttp3.OkHttpClient import timber.log.Timber internal fun OkHttpClient.Builder.addAccessTokenInterceptor(accessTokenProvider: AccessTokenProvider): OkHttpClient.Builder { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/parsing/CheckNumberType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/parsing/CheckNumberType.kt index 14d275e021..27684bbf1a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/parsing/CheckNumberType.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/parsing/CheckNumberType.kt @@ -20,7 +20,6 @@ import androidx.annotation.Nullable import com.squareup.moshi.JsonAdapter import com.squareup.moshi.JsonReader import com.squareup.moshi.JsonWriter - import com.squareup.moshi.Moshi import java.io.IOException import java.lang.reflect.Type diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/CertUtil.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/CertUtil.kt index 976751446b..d8bdc5fc2b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/CertUtil.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/CertUtil.kt @@ -16,9 +16,9 @@ package org.matrix.android.sdk.internal.network.ssl -import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig import okhttp3.ConnectionSpec import okhttp3.internal.tls.OkHostnameVerifier +import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig import timber.log.Timber import java.security.KeyStore import java.security.MessageDigest @@ -177,15 +177,13 @@ internal object CertUtil { val trustPinned = arrayOf(PinnedTrustManagerProvider.provide(hsConfig.allowedFingerprints, defaultTrustManager)) - val sslSocketFactory: SSLSocketFactory - - if (hsConfig.forceUsageTlsVersions && hsConfig.tlsVersions != null) { + val sslSocketFactory = if (hsConfig.forceUsageTlsVersions && !hsConfig.tlsVersions.isNullOrEmpty()) { // Force usage of accepted Tls Versions for Android < 20 - sslSocketFactory = TLSSocketFactory(trustPinned, hsConfig.tlsVersions) + TLSSocketFactory(trustPinned, hsConfig.tlsVersions) } else { val sslContext = SSLContext.getInstance("TLS") sslContext.init(null, trustPinned, java.security.SecureRandom()) - sslSocketFactory = sslContext.socketFactory + sslContext.socketFactory } return PinnedSSLSocketFactory(sslSocketFactory, defaultTrustManager!!) @@ -237,14 +235,14 @@ internal object CertUtil { * @return a list of accepted TLS specifications. */ fun newConnectionSpecs(hsConfig: HomeServerConnectionConfig): List { - val builder = ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS) + val builder = ConnectionSpec.Builder(ConnectionSpec.RESTRICTED_TLS) val tlsVersions = hsConfig.tlsVersions - if (null != tlsVersions && tlsVersions.isNotEmpty()) { + if (!tlsVersions.isNullOrEmpty()) { builder.tlsVersions(*tlsVersions.toTypedArray()) } val tlsCipherSuites = hsConfig.tlsCipherSuites - if (null != tlsCipherSuites && tlsCipherSuites.isNotEmpty()) { + if (!tlsCipherSuites.isNullOrEmpty()) { builder.cipherSuites(*tlsCipherSuites.toTypedArray()) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/PinnedTrustManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/PinnedTrustManager.kt index b1001bd39c..6f245aa6d8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/PinnedTrustManager.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/PinnedTrustManager.kt @@ -18,7 +18,6 @@ package org.matrix.android.sdk.internal.network.ssl import java.security.cert.CertificateException import java.security.cert.X509Certificate - import javax.net.ssl.X509TrustManager /** diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/query/QueryStringValueProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/query/QueryStringValueProcessor.kt index fd33682231..b42bf2b8c7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/query/QueryStringValueProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/query/QueryStringValueProcessor.kt @@ -20,24 +20,41 @@ import io.realm.Case import io.realm.RealmObject import io.realm.RealmQuery import org.matrix.android.sdk.api.query.QueryStringValue -import timber.log.Timber +import org.matrix.android.sdk.api.query.QueryStringValue.ContentQueryStringValue +import org.matrix.android.sdk.internal.util.Normalizer +import javax.inject.Inject -fun RealmQuery.process(field: String, queryStringValue: QueryStringValue): RealmQuery { - when (queryStringValue) { - is QueryStringValue.NoCondition -> Timber.v("No condition to process") - is QueryStringValue.IsNotNull -> isNotNull(field) - is QueryStringValue.IsNull -> isNull(field) - is QueryStringValue.IsEmpty -> isEmpty(field) - is QueryStringValue.IsNotEmpty -> isNotEmpty(field) - is QueryStringValue.Equals -> equalTo(field, queryStringValue.string, queryStringValue.case.toRealmCase()) - is QueryStringValue.Contains -> contains(field, queryStringValue.string, queryStringValue.case.toRealmCase()) +class QueryStringValueProcessor @Inject constructor( + private val normalizer: Normalizer +) { + + fun RealmQuery.process(field: String, queryStringValue: QueryStringValue): RealmQuery { + return when (queryStringValue) { + is QueryStringValue.NoCondition -> this + is QueryStringValue.IsNotNull -> isNotNull(field) + is QueryStringValue.IsNull -> isNull(field) + is QueryStringValue.IsEmpty -> isEmpty(field) + is QueryStringValue.IsNotEmpty -> isNotEmpty(field) + is ContentQueryStringValue -> when (queryStringValue) { + is QueryStringValue.Equals -> equalTo(field, queryStringValue.toRealmValue(), queryStringValue.case.toRealmCase()) + is QueryStringValue.Contains -> contains(field, queryStringValue.toRealmValue(), queryStringValue.case.toRealmCase()) + } + } + } + + private fun ContentQueryStringValue.toRealmValue(): String { + return when (case) { + QueryStringValue.Case.NORMALIZED -> normalizer.normalize(string) + QueryStringValue.Case.SENSITIVE, + QueryStringValue.Case.INSENSITIVE -> string + } } - return this } private fun QueryStringValue.Case.toRealmCase(): Case { return when (this) { QueryStringValue.Case.INSENSITIVE -> Case.INSENSITIVE - QueryStringValue.Case.SENSITIVE -> Case.SENSITIVE + QueryStringValue.Case.SENSITIVE, + QueryStringValue.Case.NORMALIZED -> Case.SENSITIVE } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt index 414c018074..46c5967876 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt @@ -25,6 +25,7 @@ import kotlinx.coroutines.completeWith import kotlinx.coroutines.withContext import okhttp3.OkHttpClient import okhttp3.Request +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.session.content.ContentUrlResolver import org.matrix.android.sdk.api.session.file.FileService @@ -33,7 +34,6 @@ import org.matrix.android.sdk.internal.crypto.attachments.MXEncryptedAttachments import org.matrix.android.sdk.internal.di.SessionDownloadsDirectory import org.matrix.android.sdk.internal.di.UnauthenticatedWithCertificateWithProgress import org.matrix.android.sdk.internal.session.download.DownloadProgressInterceptor.Companion.DOWNLOAD_PROGRESS_INTERCEPTOR_HEADER -import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers import org.matrix.android.sdk.internal.util.file.AtomicFileCreator import org.matrix.android.sdk.internal.util.md5 import org.matrix.android.sdk.internal.util.writeToFile diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt index c2bd1e24ed..c52462612a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt @@ -22,6 +22,7 @@ import io.realm.RealmConfiguration import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.withContext import okhttp3.OkHttpClient +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.auth.data.SessionParams import org.matrix.android.sdk.api.failure.GlobalError import org.matrix.android.sdk.api.federation.FederationService @@ -40,11 +41,13 @@ import org.matrix.android.sdk.api.session.file.ContentDownloadStateTracker import org.matrix.android.sdk.api.session.file.FileService import org.matrix.android.sdk.api.session.group.GroupService import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService -import org.matrix.android.sdk.api.session.initsync.InitialSyncProgressService +import org.matrix.android.sdk.api.session.identity.IdentityService +import org.matrix.android.sdk.api.session.initsync.SyncStatusService import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerService import org.matrix.android.sdk.api.session.media.MediaService import org.matrix.android.sdk.api.session.openid.OpenIdService import org.matrix.android.sdk.api.session.permalinks.PermalinkService +import org.matrix.android.sdk.api.session.presence.PresenceService import org.matrix.android.sdk.api.session.profile.ProfileService import org.matrix.android.sdk.api.session.pushers.PushersService import org.matrix.android.sdk.api.session.room.RoomDirectoryService @@ -70,7 +73,6 @@ import org.matrix.android.sdk.internal.di.SessionId import org.matrix.android.sdk.internal.di.UnauthenticatedWithCertificate import org.matrix.android.sdk.internal.di.WorkManagerProvider import org.matrix.android.sdk.internal.network.GlobalErrorHandler -import org.matrix.android.sdk.internal.session.identity.DefaultIdentityService import org.matrix.android.sdk.internal.session.sync.SyncTokenStore import org.matrix.android.sdk.internal.session.sync.job.SyncThread import org.matrix.android.sdk.internal.session.sync.job.SyncWorker @@ -86,6 +88,7 @@ internal class DefaultSession @Inject constructor( private val globalErrorHandler: GlobalErrorHandler, @SessionId override val sessionId: String, + override val coroutineDispatchers: MatrixCoroutineDispatchers, @SessionDatabase private val realmConfiguration: RealmConfiguration, private val lifecycleObservers: Set<@JvmSuppressWildcards SessionLifecycleObserver>, private val sessionListeners: SessionListeners, @@ -115,18 +118,19 @@ internal class DefaultSession @Inject constructor( private val contentUploadProgressTracker: ContentUploadStateTracker, private val typingUsersTracker: TypingUsersTracker, private val contentDownloadStateTracker: ContentDownloadStateTracker, - private val initialSyncProgressService: Lazy, + private val syncStatusService: Lazy, private val homeServerCapabilitiesService: Lazy, private val accountDataService: Lazy, private val _sharedSecretStorageService: Lazy, private val accountService: Lazy, private val eventService: Lazy, - private val defaultIdentityService: DefaultIdentityService, + private val identityService: IdentityService, private val integrationManagerService: IntegrationManagerService, private val thirdPartyService: Lazy, private val callSignalingService: Lazy, private val spaceService: Lazy, private val openIdService: Lazy, + private val presenceService: Lazy, @UnauthenticatedWithCertificate private val unauthenticatedWithCertificateOkHttpClient: Lazy ) : Session, @@ -141,10 +145,11 @@ internal class DefaultSession @Inject constructor( PushersService by pushersService.get(), EventService by eventService.get(), TermsService by termsService.get(), - InitialSyncProgressService by initialSyncProgressService.get(), + SyncStatusService by syncStatusService.get(), SecureStorageService by secureStorageService.get(), HomeServerCapabilitiesService by homeServerCapabilitiesService.get(), ProfileService by profileService.get(), + PresenceService by presenceService.get(), AccountService by accountService.get() { override val sharedSecretStorageService: SharedSecretStorageService @@ -223,6 +228,8 @@ internal class DefaultSession @Inject constructor( override fun getSyncStateLive() = getSyncThread().liveState() + override fun syncFlow() = getSyncThread().syncFlow() + override fun getSyncState() = getSyncThread().currentState() override fun hasAlreadySynced(): Boolean { @@ -268,7 +275,7 @@ internal class DefaultSession @Inject constructor( override fun cryptoService(): CryptoService = cryptoService.get() - override fun identityService() = defaultIdentityService + override fun identityService() = identityService override fun fileService(): FileService = defaultFileService.get() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/EventInsertLiveProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/EventInsertLiveProcessor.kt index 7a687b774b..a650fa2d64 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/EventInsertLiveProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/EventInsertLiveProcessor.kt @@ -16,9 +16,9 @@ package org.matrix.android.sdk.internal.session +import io.realm.Realm import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.internal.database.model.EventInsertType -import io.realm.Realm internal interface EventInsertLiveProcessor { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt index 9a936b73c2..bc8a707530 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt @@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session import dagger.BindsInstance import dagger.Component +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.auth.data.SessionParams import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.internal.crypto.CancelGossipRequestWorker @@ -29,6 +30,7 @@ import org.matrix.android.sdk.internal.crypto.verification.SendVerificationMessa import org.matrix.android.sdk.internal.di.MatrixComponent import org.matrix.android.sdk.internal.federation.FederationModule import org.matrix.android.sdk.internal.network.NetworkConnectivityChecker +import org.matrix.android.sdk.internal.network.RequestModule import org.matrix.android.sdk.internal.session.account.AccountModule import org.matrix.android.sdk.internal.session.cache.CacheModule import org.matrix.android.sdk.internal.session.call.CallModule @@ -42,11 +44,11 @@ import org.matrix.android.sdk.internal.session.identity.IdentityModule import org.matrix.android.sdk.internal.session.integrationmanager.IntegrationManagerModule import org.matrix.android.sdk.internal.session.media.MediaModule import org.matrix.android.sdk.internal.session.openid.OpenIdModule +import org.matrix.android.sdk.internal.session.presence.di.PresenceModule import org.matrix.android.sdk.internal.session.profile.ProfileModule -import org.matrix.android.sdk.internal.session.pushers.AddHttpPusherWorker +import org.matrix.android.sdk.internal.session.pushers.AddPusherWorker import org.matrix.android.sdk.internal.session.pushers.PushersModule import org.matrix.android.sdk.internal.session.room.RoomModule -import org.matrix.android.sdk.internal.session.room.relation.SendRelationWorker import org.matrix.android.sdk.internal.session.room.send.MultipleEventSendingDispatcherWorker import org.matrix.android.sdk.internal.session.room.send.RedactEventWorker import org.matrix.android.sdk.internal.session.room.send.SendEventWorker @@ -63,7 +65,6 @@ import org.matrix.android.sdk.internal.session.user.UserModule import org.matrix.android.sdk.internal.session.user.accountdata.AccountDataModule import org.matrix.android.sdk.internal.session.widgets.WidgetModule import org.matrix.android.sdk.internal.task.TaskExecutor -import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers import org.matrix.android.sdk.internal.util.system.SystemModule @Component(dependencies = [MatrixComponent::class], @@ -95,7 +96,9 @@ import org.matrix.android.sdk.internal.util.system.SystemModule CallModule::class, SearchModule::class, ThirdPartyModule::class, - SpaceModule::class + SpaceModule::class, + PresenceModule::class, + RequestModule::class ] ) @SessionScope @@ -115,8 +118,6 @@ internal interface SessionComponent { fun inject(worker: SendEventWorker) - fun inject(worker: SendRelationWorker) - fun inject(worker: MultipleEventSendingDispatcherWorker) fun inject(worker: RedactEventWorker) @@ -127,7 +128,7 @@ internal interface SessionComponent { fun inject(worker: SyncWorker) - fun inject(worker: AddHttpPusherWorker) + fun inject(worker: AddPusherWorker) fun inject(worker: SendVerificationMessageWorker) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionCoroutineScopeHolder.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionCoroutineScopeHolder.kt index 82a8f79fd5..2a741ddb9b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionCoroutineScopeHolder.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionCoroutineScopeHolder.kt @@ -20,12 +20,12 @@ import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancelChildren -import javax.inject.Inject import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.SessionLifecycleObserver +import javax.inject.Inject @SessionScope -internal class SessionCoroutineScopeHolder @Inject constructor(): SessionLifecycleObserver { +internal class SessionCoroutineScopeHolder @Inject constructor() : SessionLifecycleObserver { val scope: CoroutineScope = CoroutineScope(SupervisorJob()) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt index cb29cb4819..ebc2176a13 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt @@ -37,7 +37,7 @@ import org.matrix.android.sdk.api.session.SessionLifecycleObserver import org.matrix.android.sdk.api.session.accountdata.SessionAccountDataService import org.matrix.android.sdk.api.session.events.EventService import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService -import org.matrix.android.sdk.api.session.initsync.InitialSyncProgressService +import org.matrix.android.sdk.api.session.initsync.SyncStatusService import org.matrix.android.sdk.api.session.openid.OpenIdService import org.matrix.android.sdk.api.session.permalinks.PermalinkService import org.matrix.android.sdk.api.session.securestorage.SecureStorageService @@ -81,7 +81,7 @@ import org.matrix.android.sdk.internal.session.download.DownloadProgressIntercep import org.matrix.android.sdk.internal.session.events.DefaultEventService import org.matrix.android.sdk.internal.session.homeserver.DefaultHomeServerCapabilitiesService import org.matrix.android.sdk.internal.session.identity.DefaultIdentityService -import org.matrix.android.sdk.internal.session.initsync.DefaultInitialSyncProgressService +import org.matrix.android.sdk.internal.session.initsync.DefaultSyncStatusService import org.matrix.android.sdk.internal.session.integrationmanager.IntegrationManager import org.matrix.android.sdk.internal.session.openid.DefaultOpenIdService import org.matrix.android.sdk.internal.session.permalinks.DefaultPermalinkService @@ -163,6 +163,7 @@ internal abstract class SessionModule { @JvmStatic @Provides @SessionFilesDirectory + @SessionScope fun providesFilesDir(@UserMd5 userMd5: String, @SessionId sessionId: String, context: Context): File { @@ -355,7 +356,7 @@ internal abstract class SessionModule { abstract fun bindEventSenderProcessorAsSessionLifecycleObserver(processor: EventSenderProcessorCoroutine): SessionLifecycleObserver @Binds - abstract fun bindInitialSyncProgressService(service: DefaultInitialSyncProgressService): InitialSyncProgressService + abstract fun bindSyncStatusService(service: DefaultSyncStatusService): SyncStatusService @Binds abstract fun bindSecureStorageService(service: DefaultSecureStorageService): SecureStorageService diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/ChangePasswordTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/ChangePasswordTask.kt index 02c3735998..7b21ba2e63 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/ChangePasswordTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/ChangePasswordTask.kt @@ -45,9 +45,9 @@ internal class DefaultChangePasswordTask @Inject constructor( } catch (throwable: Throwable) { val registrationFlowResponse = throwable.toRegistrationFlowResponse() - if (registrationFlowResponse != null + if (registrationFlowResponse != null && /* Avoid infinite loop */ - && changePasswordParams.auth?.session == null) { + changePasswordParams.auth?.session == null) { // Retry with authentication executeRequest(globalErrorReceiver) { accountAPI.changePassword( diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/cache/CacheModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/cache/CacheModule.kt index 83c7d2b0b5..60adb21242 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/cache/CacheModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/cache/CacheModule.kt @@ -19,9 +19,9 @@ package org.matrix.android.sdk.internal.session.cache import dagger.Binds import dagger.Module import dagger.Provides +import io.realm.RealmConfiguration import org.matrix.android.sdk.api.session.cache.CacheService import org.matrix.android.sdk.internal.di.SessionDatabase -import io.realm.RealmConfiguration @Module internal abstract class CacheModule { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/cache/ClearCacheTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/cache/ClearCacheTask.kt index 894c3a4723..7f6b545c97 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/cache/ClearCacheTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/cache/ClearCacheTask.kt @@ -16,9 +16,9 @@ package org.matrix.android.sdk.internal.session.cache +import io.realm.RealmConfiguration import org.matrix.android.sdk.internal.database.awaitTransaction import org.matrix.android.sdk.internal.task.Task -import io.realm.RealmConfiguration import javax.inject.Inject internal interface ClearCacheTask : Task diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallEventProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallEventProcessor.kt index bdc254fc99..3f199c5cce 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallEventProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallEventProcessor.kt @@ -17,10 +17,10 @@ package org.matrix.android.sdk.internal.session.call import io.realm.Realm +import org.matrix.android.sdk.api.logger.LoggerTag import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.internal.database.model.EventInsertType -import org.matrix.android.sdk.api.logger.LoggerTag import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor import org.matrix.android.sdk.internal.session.SessionScope import timber.log.Timber @@ -29,8 +29,8 @@ import javax.inject.Inject private val loggerTag = LoggerTag("CallEventProcessor", LoggerTag.VOIP) @SessionScope -internal class CallEventProcessor @Inject constructor(private val callSignalingHandler: CallSignalingHandler) - : EventInsertLiveProcessor { +internal class CallEventProcessor @Inject constructor(private val callSignalingHandler: CallSignalingHandler) : + EventInsertLiveProcessor { private val allowedTypes = listOf( EventType.CALL_ANSWER, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/cleanup/CleanupSession.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/cleanup/CleanupSession.kt index 8870d92a35..e8d3eb1a78 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/cleanup/CleanupSession.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/cleanup/CleanupSession.kt @@ -16,22 +16,22 @@ package org.matrix.android.sdk.internal.session.cleanup -import org.matrix.android.sdk.BuildConfig +import io.realm.Realm +import io.realm.RealmConfiguration +import kotlinx.coroutines.delay import org.matrix.android.sdk.internal.SessionManager import org.matrix.android.sdk.internal.auth.SessionParamsStore import org.matrix.android.sdk.internal.crypto.CryptoModule import org.matrix.android.sdk.internal.database.RealmKeysUtils import org.matrix.android.sdk.internal.di.CryptoDatabase -import org.matrix.android.sdk.internal.di.SessionDownloadsDirectory import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.di.SessionDownloadsDirectory import org.matrix.android.sdk.internal.di.SessionFilesDirectory import org.matrix.android.sdk.internal.di.SessionId import org.matrix.android.sdk.internal.di.UserMd5 import org.matrix.android.sdk.internal.di.WorkManagerProvider import org.matrix.android.sdk.internal.session.SessionModule import org.matrix.android.sdk.internal.session.cache.ClearCacheTask -import io.realm.Realm -import io.realm.RealmConfiguration import timber.log.Timber import java.io.File import javax.inject.Inject @@ -51,37 +51,56 @@ internal class CleanupSession @Inject constructor( @UserMd5 private val userMd5: String ) { suspend fun handle() { + val sessionRealmCount = Realm.getGlobalInstanceCount(realmSessionConfiguration) + val cryptoRealmCount = Realm.getGlobalInstanceCount(realmCryptoConfiguration) + Timber.d("Realm instance ($sessionRealmCount - $cryptoRealmCount)") + Timber.d("Cleanup: delete session params...") sessionParamsStore.delete(sessionId) Timber.d("Cleanup: cancel pending works...") workManagerProvider.cancelAllWorks() + Timber.d("Cleanup: release session...") + sessionManager.releaseSession(sessionId) + Timber.d("Cleanup: clear session data...") clearSessionDataTask.execute(Unit) Timber.d("Cleanup: clear crypto data...") clearCryptoDataTask.execute(Unit) - Timber.d("Cleanup: clear file system") - sessionFiles.deleteRecursively() - sessionCache.deleteRecursively() - Timber.d("Cleanup: clear the database keys") realmKeysUtils.clear(SessionModule.getKeyAlias(userMd5)) realmKeysUtils.clear(CryptoModule.getKeyAlias(userMd5)) - Timber.d("Cleanup: release session...") - sessionManager.releaseSession(sessionId) + // Wait for all the Realm instance to be released properly. Closing Realm instance is async. + // After that we can safely delete the Realm files + waitRealmRelease() - // Sanity check - if (BuildConfig.DEBUG) { - Realm.getGlobalInstanceCount(realmSessionConfiguration) - .takeIf { it > 0 } - ?.let { Timber.e("All realm instance for session has not been closed ($it)") } - Realm.getGlobalInstanceCount(realmCryptoConfiguration) - .takeIf { it > 0 } - ?.let { Timber.e("All realm instance for crypto has not been closed ($it)") } - } + Timber.d("Cleanup: clear file system") + sessionFiles.deleteRecursively() + sessionCache.deleteRecursively() + } + + private suspend fun waitRealmRelease() { + var timeToWaitMillis = MAX_TIME_TO_WAIT_MILLIS + do { + val sessionRealmCount = Realm.getGlobalInstanceCount(realmSessionConfiguration) + val cryptoRealmCount = Realm.getGlobalInstanceCount(realmCryptoConfiguration) + Timber.d("Wait for all Realm instance to be closed ($sessionRealmCount - $cryptoRealmCount)") + if (sessionRealmCount > 0 || cryptoRealmCount > 0) { + Timber.d("Waiting ${TIME_TO_WAIT_MILLIS}ms") + delay(TIME_TO_WAIT_MILLIS) + timeToWaitMillis -= TIME_TO_WAIT_MILLIS + } else { + timeToWaitMillis = 0 + } + } while (timeToWaitMillis > 0) + } + + companion object { + private const val MAX_TIME_TO_WAIT_MILLIS = 10_000L + private const val TIME_TO_WAIT_MILLIS = 10L } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/DefaultContentUrlResolver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/DefaultContentUrlResolver.kt index e4efdaa254..5c8cf99dc6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/DefaultContentUrlResolver.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/DefaultContentUrlResolver.kt @@ -16,14 +16,14 @@ package org.matrix.android.sdk.internal.session.content +import org.matrix.android.sdk.api.MatrixUrls +import org.matrix.android.sdk.api.MatrixUrls.isMxcUrl import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig import org.matrix.android.sdk.api.session.content.ContentUrlResolver import org.matrix.android.sdk.internal.network.NetworkConstants import org.matrix.android.sdk.internal.util.ensureTrailingSlash import javax.inject.Inject -private const val MATRIX_CONTENT_URI_SCHEME = "mxc://" - internal class DefaultContentUrlResolver @Inject constructor(homeServerConnectionConfig: HomeServerConnectionConfig) : ContentUrlResolver { private val baseUrl = homeServerConnectionConfig.homeServerUriBase.toString().ensureTrailingSlash() @@ -33,7 +33,7 @@ internal class DefaultContentUrlResolver @Inject constructor(homeServerConnectio override fun resolveFullSize(contentUrl: String?): String? { return contentUrl // do not allow non-mxc content URLs - ?.takeIf { it.isValidMatrixContentUrl() } + ?.takeIf { it.isMxcUrl() } ?.let { resolve( contentUrl = it, @@ -45,7 +45,7 @@ internal class DefaultContentUrlResolver @Inject constructor(homeServerConnectio override fun resolveThumbnail(contentUrl: String?, width: Int, height: Int, method: ContentUrlResolver.ThumbnailMethod): String? { return contentUrl // do not allow non-mxc content URLs - ?.takeIf { it.isValidMatrixContentUrl() } + ?.takeIf { it.isMxcUrl() } ?.let { resolve( contentUrl = it, @@ -58,7 +58,7 @@ internal class DefaultContentUrlResolver @Inject constructor(homeServerConnectio private fun resolve(contentUrl: String, prefix: String, params: String = ""): String? { - var serverAndMediaId = contentUrl.removePrefix(MATRIX_CONTENT_URI_SCHEME) + var serverAndMediaId = contentUrl.removePrefix(MatrixUrls.MATRIX_CONTENT_URI_SCHEME) val fragmentOffset = serverAndMediaId.indexOf("#") var fragment = "" if (fragmentOffset >= 0) { @@ -68,8 +68,4 @@ internal class DefaultContentUrlResolver @Inject constructor(homeServerConnectio return baseUrl + prefix + serverAndMediaId + params + fragment } - - private fun String.isValidMatrixContentUrl(): Boolean { - return startsWith(MATRIX_CONTENT_URI_SCHEME) - } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/FileUploader.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/FileUploader.kt index 6a4dd26392..1b0ccbb489 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/FileUploader.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/FileUploader.kt @@ -35,12 +35,12 @@ import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.MatrixError import org.matrix.android.sdk.api.session.content.ContentUrlResolver import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities +import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService import org.matrix.android.sdk.internal.di.Authenticated import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.ProgressRequestBody import org.matrix.android.sdk.internal.network.awaitResponse import org.matrix.android.sdk.internal.network.toFailure -import org.matrix.android.sdk.internal.session.homeserver.DefaultHomeServerCapabilitiesService import org.matrix.android.sdk.internal.util.TemporaryFileCreator import java.io.File import java.io.FileNotFoundException @@ -50,7 +50,7 @@ import javax.inject.Inject internal class FileUploader @Inject constructor( @Authenticated private val okHttpClient: OkHttpClient, private val globalErrorReceiver: GlobalErrorReceiver, - private val homeServerCapabilitiesService: DefaultHomeServerCapabilitiesService, + private val homeServerCapabilitiesService: HomeServerCapabilitiesService, private val context: Context, private val temporaryFileCreator: TemporaryFileCreator, contentUrlResolver: ContentUrlResolver, @@ -67,8 +67,8 @@ internal class FileUploader @Inject constructor( // Check size limit val maxUploadFileSize = homeServerCapabilitiesService.getHomeServerCapabilities().maxUploadFileSize - if (maxUploadFileSize != HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN - && file.length() > maxUploadFileSize) { + if (maxUploadFileSize != HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN && + file.length() > maxUploadFileSize) { // Known limitation and file too big for the server, save the pain to upload it throw Failure.ServerError( error = MatrixError( diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ImageCompressor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ImageCompressor.kt index 9b01d0a00e..01eb52ff22 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ImageCompressor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ImageCompressor.kt @@ -20,22 +20,23 @@ import android.graphics.Bitmap import android.graphics.BitmapFactory import android.graphics.Matrix import androidx.exifinterface.media.ExifInterface -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.internal.util.TemporaryFileCreator import timber.log.Timber import java.io.File import javax.inject.Inject internal class ImageCompressor @Inject constructor( - private val temporaryFileCreator: TemporaryFileCreator + private val temporaryFileCreator: TemporaryFileCreator, + private val coroutineDispatchers: MatrixCoroutineDispatchers ) { suspend fun compress( imageFile: File, desiredWidth: Int, desiredHeight: Int, desiredQuality: Int = 80): File { - return withContext(Dispatchers.IO) { + return withContext(coroutineDispatchers.io) { val compressedBitmap = BitmapFactory.Options().run { inJustDecodeBounds = true decodeBitmap(imageFile, this) @@ -52,6 +53,8 @@ internal class ImageCompressor @Inject constructor( destinationFile.outputStream().use { compressedBitmap.compress(Bitmap.CompressFormat.JPEG, desiredQuality, it) } + }.onFailure { + return@withContext imageFile } destinationFile diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ImageExifTagRemover.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ImageExifTagRemover.kt new file mode 100644 index 0000000000..239a768498 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ImageExifTagRemover.kt @@ -0,0 +1,86 @@ +/* + * 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.internal.session.content + +import kotlinx.coroutines.withContext +import org.apache.sanselan.Sanselan +import org.apache.sanselan.formats.jpeg.JpegImageMetadata +import org.apache.sanselan.formats.jpeg.exifRewrite.ExifRewriter +import org.apache.sanselan.formats.tiff.constants.ExifTagConstants +import org.apache.sanselan.formats.tiff.constants.GPSTagConstants +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers +import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.internal.util.TemporaryFileCreator +import java.io.BufferedOutputStream +import java.io.File +import java.io.FileOutputStream +import javax.inject.Inject + +/** + * This class is responsible for removing Exif tags from image files + */ + +internal class ImageExifTagRemover @Inject constructor( + private val temporaryFileCreator: TemporaryFileCreator, + private val coroutineDispatchers: MatrixCoroutineDispatchers +) { + + /** + * Remove sensitive exif tags from a jpeg image file. + * Scrubbing exif tags like GPS location and user comments + * @param jpegImageFile The image file to be scrubbed + * @return the new scrubbed image file, or the original file if the operation failed + */ + suspend fun removeSensitiveJpegExifTags(jpegImageFile: File): File = withContext(coroutineDispatchers.io) { + val outputSet = tryOrNull("Unable to read JpegImageMetadata") { + (Sanselan.getMetadata(jpegImageFile) as? JpegImageMetadata)?.exif?.outputSet + } ?: return@withContext jpegImageFile + + tryOrNull("Unable to remove ExifData") { + outputSet.removeField(ExifTagConstants.EXIF_TAG_GPSINFO) + outputSet.removeField(ExifTagConstants.EXIF_TAG_SUBJECT_LOCATION_1) + outputSet.removeField(ExifTagConstants.EXIF_TAG_SUBJECT_LOCATION_2) + outputSet.removeField(ExifTagConstants.EXIF_TAG_USER_COMMENT) + outputSet.removeField(GPSTagConstants.GPS_TAG_GPS_ALTITUDE) + outputSet.removeField(GPSTagConstants.GPS_TAG_GPS_ALTITUDE_REF) + outputSet.removeField(GPSTagConstants.GPS_TAG_GPS_LONGITUDE) + outputSet.removeField(GPSTagConstants.GPS_TAG_GPS_LONGITUDE_REF) + outputSet.removeField(GPSTagConstants.GPS_TAG_GPS_DEST_LONGITUDE) + outputSet.removeField(GPSTagConstants.GPS_TAG_GPS_DEST_LONGITUDE_REF) + outputSet.removeField(GPSTagConstants.GPS_TAG_GPS_LATITUDE) + outputSet.removeField(GPSTagConstants.GPS_TAG_GPS_LATITUDE_REF) + outputSet.removeField(GPSTagConstants.GPS_TAG_GPS_DEST_LATITUDE) + outputSet.removeField(GPSTagConstants.GPS_TAG_GPS_DEST_LATITUDE_REF) + } ?: return@withContext jpegImageFile + + val scrubbedFile = temporaryFileCreator.create() + return@withContext runCatching { + FileOutputStream(scrubbedFile).use { fos -> + val outputStream = BufferedOutputStream(fos) + ExifRewriter().updateExifMetadataLossless(jpegImageFile, outputStream, outputSet) + } + }.fold( + onSuccess = { + scrubbedFile + }, + onFailure = { + scrubbedFile.delete() + jpegImageFile + } + ) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt index f14c85cf80..b657d950bd 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt @@ -63,8 +63,8 @@ private data class NewAttachmentAttributes( * Possible previous worker: None * Possible next worker : Always [MultipleEventSendingDispatcherWorker] */ -internal class UploadContentWorker(val context: Context, params: WorkerParameters) - : SessionSafeCoroutineWorker(context, params, Params::class.java) { +internal class UploadContentWorker(val context: Context, params: WorkerParameters) : + SessionSafeCoroutineWorker(context, params, Params::class.java) { @JsonClass(generateAdapter = true) internal data class Params( @@ -81,6 +81,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter @Inject lateinit var fileService: DefaultFileService @Inject lateinit var cancelSendTracker: CancelSendTracker @Inject lateinit var imageCompressor: ImageCompressor + @Inject lateinit var imageExitTagRemover: ImageExifTagRemover @Inject lateinit var videoCompressor: VideoCompressor @Inject lateinit var thumbnailExtractor: ThumbnailExtractor @Inject lateinit var localEchoRepository: LocalEchoRepository @@ -114,7 +115,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter } val attachment = params.attachment - val filesToDelete = mutableListOf() + val filesToDelete = hashSetOf() return try { val inputStream = context.contentResolver.openInputStream(attachment.queryUri) @@ -157,10 +158,10 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter params.attachment.size ) - if (attachment.type == ContentAttachmentData.Type.IMAGE + if (attachment.type == ContentAttachmentData.Type.IMAGE && // Do not compress gif - && attachment.mimeType != MimeTypes.Gif - && params.compressBeforeSending) { + attachment.mimeType != MimeTypes.Gif && + params.compressBeforeSending) { notifyTracker(params) { contentUploadStateTracker.setCompressingImage(it) } fileToUpload = imageCompressor.compress(workingFile, MAX_IMAGE_SIZE, MAX_IMAGE_SIZE) @@ -177,10 +178,10 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter } } .also { filesToDelete.add(it) } - } else if (attachment.type == ContentAttachmentData.Type.VIDEO + } else if (attachment.type == ContentAttachmentData.Type.VIDEO && // Do not compress gif - && attachment.mimeType != MimeTypes.Gif - && params.compressBeforeSending) { + attachment.mimeType != MimeTypes.Gif && + params.compressBeforeSending) { fileToUpload = videoCompressor.compress(workingFile, object : ProgressListener { override fun onProgress(progress: Int, total: Int) { notifyTracker(params) { contentUploadStateTracker.setCompressingVideo(it, progress.toFloat()) } @@ -213,12 +214,19 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter .also { filesToDelete.add(it) } } VideoCompressionResult.CompressionNotNeeded, - VideoCompressionResult.CompressionCancelled, + VideoCompressionResult.CompressionCancelled -> { + workingFile + } is VideoCompressionResult.CompressionFailed -> { + Timber.e(videoCompressionResult.failure, "Video compression failed") workingFile } } } + } else if (attachment.type == ContentAttachmentData.Type.IMAGE && !params.compressBeforeSending) { + fileToUpload = imageExitTagRemover.removeSensitiveJpegExifTags(workingFile) + .also { filesToDelete.add(it) } + newAttachmentAttributes = newAttachmentAttributes.copy(newFileSize = fileToUpload.length()) } else { fileToUpload = workingFile // Fix: OpenableColumns.SIZE may return -1 or 0 @@ -291,6 +299,11 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter filesToDelete.forEach { tryOrNull { it.delete() } } + + // Delete the temporary voice message file + if (params.attachment.type == ContentAttachmentData.Type.AUDIO && params.attachment.mimeType == MimeTypes.Ogg) { + context.contentResolver.delete(params.attachment.queryUri, null, null) + } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/VideoCompressor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/VideoCompressor.kt index 05aaf4e9f1..a43f8abf33 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/VideoCompressor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/VideoCompressor.kt @@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session.content import com.otaliastudios.transcoder.Transcoder import com.otaliastudios.transcoder.TranscoderListener +import com.otaliastudios.transcoder.source.FilePathDataSource import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.withContext @@ -43,7 +44,16 @@ internal class VideoCompressor @Inject constructor( var result: Int = -1 var failure: Throwable? = null Transcoder.into(destinationFile.path) - .addDataSource(videoFile.path) + .addDataSource(object : FilePathDataSource(videoFile.path) { + // https://github.com/natario1/Transcoder/issues/154 + @Suppress("SENSELESS_COMPARISON") // Source is annotated as @NonNull, but can actually be null... + override fun isInitialized(): Boolean { + if (source == null) { + return false + } + return super.isInitialized() + } + }) .setListener(object : TranscoderListener { override fun onTranscodeProgress(progress: Double) { Timber.d("Compressing: $progress%") diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/displayname/DisplayNameResolver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/displayname/DisplayNameResolver.kt new file mode 100644 index 0000000000..76d956f9a5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/displayname/DisplayNameResolver.kt @@ -0,0 +1,37 @@ +/* + * 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.internal.session.displayname + +import org.matrix.android.sdk.api.MatrixConfiguration +import org.matrix.android.sdk.api.util.MatrixItem +import javax.inject.Inject + +internal class DisplayNameResolver @Inject constructor( + private val matrixConfiguration: MatrixConfiguration +) { + fun getBestName(matrixItem: MatrixItem): String { + return if (matrixItem is MatrixItem.GroupItem || matrixItem is MatrixItem.RoomAliasItem) { + // Best name is the id, and we keep the displayName of the room for the case we need the first letter + matrixItem.id + } else { + matrixItem.displayName?.takeIf { it.isNotBlank() } + ?: matrixConfiguration.matrixItemDisplayNameFallbackProvider?.getDefaultName(matrixItem) + ?: matrixItem.id + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/DefaultFilterRepository.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/DefaultFilterRepository.kt index 095c39a485..1d1bb0e715 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/DefaultFilterRepository.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/DefaultFilterRepository.kt @@ -17,14 +17,14 @@ package org.matrix.android.sdk.internal.session.filter import com.zhuinden.monarchy.Monarchy +import io.realm.Realm +import io.realm.kotlin.where import org.matrix.android.sdk.internal.database.model.FilterEntity import org.matrix.android.sdk.internal.database.model.FilterEntityFields import org.matrix.android.sdk.internal.database.query.get import org.matrix.android.sdk.internal.database.query.getOrCreate import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.util.awaitTransaction -import io.realm.Realm -import io.realm.kotlin.where import javax.inject.Inject internal class DefaultFilterRepository @Inject constructor(@SessionDatabase private val monarchy: Monarchy) : FilterRepository { @@ -33,9 +33,9 @@ internal class DefaultFilterRepository @Inject constructor(@SessionDatabase priv return Realm.getInstance(monarchy.realmConfiguration).use { realm -> val filterEntity = FilterEntity.get(realm) // Filter has changed, or no filter Id yet - filterEntity == null - || filterEntity.filterBodyJson != filter.toJSONString() - || filterEntity.filterId.isBlank() + filterEntity == null || + filterEntity.filterBodyJson != filter.toJSONString() || + filterEntity.filterId.isBlank() }.also { hasChanged -> if (hasChanged) { // Filter is new or has changed, store it and reset the filter Id. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/EventFilter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/EventFilter.kt index 1be62304a1..37630ef8ba 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/EventFilter.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/EventFilter.kt @@ -50,10 +50,10 @@ data class EventFilter( @Json(name = "not_types") val notTypes: List? = null ) { fun hasData(): Boolean { - return limit != null - || senders != null - || notSenders != null - || types != null - || notTypes != null + return limit != null || + senders != null || + notSenders != null || + types != null || + notTypes != null } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/RoomEventFilter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/RoomEventFilter.kt index d6089f9f5b..7047d38260 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/RoomEventFilter.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/RoomEventFilter.kt @@ -73,14 +73,14 @@ data class RoomEventFilter( } fun hasData(): Boolean { - return (limit != null - || notSenders != null - || notTypes != null - || senders != null - || types != null - || rooms != null - || notRooms != null - || containsUrl != null - || lazyLoadMembers != null) + return (limit != null || + notSenders != null || + notTypes != null || + senders != null || + types != null || + rooms != null || + notRooms != null || + containsUrl != null || + lazyLoadMembers != null) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/RoomFilter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/RoomFilter.kt index fbf22fde51..2c56a30d39 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/RoomFilter.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/RoomFilter.kt @@ -59,12 +59,12 @@ data class RoomFilter( ) { fun hasData(): Boolean { - return (notRooms != null - || rooms != null - || ephemeral != null - || includeLeave != null - || state != null - || timeline != null - || accountData != null) + return (notRooms != null || + rooms != null || + ephemeral != null || + includeLeave != null || + state != null || + timeline != null || + accountData != null) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/DefaultGroupService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/DefaultGroupService.kt index 425d6a9aca..9334d09377 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/DefaultGroupService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/DefaultGroupService.kt @@ -18,6 +18,8 @@ package org.matrix.android.sdk.internal.session.group import androidx.lifecycle.LiveData import com.zhuinden.monarchy.Monarchy +import io.realm.Realm +import io.realm.RealmQuery import org.matrix.android.sdk.api.session.group.Group import org.matrix.android.sdk.api.session.group.GroupService import org.matrix.android.sdk.api.session.group.GroupSummaryQueryParams @@ -28,14 +30,16 @@ import org.matrix.android.sdk.internal.database.model.GroupSummaryEntity import org.matrix.android.sdk.internal.database.model.GroupSummaryEntityFields import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.query.QueryStringValueProcessor import org.matrix.android.sdk.internal.query.process import org.matrix.android.sdk.internal.util.fetchCopyMap -import io.realm.Realm -import io.realm.RealmQuery import javax.inject.Inject -internal class DefaultGroupService @Inject constructor(@SessionDatabase private val monarchy: Monarchy, - private val groupFactory: GroupFactory) : GroupService { +internal class DefaultGroupService @Inject constructor( + @SessionDatabase private val monarchy: Monarchy, + private val groupFactory: GroupFactory, + private val queryStringValueProcessor: QueryStringValueProcessor, +) : GroupService { override fun getGroup(groupId: String): Group? { return Realm.getInstance(monarchy.realmConfiguration).use { realm -> @@ -67,8 +71,10 @@ internal class DefaultGroupService @Inject constructor(@SessionDatabase private } private fun groupSummariesQuery(realm: Realm, queryParams: GroupSummaryQueryParams): RealmQuery { - return GroupSummaryEntity.where(realm) - .process(GroupSummaryEntityFields.DISPLAY_NAME, queryParams.displayName) - .process(GroupSummaryEntityFields.MEMBERSHIP_STR, queryParams.memberships) + return with(queryStringValueProcessor) { + GroupSummaryEntity.where(realm) + .process(GroupSummaryEntityFields.DISPLAY_NAME, queryParams.displayName) + .process(GroupSummaryEntityFields.MEMBERSHIP_STR, queryParams.memberships) + } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/GetGroupDataWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/GetGroupDataWorker.kt index d6b9363d54..338f43bdbb 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/GetGroupDataWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/GetGroupDataWorker.kt @@ -28,8 +28,8 @@ import javax.inject.Inject * Possible previous worker: None * Possible next worker : None */ -internal class GetGroupDataWorker(context: Context, params: WorkerParameters) - : SessionSafeCoroutineWorker(context, params, Params::class.java) { +internal class GetGroupDataWorker(context: Context, params: WorkerParameters) : + SessionSafeCoroutineWorker(context, params, Params::class.java) { @JsonClass(generateAdapter = true) internal data class Params( diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/HomeServerPinger.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/HomeServerPinger.kt index bb526adf4a..70e1e551aa 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/HomeServerPinger.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/HomeServerPinger.kt @@ -16,10 +16,10 @@ package org.matrix.android.sdk.internal.session.homeserver +import kotlinx.coroutines.launch import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.task.TaskExecutor -import kotlinx.coroutines.launch import javax.inject.Inject internal class HomeServerPinger @Inject constructor(private val taskExecutor: TaskExecutor, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/DefaultIdentityService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/DefaultIdentityService.kt index fdb6caf53f..c8a9c0f09a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/DefaultIdentityService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/DefaultIdentityService.kt @@ -20,10 +20,17 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleRegistry import dagger.Lazy +import kotlinx.coroutines.withContext +import okhttp3.OkHttpClient +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.auth.data.SessionParams +import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.MatrixError +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.SessionLifecycleObserver +import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService import org.matrix.android.sdk.api.session.identity.FoundThreePid @@ -36,23 +43,16 @@ import org.matrix.android.sdk.internal.di.AuthenticatedIdentity import org.matrix.android.sdk.internal.di.UnauthenticatedWithCertificate import org.matrix.android.sdk.internal.extensions.observeNotNull import org.matrix.android.sdk.internal.network.RetrofitFactory -import org.matrix.android.sdk.api.session.SessionLifecycleObserver import org.matrix.android.sdk.internal.session.SessionScope import org.matrix.android.sdk.internal.session.identity.data.IdentityStore +import org.matrix.android.sdk.internal.session.identity.model.SignInvitationResult import org.matrix.android.sdk.internal.session.openid.GetOpenIdTokenTask import org.matrix.android.sdk.internal.session.profile.BindThreePidsTask import org.matrix.android.sdk.internal.session.profile.UnbindThreePidsTask import org.matrix.android.sdk.internal.session.sync.model.accountdata.IdentityServerContent -import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes -import org.matrix.android.sdk.internal.session.user.accountdata.UserAccountDataDataSource import org.matrix.android.sdk.internal.session.user.accountdata.UpdateUserAccountDataTask -import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers +import org.matrix.android.sdk.internal.session.user.accountdata.UserAccountDataDataSource import org.matrix.android.sdk.internal.util.ensureProtocol -import kotlinx.coroutines.withContext -import okhttp3.OkHttpClient -import org.matrix.android.sdk.api.extensions.orFalse -import org.matrix.android.sdk.api.session.Session -import org.matrix.android.sdk.internal.session.identity.model.SignInvitationResult import timber.log.Timber import javax.inject.Inject import javax.net.ssl.HttpsURLConnection @@ -80,7 +80,7 @@ internal class DefaultIdentityService @Inject constructor( private val identityApiProvider: IdentityApiProvider, private val accountDataDataSource: UserAccountDataDataSource, private val homeServerCapabilitiesService: HomeServerCapabilitiesService, - private val sign3pidInvitationTask: DefaultSign3pidInvitationTask, + private val sign3pidInvitationTask: Sign3pidInvitationTask, private val sessionParams: SessionParams ) : IdentityService, SessionLifecycleObserver { @@ -202,6 +202,8 @@ internal class DefaultIdentityService @Inject constructor( identityStore.setUrl(urlCandidate) identityStore.setToken(token) + // could we remember if it was previously given? + identityStore.setUserConsent(false) updateIdentityAPI(urlCandidate) updateAccountData(urlCandidate) @@ -230,6 +232,8 @@ internal class DefaultIdentityService @Inject constructor( } override suspend fun lookUp(threePids: List): List { + if (getCurrentIdentityServerUrl() == null) throw IdentityServiceError.NoIdentityServerConfigured + if (!getUserConsent()) { throw IdentityServiceError.UserConsentNotProvided } @@ -316,12 +320,12 @@ internal class DefaultIdentityService @Inject constructor( } private fun Throwable.isInvalidToken(): Boolean { - return this is Failure.ServerError - && httpCode == HttpsURLConnection.HTTP_UNAUTHORIZED /* 401 */ + return this is Failure.ServerError && + httpCode == HttpsURLConnection.HTTP_UNAUTHORIZED /* 401 */ } private fun Throwable.isTermsNotSigned(): Boolean { - return this is Failure.ServerError - && httpCode == HttpsURLConnection.HTTP_FORBIDDEN /* 403 */ - && error.code == MatrixError.M_TERMS_NOT_SIGNED + return this is Failure.ServerError && + httpCode == HttpsURLConnection.HTTP_FORBIDDEN && /* 403 */ + error.code == MatrixError.M_TERMS_NOT_SIGNED } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/EnsureIdentityToken.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/EnsureIdentityToken.kt index 5e1434403e..657d1f3ac7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/EnsureIdentityToken.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/EnsureIdentityToken.kt @@ -17,13 +17,13 @@ package org.matrix.android.sdk.internal.session.identity import dagger.Lazy +import okhttp3.OkHttpClient import org.matrix.android.sdk.api.session.identity.IdentityServiceError import org.matrix.android.sdk.internal.di.UnauthenticatedWithCertificate import org.matrix.android.sdk.internal.network.RetrofitFactory import org.matrix.android.sdk.internal.session.identity.data.IdentityStore import org.matrix.android.sdk.internal.session.openid.GetOpenIdTokenTask import org.matrix.android.sdk.internal.task.Task -import okhttp3.OkHttpClient import javax.inject.Inject internal interface EnsureIdentityTokenTask : Task diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityBulkLookupTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityBulkLookupTask.kt index 114695062c..f6ef370f8d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityBulkLookupTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityBulkLookupTask.kt @@ -117,8 +117,8 @@ internal class DefaultIdentityBulkLookupTask @Inject constructor( return withOlmUtility { olmUtility -> threePids.map { threePid -> base64ToBase64Url( - olmUtility.sha256(threePid.value.lowercase(Locale.ROOT) - + " " + threePid.toMedium() + " " + pepper) + olmUtility.sha256(threePid.value.lowercase(Locale.ROOT) + + " " + threePid.toMedium() + " " + pepper) ) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityModule.kt index 4d664b76be..65794e6b14 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityModule.kt @@ -19,6 +19,9 @@ package org.matrix.android.sdk.internal.session.identity import dagger.Binds import dagger.Module import dagger.Provides +import io.realm.RealmConfiguration +import okhttp3.OkHttpClient +import org.matrix.android.sdk.api.session.identity.IdentityService import org.matrix.android.sdk.internal.database.RealmKeysUtils import org.matrix.android.sdk.internal.di.AuthenticatedIdentity import org.matrix.android.sdk.internal.di.IdentityDatabase @@ -32,8 +35,6 @@ import org.matrix.android.sdk.internal.session.SessionScope import org.matrix.android.sdk.internal.session.identity.data.IdentityStore import org.matrix.android.sdk.internal.session.identity.db.IdentityRealmModule import org.matrix.android.sdk.internal.session.identity.db.RealmIdentityStore -import io.realm.RealmConfiguration -import okhttp3.OkHttpClient import org.matrix.android.sdk.internal.session.identity.db.RealmIdentityStoreMigration import java.io.File @@ -75,6 +76,9 @@ internal abstract class IdentityModule { } } + @Binds + abstract fun bindIdentityService(service: DefaultIdentityService): IdentityService + @Binds @AuthenticatedIdentity abstract fun bindAccessTokenProvider(provider: IdentityAccessTokenProvider): AccessTokenProvider diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/IdentityPendingBindingEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/IdentityPendingBindingEntity.kt index be68e17a49..fcc91b0121 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/IdentityPendingBindingEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/IdentityPendingBindingEntity.kt @@ -16,10 +16,10 @@ package org.matrix.android.sdk.internal.session.identity.db -import org.matrix.android.sdk.api.session.identity.ThreePid -import org.matrix.android.sdk.api.session.identity.toMedium import io.realm.RealmObject import io.realm.annotations.PrimaryKey +import org.matrix.android.sdk.api.session.identity.ThreePid +import org.matrix.android.sdk.api.session.identity.toMedium internal open class IdentityPendingBindingEntity( @PrimaryKey var threePid: String = "", diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/IdentityPendingBindingEntityQuery.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/IdentityPendingBindingEntityQuery.kt index aa2f4dd5b2..8d4afc7beb 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/IdentityPendingBindingEntityQuery.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/IdentityPendingBindingEntityQuery.kt @@ -16,10 +16,10 @@ package org.matrix.android.sdk.internal.session.identity.db -import org.matrix.android.sdk.api.session.identity.ThreePid import io.realm.Realm import io.realm.kotlin.createObject import io.realm.kotlin.where +import org.matrix.android.sdk.api.session.identity.ThreePid internal fun IdentityPendingBindingEntity.Companion.get(realm: Realm, threePid: ThreePid): IdentityPendingBindingEntity? { return realm.where() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/RealmIdentityStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/RealmIdentityStore.kt index 2fa3fc0cfb..ce8b78b2d0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/RealmIdentityStore.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/RealmIdentityStore.kt @@ -16,15 +16,15 @@ package org.matrix.android.sdk.internal.session.identity.db +import io.realm.Realm +import io.realm.RealmConfiguration import org.matrix.android.sdk.api.session.identity.ThreePid import org.matrix.android.sdk.internal.di.IdentityDatabase import org.matrix.android.sdk.internal.session.SessionScope -import org.matrix.android.sdk.internal.session.identity.data.IdentityPendingBinding import org.matrix.android.sdk.internal.session.identity.data.IdentityData +import org.matrix.android.sdk.internal.session.identity.data.IdentityPendingBinding import org.matrix.android.sdk.internal.session.identity.data.IdentityStore import org.matrix.android.sdk.internal.session.identity.model.IdentityHashDetailResponse -import io.realm.Realm -import io.realm.RealmConfiguration import javax.inject.Inject @SessionScope diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/initsync/DefaultInitialSyncProgressService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/initsync/DefaultSyncStatusService.kt similarity index 78% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/initsync/DefaultInitialSyncProgressService.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/initsync/DefaultSyncStatusService.kt index eb3e3066b1..079b0d0115 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/initsync/DefaultInitialSyncProgressService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/initsync/DefaultSyncStatusService.kt @@ -18,23 +18,28 @@ package org.matrix.android.sdk.internal.session.initsync import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import org.matrix.android.sdk.api.session.initsync.InitSyncStep -import org.matrix.android.sdk.api.session.initsync.InitialSyncProgressService +import org.matrix.android.sdk.api.session.initsync.SyncStatusService import org.matrix.android.sdk.internal.session.SessionScope import javax.inject.Inject @SessionScope -internal class DefaultInitialSyncProgressService @Inject constructor() - : InitialSyncProgressService, +internal class DefaultSyncStatusService @Inject constructor() : + SyncStatusService, ProgressReporter { - private val status = MutableLiveData() + private val status = MutableLiveData() private var rootTask: TaskInfo? = null - override fun getInitialSyncProgressStatus(): LiveData { + override fun getSyncStatusLive(): LiveData { return status } + // Only to be used for incremental sync + fun setStatus(newStatus: SyncStatusService.Status.IncrementalSyncStatus) { + status.postValue(newStatus) + } + /** * Create a rootTask */ @@ -67,7 +72,7 @@ internal class DefaultInitialSyncProgressService @Inject constructor() // Update the progress of the leaf and all its parents leaf.setProgress(progress) // Then update the live data using leaf wording and root progress - status.postValue(InitialSyncProgressService.Status.Progressing(leaf.initSyncStep, root.currentProgress.toInt())) + status.postValue(SyncStatusService.Status.Progressing(leaf.initSyncStep, root.currentProgress.toInt())) } } } @@ -82,13 +87,13 @@ internal class DefaultInitialSyncProgressService @Inject constructor() // And close it endedTask.parent.child = null } else { - status.postValue(InitialSyncProgressService.Status.Idle) + status.postValue(SyncStatusService.Status.Idle) } } } fun endAll() { rootTask = null - status.postValue(InitialSyncProgressService.Status.Idle) + status.postValue(SyncStatusService.Status.Idle) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/IntegrationManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/IntegrationManager.kt index b654b8610d..30b1589169 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/IntegrationManager.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/IntegrationManager.kt @@ -22,6 +22,9 @@ import androidx.lifecycle.LifecycleRegistry import com.zhuinden.monarchy.Monarchy import org.matrix.android.sdk.api.MatrixConfiguration import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.SessionLifecycleObserver +import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent +import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerConfig import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerService @@ -30,12 +33,9 @@ import org.matrix.android.sdk.api.session.widgets.model.WidgetType import org.matrix.android.sdk.internal.database.model.WellknownIntegrationManagerConfigEntity import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.extensions.observeNotNull -import org.matrix.android.sdk.api.session.SessionLifecycleObserver import org.matrix.android.sdk.internal.session.SessionScope -import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes -import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent -import org.matrix.android.sdk.internal.session.user.accountdata.UserAccountDataDataSource import org.matrix.android.sdk.internal.session.user.accountdata.UpdateUserAccountDataTask +import org.matrix.android.sdk.internal.session.user.accountdata.UserAccountDataDataSource import org.matrix.android.sdk.internal.session.widgets.helper.WidgetFactory import org.matrix.android.sdk.internal.session.widgets.helper.extractWidgetSequence import timber.log.Timber @@ -58,8 +58,8 @@ internal class IntegrationManager @Inject constructor(matrixConfiguration: Matri @SessionDatabase private val monarchy: Monarchy, private val updateUserAccountDataTask: UpdateUserAccountDataTask, private val accountDataDataSource: UserAccountDataDataSource, - private val widgetFactory: WidgetFactory) - : SessionLifecycleObserver { + private val widgetFactory: WidgetFactory) : + SessionLifecycleObserver { private val currentConfigs = ArrayList() private val lifecycleOwner: LifecycleOwner = LifecycleOwner { lifecycleRegistry } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/IntegrationManagerConfigExtractor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/IntegrationManagerConfigExtractor.kt index f7a8b68515..8c63dbeb0d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/IntegrationManagerConfigExtractor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/IntegrationManagerConfigExtractor.kt @@ -29,9 +29,9 @@ internal class IntegrationManagerConfigExtractor @Inject constructor() { (config as? Map<*, *>)?.let { map -> val apiUrl = map["api_url"] as? String val uiUrl = map["ui_url"] as? String ?: apiUrl - if (apiUrl != null - && apiUrl.startsWith("https://") - && uiUrl!!.startsWith("https://")) { + if (apiUrl != null && + apiUrl.startsWith("https://") && + uiUrl!!.startsWith("https://")) { return WellknownIntegrationManagerConfigEntity( apiUrl = apiUrl, uiUrl = uiUrl diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/UrlsExtractor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/UrlsExtractor.kt index d1fb5b98ff..85a126ef78 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/UrlsExtractor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/UrlsExtractor.kt @@ -33,9 +33,9 @@ internal class UrlsExtractor @Inject constructor() { return event.takeIf { it.root.getClearType() == EventType.MESSAGE } ?.getLastMessageContent() ?.takeIf { - it.msgType == MessageType.MSGTYPE_TEXT - || it.msgType == MessageType.MSGTYPE_NOTICE - || it.msgType == MessageType.MSGTYPE_EMOTE + it.msgType == MessageType.MSGTYPE_TEXT || + it.msgType == MessageType.MSGTYPE_NOTICE || + it.msgType == MessageType.MSGTYPE_EMOTE } ?.let { messageContent -> if (event.isReply()) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/ProcessEventForPushTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/ProcessEventForPushTask.kt index 0ece07fc15..3c74888eda 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/ProcessEventForPushTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/ProcessEventForPushTask.kt @@ -18,8 +18,9 @@ package org.matrix.android.sdk.internal.session.notification import org.matrix.android.sdk.api.pushrules.rest.PushRule import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.isInvitation +import org.matrix.android.sdk.api.session.sync.model.RoomsSyncResponse import org.matrix.android.sdk.internal.di.UserId -import org.matrix.android.sdk.internal.session.sync.model.RoomsSyncResponse import org.matrix.android.sdk.internal.task.Task import timber.log.Timber import javax.inject.Inject @@ -48,14 +49,18 @@ internal class DefaultProcessEventForPushTask @Inject constructor( } val newJoinEvents = params.syncResponse.join .mapNotNull { (key, value) -> - value.timeline?.events?.map { it.copy(roomId = key) } + value.timeline?.events?.mapNotNull { + it.takeIf { !it.isInvitation() }?.copy(roomId = key) + } } .flatten() + val inviteEvents = params.syncResponse.invite .mapNotNull { (key, value) -> value.inviteState?.events?.map { it.copy(roomId = key) } } .flatten() + val allEvents = (newJoinEvents + inviteEvents).filter { event -> when (event.type) { EventType.MESSAGE, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/DefaultOpenIdService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/DefaultOpenIdService.kt index b90a2435f7..afff0f3515 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/DefaultOpenIdService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/DefaultOpenIdService.kt @@ -20,7 +20,7 @@ import org.matrix.android.sdk.api.session.openid.OpenIdService import org.matrix.android.sdk.api.session.openid.OpenIdToken import javax.inject.Inject -internal class DefaultOpenIdService @Inject constructor(private val getOpenIdTokenTask: GetOpenIdTokenTask): OpenIdService { +internal class DefaultOpenIdService @Inject constructor(private val getOpenIdTokenTask: GetOpenIdTokenTask) : OpenIdService { override suspend fun getOpenIdToken(): OpenIdToken { return getOpenIdTokenTask.execute(Unit) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/DefaultPermalinkService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/DefaultPermalinkService.kt index 134da4ce51..144ebb5404 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/DefaultPermalinkService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/DefaultPermalinkService.kt @@ -18,33 +18,29 @@ package org.matrix.android.sdk.internal.session.permalinks import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.permalinks.PermalinkService -import org.matrix.android.sdk.api.session.permalinks.PermalinkService.Companion.MATRIX_TO_URL_BASE import javax.inject.Inject internal class DefaultPermalinkService @Inject constructor( private val permalinkFactory: PermalinkFactory ) : PermalinkService { - override fun createPermalink(event: Event): String? { - return permalinkFactory.createPermalink(event) + override fun createPermalink(event: Event, forceMatrixTo: Boolean): String? { + return permalinkFactory.createPermalink(event, forceMatrixTo) } - override fun createPermalink(id: String): String? { - return permalinkFactory.createPermalink(id) + override fun createPermalink(id: String, forceMatrixTo: Boolean): String? { + return permalinkFactory.createPermalink(id, forceMatrixTo) } - override fun createRoomPermalink(roomId: String, viaServers: List?): String? { - return permalinkFactory.createRoomPermalink(roomId, viaServers) + override fun createRoomPermalink(roomId: String, viaServers: List?, forceMatrixTo: Boolean): String? { + return permalinkFactory.createRoomPermalink(roomId, viaServers, forceMatrixTo) } - override fun createPermalink(roomId: String, eventId: String): String { - return permalinkFactory.createPermalink(roomId, eventId) + override fun createPermalink(roomId: String, eventId: String, forceMatrixTo: Boolean): String { + return permalinkFactory.createPermalink(roomId, eventId, forceMatrixTo) } override fun getLinkedId(url: String): String? { - return url - .takeIf { it.startsWith(MATRIX_TO_URL_BASE) } - ?.substring(MATRIX_TO_URL_BASE.length) - ?.substringBeforeLast("?") + return permalinkFactory.getLinkedId(url) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/PermalinkFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/PermalinkFactory.kt index 639e45582a..39c1ddfdce 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/PermalinkFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/PermalinkFactory.kt @@ -16,7 +16,11 @@ package org.matrix.android.sdk.internal.session.permalinks +import org.matrix.android.sdk.api.MatrixConfiguration +import org.matrix.android.sdk.api.MatrixPatterns import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.permalinks.PermalinkData +import org.matrix.android.sdk.api.session.permalinks.PermalinkParser import org.matrix.android.sdk.api.session.permalinks.PermalinkService.Companion.MATRIX_TO_URL_BASE import org.matrix.android.sdk.internal.di.UserId import javax.inject.Inject @@ -24,28 +28,44 @@ import javax.inject.Inject internal class PermalinkFactory @Inject constructor( @UserId private val userId: String, - private val viaParameterFinder: ViaParameterFinder + private val viaParameterFinder: ViaParameterFinder, + private val matrixConfiguration: MatrixConfiguration ) { - fun createPermalink(event: Event): String? { + fun createPermalink(event: Event, forceMatrixTo: Boolean): String? { if (event.roomId.isNullOrEmpty() || event.eventId.isNullOrEmpty()) { return null } - return createPermalink(event.roomId, event.eventId) + return createPermalink(event.roomId, event.eventId, forceMatrixTo) } - fun createPermalink(id: String): String? { - return if (id.isEmpty()) { - null - } else MATRIX_TO_URL_BASE + escape(id) + fun createPermalink(id: String, forceMatrixTo: Boolean): String? { + return when { + id.isEmpty() -> null + !useClientFormat(forceMatrixTo) -> MATRIX_TO_URL_BASE + escape(id) + else -> { + buildString { + append(matrixConfiguration.clientPermalinkBaseUrl) + when { + MatrixPatterns.isRoomId(id) || MatrixPatterns.isRoomAlias(id) -> append(ROOM_PATH) + MatrixPatterns.isUserId(id) -> append(USER_PATH) + MatrixPatterns.isGroupId(id) -> append(GROUP_PATH) + } + append(escape(id)) + } + } + } } - fun createRoomPermalink(roomId: String, via: List? = null): String? { + fun createRoomPermalink(roomId: String, via: List? = null, forceMatrixTo: Boolean): String? { return if (roomId.isEmpty()) { null } else { buildString { - append(MATRIX_TO_URL_BASE) + append(baseUrl(forceMatrixTo)) + if (useClientFormat(forceMatrixTo)) { + append(ROOM_PATH) + } append(escape(roomId)) append( via?.takeIf { it.isNotEmpty() }?.let { viaParameterFinder.asUrlViaParameters(it) } @@ -55,16 +75,34 @@ internal class PermalinkFactory @Inject constructor( } } - fun createPermalink(roomId: String, eventId: String): String { - return MATRIX_TO_URL_BASE + escape(roomId) + "/" + escape(eventId) + viaParameterFinder.computeViaParams(userId, roomId) + fun createPermalink(roomId: String, eventId: String, forceMatrixTo: Boolean): String { + return buildString { + append(baseUrl(forceMatrixTo)) + if (useClientFormat(forceMatrixTo)) { + append(ROOM_PATH) + } + append(escape(roomId)) + append("/") + append(escape(eventId)) + append(viaParameterFinder.computeViaParams(userId, roomId)) + } } fun getLinkedId(url: String): String? { - val isSupported = url.startsWith(MATRIX_TO_URL_BASE) - - return if (isSupported) { - url.substring(MATRIX_TO_URL_BASE.length) - } else null + val clientBaseUrl = matrixConfiguration.clientPermalinkBaseUrl + return when { + url.startsWith(MATRIX_TO_URL_BASE) -> url.substring(MATRIX_TO_URL_BASE.length) + clientBaseUrl != null && url.startsWith(clientBaseUrl) -> { + when (PermalinkParser.parse(url)) { + is PermalinkData.GroupLink -> url.substring(clientBaseUrl.length + GROUP_PATH.length) + is PermalinkData.RoomLink -> url.substring(clientBaseUrl.length + ROOM_PATH.length) + is PermalinkData.UserLink -> url.substring(clientBaseUrl.length + USER_PATH.length) + else -> null + } + } + else -> null + } + ?.substringBeforeLast("?") } /** @@ -86,4 +124,28 @@ internal class PermalinkFactory @Inject constructor( private fun unescape(id: String): String { return id.replace("%2F", "/") } + + /** + * Get the permalink base URL according to the potential one in [MatrixConfiguration.clientPermalinkBaseUrl] + * and the [forceMatrixTo] parameter. + * + * @param forceMatrixTo whether we should force using matrix.to base URL. + * + * @return the permalink base URL. + */ + private fun baseUrl(forceMatrixTo: Boolean): String { + return matrixConfiguration.clientPermalinkBaseUrl + ?.takeUnless { forceMatrixTo } + ?: MATRIX_TO_URL_BASE + } + + private fun useClientFormat(forceMatrixTo: Boolean): Boolean { + return !forceMatrixTo && matrixConfiguration.clientPermalinkBaseUrl != null + } + + companion object { + private const val ROOM_PATH = "room/" + private const val USER_PATH = "user/" + private const val GROUP_PATH = "group/" + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/presence/PresenceAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/presence/PresenceAPI.kt new file mode 100644 index 0000000000..53d0d5e963 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/presence/PresenceAPI.kt @@ -0,0 +1,43 @@ +/* + * 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.internal.session.presence + +import org.matrix.android.sdk.internal.network.NetworkConstants +import org.matrix.android.sdk.internal.session.presence.model.GetPresenceResponse +import org.matrix.android.sdk.internal.session.presence.model.SetPresenceBody +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.PUT +import retrofit2.http.Path + +internal interface PresenceAPI { + + /** + * Set the presence status of the current user + * Ref: https://matrix.org/docs/spec/client_server/latest#put-matrix-client-r0-presence-userid-status + */ + @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "presence/{userId}/status") + suspend fun setPresence(@Path("userId") userId: String, + @Body body: SetPresenceBody) + + /** + * Get the given user's presence state. + * Ref: https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-presence-userid-status + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "presence/{userId}/status") + suspend fun getPresence(@Path("userId") userId: String): GetPresenceResponse +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/presence/di/PresenceModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/presence/di/PresenceModule.kt new file mode 100644 index 0000000000..6b2ee76046 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/presence/di/PresenceModule.kt @@ -0,0 +1,53 @@ +/* + * 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.internal.session.presence.di + +import dagger.Binds +import dagger.Module +import dagger.Provides +import org.matrix.android.sdk.api.session.presence.PresenceService +import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.session.presence.PresenceAPI +import org.matrix.android.sdk.internal.session.presence.service.DefaultPresenceService +import org.matrix.android.sdk.internal.session.presence.service.task.DefaultGetPresenceTask +import org.matrix.android.sdk.internal.session.presence.service.task.DefaultSetPresenceTask +import org.matrix.android.sdk.internal.session.presence.service.task.GetPresenceTask +import org.matrix.android.sdk.internal.session.presence.service.task.SetPresenceTask +import retrofit2.Retrofit + +@Module +internal abstract class PresenceModule { + + @Module + companion object { + @Provides + @JvmStatic + @SessionScope + fun providesPresenceAPI(retrofit: Retrofit): PresenceAPI { + return retrofit.create(PresenceAPI::class.java) + } + } + + @Binds + abstract fun bindPresenceService(service: DefaultPresenceService): PresenceService + + @Binds + abstract fun bindSetPresenceTask(task: DefaultSetPresenceTask): SetPresenceTask + + @Binds + abstract fun bindGetPresenceTask(task: DefaultGetPresenceTask): GetPresenceTask +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/presence/model/GetPresenceResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/presence/model/GetPresenceResponse.kt new file mode 100644 index 0000000000..a7552f7b02 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/presence/model/GetPresenceResponse.kt @@ -0,0 +1,33 @@ +/* + * 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.internal.session.presence.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.presence.model.PresenceEnum + +@JsonClass(generateAdapter = true) +data class GetPresenceResponse( + @Json(name = "presence") + val presence: PresenceEnum, + @Json(name = "last_active_ago") + val lastActiveAgo: Long? = null, + @Json(name = "status_msg") + val message: String? = null, + @Json(name = "currently_active") + val isCurrentlyActive: Boolean? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/presence/model/PresenceContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/presence/model/PresenceContent.kt new file mode 100644 index 0000000000..45e0fcf06e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/presence/model/PresenceContent.kt @@ -0,0 +1,52 @@ +/* + * 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.internal.session.presence.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.presence.model.PresenceEnum + +/** + * Class representing the EventType.PRESENCE event content + */ +@JsonClass(generateAdapter = true) +data class PresenceContent( + /** + * Required. The presence state for this user. One of: ["online", "offline", "unavailable"] + */ + @Json(name = "presence") val presence: PresenceEnum, + /** + * The last time since this used performed some action, in milliseconds. + */ + @Json(name = "last_active_ago") val lastActiveAgo: Long? = null, + /** + * An optional description to accompany the presence. + */ + @Json(name = "status_msg") val statusMessage: String? = null, + /** + * Whether the user is currently active + */ + @Json(name = "currently_active") val isCurrentlyActive: Boolean = false, + /** + * The current avatar URL for this user, if any. + */ + @Json(name = "avatar_url") val avatarUrl: String? = null, + /** + * The current display name for this user, if any. + */ + @Json(name = "displayname") val displayName: String? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/presence/model/SetPresenceBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/presence/model/SetPresenceBody.kt new file mode 100644 index 0000000000..0c81791ec0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/presence/model/SetPresenceBody.kt @@ -0,0 +1,28 @@ +/* + * 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.internal.session.presence.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.presence.model.PresenceEnum + +@JsonClass(generateAdapter = true) +internal data class SetPresenceBody( + @Json(name = "presence") + val presence: PresenceEnum, + @Json(name = "status_msg") + val statusMsg: String? +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/presence/service/DefaultPresenceService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/presence/service/DefaultPresenceService.kt new file mode 100644 index 0000000000..1083d5b4c2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/presence/service/DefaultPresenceService.kt @@ -0,0 +1,48 @@ +/* + * 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.internal.session.presence.service + +import org.matrix.android.sdk.api.session.presence.PresenceService +import org.matrix.android.sdk.api.session.presence.model.PresenceEnum +import org.matrix.android.sdk.api.session.presence.model.UserPresence +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.session.presence.service.task.GetPresenceTask +import org.matrix.android.sdk.internal.session.presence.service.task.SetPresenceTask +import javax.inject.Inject + +internal class DefaultPresenceService @Inject constructor( + @UserId private val userId: String, + private val setPresenceTask: SetPresenceTask, + private val getPresenceTask: GetPresenceTask +) : PresenceService { + + override suspend fun setMyPresence(presence: PresenceEnum, statusMsg: String?) { + setPresenceTask.execute(SetPresenceTask.Params(userId, presence, statusMsg)) + } + + override suspend fun fetchPresence(userId: String): UserPresence { + val result = getPresenceTask.execute(GetPresenceTask.Params(userId)) + + return UserPresence( + lastActiveAgo = result.lastActiveAgo, + statusMessage = result.message, + isCurrentlyActive = result.isCurrentlyActive, + presence = result.presence + ) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/presence/service/task/GetPresenceTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/presence/service/task/GetPresenceTask.kt new file mode 100644 index 0000000000..bb628dbab4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/presence/service/task/GetPresenceTask.kt @@ -0,0 +1,42 @@ +/* + * 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.internal.session.presence.service.task + +import org.matrix.android.sdk.internal.network.GlobalErrorReceiver +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.presence.PresenceAPI +import org.matrix.android.sdk.internal.session.presence.model.GetPresenceResponse +import org.matrix.android.sdk.internal.task.Task +import javax.inject.Inject + +internal abstract class GetPresenceTask : Task { + data class Params( + val userId: String + ) +} + +internal class DefaultGetPresenceTask @Inject constructor( + private val presenceAPI: PresenceAPI, + private val globalErrorReceiver: GlobalErrorReceiver +) : GetPresenceTask() { + override suspend fun execute(params: Params): GetPresenceResponse { + return executeRequest(globalErrorReceiver) { + presenceAPI.getPresence(params.userId) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/presence/service/task/SetPresenceTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/presence/service/task/SetPresenceTask.kt new file mode 100644 index 0000000000..1b3bdabd30 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/presence/service/task/SetPresenceTask.kt @@ -0,0 +1,47 @@ +/* + * 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.internal.session.presence.service.task + +import org.matrix.android.sdk.api.session.presence.model.PresenceEnum +import org.matrix.android.sdk.internal.network.GlobalErrorReceiver +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.presence.PresenceAPI +import org.matrix.android.sdk.internal.session.presence.model.SetPresenceBody +import org.matrix.android.sdk.internal.task.Task +import javax.inject.Inject + +internal abstract class SetPresenceTask : Task { + data class Params( + val userId: String, + val presence: PresenceEnum, + val statusMsg: String? + ) +} + +internal class DefaultSetPresenceTask @Inject constructor( + private val presenceAPI: PresenceAPI, + private val globalErrorReceiver: GlobalErrorReceiver +) : SetPresenceTask() { + + override suspend fun execute(params: Params): Any { + return executeRequest(globalErrorReceiver) { + val setPresenceBody = SetPresenceBody(params.presence, params.statusMsg) + presenceAPI.setPresence(params.userId, setPresenceBody) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DefaultProfileService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DefaultProfileService.kt index 386fec8256..a19832c523 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DefaultProfileService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DefaultProfileService.kt @@ -22,6 +22,7 @@ import androidx.lifecycle.LiveData import com.zhuinden.monarchy.Monarchy import io.realm.kotlin.where import kotlinx.coroutines.withContext +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor import org.matrix.android.sdk.api.session.identity.ThreePid import org.matrix.android.sdk.api.session.profile.ProfileService @@ -35,7 +36,6 @@ import org.matrix.android.sdk.internal.session.content.FileUploader import org.matrix.android.sdk.internal.session.user.UserStore import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.task.configureWith -import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers import javax.inject.Inject internal class DefaultProfileService @Inject constructor(private val taskExecutor: TaskExecutor, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/FinalizeAddingThreePidTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/FinalizeAddingThreePidTask.kt index 5f063365e0..6ff4efaf11 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/FinalizeAddingThreePidTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/FinalizeAddingThreePidTask.kt @@ -17,12 +17,12 @@ package org.matrix.android.sdk.internal.session.profile import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.auth.UIABaseAuth import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.toRegistrationFlowResponse import org.matrix.android.sdk.api.session.identity.ThreePid import org.matrix.android.sdk.internal.auth.registration.handleUIA -import org.matrix.android.sdk.api.auth.UIABaseAuth import org.matrix.android.sdk.internal.database.model.PendingThreePidEntity import org.matrix.android.sdk.internal.database.model.PendingThreePidEntityFields import org.matrix.android.sdk.internal.di.SessionDatabase @@ -71,8 +71,8 @@ internal class DefaultFinalizeAddingThreePidTask @Inject constructor( } true } catch (throwable: Throwable) { - if (params.userInteractiveAuthInterceptor == null - || !handleUIA( + if (params.userInteractiveAuthInterceptor == null || + !handleUIA( failure = throwable, interceptor = params.userInteractiveAuthInterceptor, retryBlock = { authUpdate -> diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/AddHttpPusherWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/AddHttpPusherWorker.kt deleted file mode 100644 index c9d7ad2193..0000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/AddHttpPusherWorker.kt +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright 2020 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.internal.session.pushers - -import android.content.Context -import androidx.work.WorkerParameters -import com.squareup.moshi.JsonClass -import com.zhuinden.monarchy.Monarchy -import org.matrix.android.sdk.api.failure.Failure -import org.matrix.android.sdk.api.session.pushers.PusherState -import org.matrix.android.sdk.internal.database.mapper.toEntity -import org.matrix.android.sdk.internal.database.model.PusherEntity -import org.matrix.android.sdk.internal.database.query.where -import org.matrix.android.sdk.internal.di.SessionDatabase -import org.matrix.android.sdk.internal.network.GlobalErrorReceiver -import org.matrix.android.sdk.internal.network.executeRequest -import org.matrix.android.sdk.internal.session.SessionComponent -import org.matrix.android.sdk.internal.util.awaitTransaction -import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker -import org.matrix.android.sdk.internal.worker.SessionWorkerParams -import javax.inject.Inject - -internal class AddHttpPusherWorker(context: Context, params: WorkerParameters) - : SessionSafeCoroutineWorker(context, params, Params::class.java) { - - @JsonClass(generateAdapter = true) - internal data class Params( - override val sessionId: String, - val pusher: JsonPusher, - override val lastFailureMessage: String? = null - ) : SessionWorkerParams - - @Inject lateinit var pushersAPI: PushersAPI - @Inject @SessionDatabase lateinit var monarchy: Monarchy - @Inject lateinit var globalErrorReceiver: GlobalErrorReceiver - - override fun injectWith(injector: SessionComponent) { - injector.inject(this) - } - - override suspend fun doSafeWork(params: Params): Result { - val pusher = params.pusher - - if (pusher.pushKey.isBlank()) { - return Result.failure() - } - return try { - setPusher(pusher) - Result.success() - } catch (exception: Throwable) { - when (exception) { - is Failure.NetworkConnection -> Result.retry() - else -> { - monarchy.awaitTransaction { realm -> - PusherEntity.where(realm, pusher.pushKey).findFirst()?.let { - // update it - it.state = PusherState.FAILED_TO_REGISTER - } - } - Result.failure() - } - } - } - } - - override fun buildErrorParams(params: Params, message: String): Params { - return params.copy(lastFailureMessage = params.lastFailureMessage ?: message) - } - - private suspend fun setPusher(pusher: JsonPusher) { - executeRequest(globalErrorReceiver) { - pushersAPI.setPusher(pusher) - } - monarchy.awaitTransaction { realm -> - val echo = PusherEntity.where(realm, pusher.pushKey).findFirst() - if (echo != null) { - // update it - echo.appDisplayName = pusher.appDisplayName - echo.appId = pusher.appId - echo.kind = pusher.kind - echo.lang = pusher.lang - echo.profileTag = pusher.profileTag - echo.data?.format = pusher.data?.format - echo.data?.url = pusher.data?.url - echo.state = PusherState.REGISTERED - } else { - pusher.toEntity().also { - it.state = PusherState.REGISTERED - realm.insertOrUpdate(it) - } - } - } - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/AddPusherTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/AddPusherTask.kt new file mode 100644 index 0000000000..7d81e19265 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/AddPusherTask.kt @@ -0,0 +1,78 @@ +/* + * Copyright (c) 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.internal.session.pushers + +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.session.pushers.PusherState +import org.matrix.android.sdk.internal.database.mapper.toEntity +import org.matrix.android.sdk.internal.database.model.PusherEntity +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.network.GlobalErrorReceiver +import org.matrix.android.sdk.internal.network.RequestExecutor +import org.matrix.android.sdk.internal.task.Task +import org.matrix.android.sdk.internal.util.awaitTransaction +import javax.inject.Inject + +internal interface AddPusherTask : Task { + data class Params(val pusher: JsonPusher) +} + +internal class DefaultAddPusherTask @Inject constructor( + private val pushersAPI: PushersAPI, + @SessionDatabase private val monarchy: Monarchy, + private val requestExecutor: RequestExecutor, + private val globalErrorReceiver: GlobalErrorReceiver +) : AddPusherTask { + override suspend fun execute(params: AddPusherTask.Params) { + val pusher = params.pusher + try { + setPusher(pusher) + } catch (error: Throwable) { + monarchy.awaitTransaction { realm -> + PusherEntity.where(realm, pusher.pushKey).findFirst()?.let { + it.state = PusherState.FAILED_TO_REGISTER + } + } + throw error + } + } + + private suspend fun setPusher(pusher: JsonPusher) { + requestExecutor.executeRequest(globalErrorReceiver) { + pushersAPI.setPusher(pusher) + } + monarchy.awaitTransaction { realm -> + val echo = PusherEntity.where(realm, pusher.pushKey).findFirst() + if (echo == null) { + pusher.toEntity().also { + it.state = PusherState.REGISTERED + realm.insertOrUpdate(it) + } + } else { + echo.appDisplayName = pusher.appDisplayName + echo.appId = pusher.appId + echo.kind = pusher.kind + echo.lang = pusher.lang + echo.profileTag = pusher.profileTag + echo.data?.format = pusher.data?.format + echo.data?.url = pusher.data?.url + echo.state = PusherState.REGISTERED + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/AddPusherWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/AddPusherWorker.kt new file mode 100644 index 0000000000..4df42b2cfb --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/AddPusherWorker.kt @@ -0,0 +1,63 @@ +/* + * Copyright 2020 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.internal.session.pushers + +import android.content.Context +import androidx.work.WorkerParameters +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.failure.Failure +import org.matrix.android.sdk.internal.session.SessionComponent +import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker +import org.matrix.android.sdk.internal.worker.SessionWorkerParams +import javax.inject.Inject + +internal class AddPusherWorker(context: Context, params: WorkerParameters) : + SessionSafeCoroutineWorker(context, params, Params::class.java) { + + @JsonClass(generateAdapter = true) + internal data class Params( + override val sessionId: String, + val pusher: JsonPusher, + override val lastFailureMessage: String? = null + ) : SessionWorkerParams + + @Inject lateinit var addPusherTask: AddPusherTask + + override fun injectWith(injector: SessionComponent) { + injector.inject(this) + } + + override suspend fun doSafeWork(params: Params): Result { + val pusher = params.pusher + + if (pusher.pushKey.isBlank()) { + return Result.failure() + } + return try { + addPusherTask.execute(AddPusherTask.Params(pusher)) + Result.success() + } catch (exception: Throwable) { + when (exception) { + is Failure.NetworkConnection -> Result.retry() + else -> Result.failure() + } + } + } + + override fun buildErrorParams(params: Params, message: String): Params { + return params.copy(lastFailureMessage = params.lastFailureMessage ?: message) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/DefaultPushersService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/DefaultPushersService.kt index a772cf5ebb..e87c27e601 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/DefaultPushersService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/DefaultPushersService.kt @@ -30,7 +30,6 @@ import org.matrix.android.sdk.internal.session.pushers.gateway.PushGatewayNotify import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.task.configureWith import org.matrix.android.sdk.internal.worker.WorkerParamsFactory -import java.security.InvalidParameterException import java.util.UUID import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -41,6 +40,7 @@ internal class DefaultPushersService @Inject constructor( @SessionId private val sessionId: String, private val getPusherTask: GetPushersTask, private val pushGatewayNotifyTask: PushGatewayNotifyTask, + private val addPusherTask: AddPusherTask, private val removePusherTask: RemovePusherTask, private val taskExecutor: TaskExecutor ) : PushersService { @@ -58,35 +58,50 @@ internal class DefaultPushersService @Inject constructor( .executeBy(taskExecutor) } - override fun addHttpPusher(pushkey: String, - appId: String, - profileTag: String, - lang: String, - appDisplayName: String, - deviceDisplayName: String, - url: String, - append: Boolean, - withEventIdOnly: Boolean) - : UUID { - // Do some parameter checks. It's ok to throw Exception, to inform developer of the problem - if (pushkey.length > 512) throw InvalidParameterException("pushkey should not exceed 512 chars") - if (appId.length > 64) throw InvalidParameterException("appId should not exceed 64 chars") - if ("/_matrix/push/v1/notify" !in url) throw InvalidParameterException("url should contain '/_matrix/push/v1/notify'") + override fun enqueueAddHttpPusher(httpPusher: PushersService.HttpPusher): UUID { + return enqueueAddPusher(httpPusher.toJsonPusher()) + } - val pusher = JsonPusher( - pushKey = pushkey, - kind = "http", - appId = appId, - appDisplayName = appDisplayName, - deviceDisplayName = deviceDisplayName, - profileTag = profileTag, - lang = lang, - data = JsonPusherData(url, EVENT_ID_ONLY.takeIf { withEventIdOnly }), - append = append) + override suspend fun addHttpPusher(httpPusher: PushersService.HttpPusher) { + addPusherTask.execute(AddPusherTask.Params(httpPusher.toJsonPusher())) + } - val params = AddHttpPusherWorker.Params(sessionId, pusher) + private fun PushersService.HttpPusher.toJsonPusher() = JsonPusher( + pushKey = pushkey, + kind = "http", + appId = appId, + profileTag = profileTag, + lang = lang, + appDisplayName = appDisplayName, + deviceDisplayName = deviceDisplayName, + data = JsonPusherData(url, EVENT_ID_ONLY.takeIf { withEventIdOnly }), + append = append + ) - val request = workManagerProvider.matrixOneTimeWorkRequestBuilder() + override suspend fun addEmailPusher(email: String, + lang: String, + emailBranding: String, + appDisplayName: String, + deviceDisplayName: String, + append: Boolean) { + addPusherTask.execute( + AddPusherTask.Params(JsonPusher( + pushKey = email, + kind = Pusher.KIND_EMAIL, + appId = Pusher.APP_ID_EMAIL, + profileTag = "", + lang = lang, + appDisplayName = appDisplayName, + deviceDisplayName = deviceDisplayName, + data = JsonPusherData(brand = emailBranding), + append = append + )) + ) + } + + private fun enqueueAddPusher(pusher: JsonPusher): UUID { + val params = AddPusherWorker.Params(sessionId, pusher) + val request = workManagerProvider.matrixOneTimeWorkRequestBuilder() .setConstraints(WorkManagerProvider.workConstraints) .setInputData(WorkerParamsFactory.toData(params)) .setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY_MILLIS, TimeUnit.MILLISECONDS) @@ -95,8 +110,20 @@ internal class DefaultPushersService @Inject constructor( return request.id } + override suspend fun removePusher(pusher: Pusher) { + removePusher(pusher.pushKey, pusher.appId) + } + override suspend fun removeHttpPusher(pushkey: String, appId: String) { - val params = RemovePusherTask.Params(pushkey, appId) + removePusher(pushkey, appId) + } + + override suspend fun removeEmailPusher(email: String) { + removePusher(pushKey = email, Pusher.APP_ID_EMAIL) + } + + private suspend fun removePusher(pushKey: String, pushAppId: String) { + val params = RemovePusherTask.Params(pushKey, pushAppId) removePusherTask.execute(params) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/JsonPusher.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/JsonPusher.kt index a594675e28..8dc0954694 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/JsonPusher.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/JsonPusher.kt @@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session.pushers import com.squareup.moshi.Json import com.squareup.moshi.JsonClass import org.matrix.android.sdk.internal.di.SerializeNulls +import java.security.InvalidParameterException /** * Example: @@ -112,4 +113,11 @@ internal data class JsonPusher( */ @Json(name = "append") val append: Boolean? = false -) +) { + init { + // Do some parameter checks. It's ok to throw Exception, to inform developer of the problem + if (pushKey.length > 512) throw InvalidParameterException("pushkey should not exceed 512 chars") + if (appId.length > 64) throw InvalidParameterException("appId should not exceed 64 chars") + data?.url?.let { url -> if ("/_matrix/push/v1/notify" !in url) throw InvalidParameterException("url should contain '/_matrix/push/v1/notify'") } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/JsonPusherData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/JsonPusherData.kt index c8d4d77fb1..42a8fa6ff3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/JsonPusherData.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/JsonPusherData.kt @@ -32,5 +32,8 @@ internal data class JsonPusherData( * Currently the only format available is 'event_id_only'. */ @Json(name = "format") - val format: String? = null + val format: String? = null, + + @Json(name = "brand") + val brand: String? = null ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/PushersModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/PushersModule.kt index 4030c63514..d53a4eed65 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/PushersModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/PushersModule.kt @@ -65,6 +65,9 @@ internal abstract class PushersModule { @Binds abstract fun bindSavePushRulesTask(task: DefaultSavePushRulesTask): SavePushRulesTask + @Binds + abstract fun bindAddPusherTask(task: DefaultAddPusherTask): AddPusherTask + @Binds abstract fun bindRemovePusherTask(task: DefaultRemovePusherTask): RemovePusherTask diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/RemovePusherTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/RemovePusherTask.kt index 3a2ebf40c2..057c309078 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/RemovePusherTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/RemovePusherTask.kt @@ -17,16 +17,16 @@ package org.matrix.android.sdk.internal.session.pushers import com.zhuinden.monarchy.Monarchy +import io.realm.Realm import org.matrix.android.sdk.api.session.pushers.PusherState import org.matrix.android.sdk.internal.database.mapper.asDomain import org.matrix.android.sdk.internal.database.model.PusherEntity import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.task.Task import org.matrix.android.sdk.internal.util.awaitTransaction -import io.realm.Realm -import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import javax.inject.Inject internal interface RemovePusherTask : Task { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt index 8afd690f64..cb4bcdb606 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt @@ -17,6 +17,7 @@ package org.matrix.android.sdk.internal.session.room import androidx.lifecycle.LiveData +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.session.crypto.CryptoService import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.room.Room @@ -71,7 +72,8 @@ internal class DefaultRoom(override val roomId: String, private val roomVersionService: RoomVersionService, private val sendStateTask: SendStateTask, private val viaParameterFinder: ViaParameterFinder, - private val searchTask: SearchTask + private val searchTask: SearchTask, + override val coroutineDispatchers: MatrixCoroutineDispatchers ) : Room, TimelineService by timelineService, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt index f69949cbb6..7ca64aa66a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt @@ -85,21 +85,25 @@ internal class DefaultRoomService @Inject constructor( return roomSummaryDataSource.getRoomSummary(roomIdOrAlias) } - override fun getRoomSummaries(queryParams: RoomSummaryQueryParams): List { - return roomSummaryDataSource.getRoomSummaries(queryParams) + override fun getRoomSummaries(queryParams: RoomSummaryQueryParams, + sortOrder: RoomSortOrder): List { + return roomSummaryDataSource.getRoomSummaries(queryParams, sortOrder) } - override fun getRoomSummariesLive(queryParams: RoomSummaryQueryParams): LiveData> { - return roomSummaryDataSource.getRoomSummariesLive(queryParams) + override fun getRoomSummariesLive(queryParams: RoomSummaryQueryParams, + sortOrder: RoomSortOrder): LiveData> { + return roomSummaryDataSource.getRoomSummariesLive(queryParams, sortOrder) } - override fun getPagedRoomSummariesLive(queryParams: RoomSummaryQueryParams, pagedListConfig: PagedList.Config, sortOrder: RoomSortOrder) - : LiveData> { + override fun getPagedRoomSummariesLive(queryParams: RoomSummaryQueryParams, + pagedListConfig: PagedList.Config, + sortOrder: RoomSortOrder): LiveData> { return roomSummaryDataSource.getSortedPagedRoomSummariesLive(queryParams, pagedListConfig, sortOrder) } - override fun getFilteredPagedRoomSummariesLive(queryParams: RoomSummaryQueryParams, pagedListConfig: PagedList.Config, sortOrder: RoomSortOrder) - : UpdatableLivePageResult { + override fun getFilteredPagedRoomSummariesLive(queryParams: RoomSummaryQueryParams, + pagedListConfig: PagedList.Config, + sortOrder: RoomSortOrder): UpdatableLivePageResult { return roomSummaryDataSource.getUpdatablePagedRoomSummariesLive(queryParams, pagedListConfig, sortOrder) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt index c7e09e5954..5a1eb190a8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt @@ -131,8 +131,8 @@ internal class EventRelationsAggregationProcessor @Inject constructor( EventType.ENCRYPTED -> { // Relation type is in clear val encryptedEventContent = event.content.toModel() - if (encryptedEventContent?.relatesTo?.type == RelationType.REPLACE - || encryptedEventContent?.relatesTo?.type == RelationType.RESPONSE + if (encryptedEventContent?.relatesTo?.type == RelationType.REPLACE || + encryptedEventContent?.relatesTo?.type == RelationType.RESPONSE ) { event.getClearContent().toModel()?.let { if (encryptedEventContent.relatesTo.type == RelationType.REPLACE) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt index 98e7659238..efc5166a0c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt @@ -216,22 +216,6 @@ internal interface RoomAPI { @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/state") suspend fun getRoomState(@Path("roomId") roomId: String): List - /** - * Send a relation event to a room. - * - * @param txId the transaction Id - * @param roomId the room id - * @param eventType the event type - * @param content the event content - */ - @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/send_relation/{parent_id}/{relation_type}/{event_type}") - suspend fun sendRelation(@Path("roomId") roomId: String, - @Path("parent_id") parentId: String, - @Path("relation_type") relationType: String, - @Path("event_type") eventType: String, - @Body content: Content? - ): SendResponse - /** * Paginate relations for event based in normal topological order * diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt index d44eb32529..4ab06338a2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt @@ -16,6 +16,7 @@ package org.matrix.android.sdk.internal.session.room +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.session.crypto.CryptoService import org.matrix.android.sdk.api.session.room.Room import org.matrix.android.sdk.internal.session.SessionScope @@ -66,7 +67,8 @@ internal class DefaultRoomFactory @Inject constructor(private val cryptoService: private val roomAccountDataServiceFactory: DefaultRoomAccountDataService.Factory, private val sendStateTask: SendStateTask, private val viaParameterFinder: ViaParameterFinder, - private val searchTask: SearchTask) : + private val searchTask: SearchTask, + private val coroutineDispatchers: MatrixCoroutineDispatchers) : RoomFactory { override fun create(roomId: String): Room { @@ -92,7 +94,8 @@ internal class DefaultRoomFactory @Inject constructor(private val cryptoService: roomVersionService = roomVersionServiceFactory.create(roomId), sendStateTask = sendStateTask, searchTask = searchTask, - viaParameterFinder = viaParameterFinder + viaParameterFinder = viaParameterFinder, + coroutineDispatchers = coroutineDispatchers ) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/DefaultAliasService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/DefaultAliasService.kt index 8f58094a2a..73a3b285e9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/DefaultAliasService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/DefaultAliasService.kt @@ -17,8 +17,8 @@ package org.matrix.android.sdk.internal.session.room.alias import dagger.assisted.Assisted -import dagger.assisted.AssistedInject import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import org.matrix.android.sdk.api.session.room.alias.AliasService internal class DefaultAliasService @AssistedInject constructor( diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/call/DefaultRoomCallService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/call/DefaultRoomCallService.kt index 9bde5054f6..675034531d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/call/DefaultRoomCallService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/call/DefaultRoomCallService.kt @@ -17,8 +17,8 @@ package org.matrix.android.sdk.internal.session.room.call import dagger.assisted.Assisted -import dagger.assisted.AssistedInject import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.room.call.RoomCallService import org.matrix.android.sdk.internal.session.room.RoomGetter diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt index 9bb3899f2f..84261e6ebf 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt @@ -77,9 +77,9 @@ internal class CreateRoomBodyBuilder @Inject constructor( buildHistoryVisibilityEvent(params), buildAvatarEvent(params), buildGuestAccess(params) - ) - + params.featurePreset?.setupInitialStates().orEmpty() - + buildCustomInitialStates(params) + ) + + params.featurePreset?.setupInitialStates().orEmpty() + + buildCustomInitialStates(params) ) .takeIf { it.isNotEmpty() } @@ -154,8 +154,8 @@ internal class CreateRoomBodyBuilder @Inject constructor( * Add the crypto algorithm to the room creation parameters. */ private suspend fun buildEncryptionWithAlgorithmEvent(params: CreateRoomParams): Event? { - if (params.algorithm == null - && canEnableEncryption(params)) { + if (params.algorithm == null && + canEnableEncryption(params)) { // Enable the encryption params.enableEncryption() } @@ -173,13 +173,13 @@ internal class CreateRoomBodyBuilder @Inject constructor( } private suspend fun canEnableEncryption(params: CreateRoomParams): Boolean { - return params.enableEncryptionIfInvitedUsersSupportIt + return params.enableEncryptionIfInvitedUsersSupportIt && // Parity with web, enable if users have encryption ready devices // for now remove checks on cross signing and 3pid invites // && crossSigningService.isCrossSigningVerified() - && params.invite3pids.isEmpty() - && params.invitedUserIds.isNotEmpty() - && params.invitedUserIds.let { userIds -> + params.invite3pids.isEmpty() && + params.invitedUserIds.isNotEmpty() && + params.invitedUserIds.let { userIds -> val keys = deviceListManager.downloadKeys(userIds, forceDownload = false) userIds.all { userId -> diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomTask.kt index 518f0a0a6d..ac6e0562b0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomTask.kt @@ -81,13 +81,13 @@ internal class DefaultCreateRoomTask @Inject constructor( } } catch (throwable: Throwable) { if (throwable is Failure.ServerError) { - if (throwable.httpCode == 403 - && throwable.error.code == MatrixError.M_FORBIDDEN - && throwable.error.message.startsWith("Federation denied with")) { + if (throwable.httpCode == 403 && + throwable.error.code == MatrixError.M_FORBIDDEN && + throwable.error.message.startsWith("Federation denied with")) { throw CreateRoomFailure.CreatedWithFederationFailure(throwable.error) - } else if (throwable.httpCode == 400 - && throwable.error.code == MatrixError.M_UNKNOWN - && throwable.error.message == "Invalid characters in room alias") { + } else if (throwable.httpCode == 400 && + throwable.error.code == MatrixError.M_UNKNOWN && + throwable.error.message == "Invalid characters in room alias") { throw CreateRoomFailure.AliasError(RoomAliasError.AliasInvalid) } } @@ -138,8 +138,8 @@ internal class DefaultCreateRoomTask @Inject constructor( * @return true if it is a direct chat */ private fun CreateRoomParams.isDirect(): Boolean { - return preset == CreateRoomPreset.PRESET_TRUSTED_PRIVATE_CHAT - && isDirect == true + return preset == CreateRoomPreset.PRESET_TRUSTED_PRIVATE_CHAT && + isDirect == true } /** diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/RoomCreateEventProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/RoomCreateEventProcessor.kt index cc66a0a2d2..eb966b684c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/RoomCreateEventProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/RoomCreateEventProcessor.kt @@ -16,6 +16,7 @@ package org.matrix.android.sdk.internal.session.room.create +import io.realm.Realm import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.toModel @@ -25,7 +26,6 @@ import org.matrix.android.sdk.internal.database.model.EventInsertType import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor -import io.realm.Realm import javax.inject.Inject internal class RoomCreateEventProcessor @Inject constructor() : EventInsertLiveProcessor { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/draft/DefaultDraftService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/draft/DefaultDraftService.kt index 1d4ab6d516..3867e0dc8d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/draft/DefaultDraftService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/draft/DefaultDraftService.kt @@ -18,13 +18,13 @@ package org.matrix.android.sdk.internal.session.room.draft import androidx.lifecycle.LiveData import dagger.assisted.Assisted -import dagger.assisted.AssistedInject import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import kotlinx.coroutines.withContext +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.session.room.send.DraftService import org.matrix.android.sdk.api.session.room.send.UserDraft import org.matrix.android.sdk.api.util.Optional -import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers internal class DefaultDraftService @AssistedInject constructor(@Assisted private val roomId: String, private val draftRepository: DraftRepository, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/DefaultMembershipService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/DefaultMembershipService.kt index 41e891f78e..6cf82dde44 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/DefaultMembershipService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/DefaultMembershipService.kt @@ -17,10 +17,12 @@ package org.matrix.android.sdk.internal.session.room.membership import androidx.lifecycle.LiveData -import dagger.assisted.Assisted -import dagger.assisted.AssistedInject -import dagger.assisted.AssistedFactory import com.zhuinden.monarchy.Monarchy +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import io.realm.Realm +import io.realm.RealmQuery import org.matrix.android.sdk.api.session.identity.ThreePid import org.matrix.android.sdk.api.session.room.members.MembershipService import org.matrix.android.sdk.api.session.room.members.RoomMemberQueryParams @@ -31,6 +33,7 @@ import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFields import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.query.QueryStringValueProcessor import org.matrix.android.sdk.internal.query.process import org.matrix.android.sdk.internal.session.room.membership.admin.MembershipAdminTask import org.matrix.android.sdk.internal.session.room.membership.joining.InviteTask @@ -38,8 +41,6 @@ import org.matrix.android.sdk.internal.session.room.membership.joining.JoinRoomT import org.matrix.android.sdk.internal.session.room.membership.leaving.LeaveRoomTask import org.matrix.android.sdk.internal.session.room.membership.threepid.InviteThreePidTask import org.matrix.android.sdk.internal.util.fetchCopied -import io.realm.Realm -import io.realm.RealmQuery internal class DefaultMembershipService @AssistedInject constructor( @Assisted private val roomId: String, @@ -51,7 +52,8 @@ internal class DefaultMembershipService @AssistedInject constructor( private val leaveRoomTask: LeaveRoomTask, private val membershipAdminTask: MembershipAdminTask, @UserId - private val userId: String + private val userId: String, + private val queryStringValueProcessor: QueryStringValueProcessor ) : MembershipService { @AssistedFactory @@ -94,15 +96,17 @@ internal class DefaultMembershipService @AssistedInject constructor( } private fun roomMembersQuery(realm: Realm, queryParams: RoomMemberQueryParams): RealmQuery { - return RoomMemberHelper(realm, roomId).queryRoomMembersEvent() - .process(RoomMemberSummaryEntityFields.USER_ID, queryParams.userId) - .process(RoomMemberSummaryEntityFields.MEMBERSHIP_STR, queryParams.memberships) - .process(RoomMemberSummaryEntityFields.DISPLAY_NAME, queryParams.displayName) - .apply { - if (queryParams.excludeSelf) { - notEqualTo(RoomMemberSummaryEntityFields.USER_ID, userId) + return with(queryStringValueProcessor) { + RoomMemberHelper(realm, roomId).queryRoomMembersEvent() + .process(RoomMemberSummaryEntityFields.USER_ID, queryParams.userId) + .process(RoomMemberSummaryEntityFields.MEMBERSHIP_STR, queryParams.memberships) + .process(RoomMemberSummaryEntityFields.DISPLAY_NAME, queryParams.displayName) + .apply { + if (queryParams.excludeSelf) { + notEqualTo(RoomMemberSummaryEntityFields.USER_ID, userId) + } } - } + } } override fun getNumberOfJoinedMembers(): Int { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomDisplayNameResolver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomDisplayNameResolver.kt index 3aa812d93d..bd9f2ecc36 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomDisplayNameResolver.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomDisplayNameResolver.kt @@ -33,6 +33,8 @@ import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity import org.matrix.android.sdk.internal.database.query.getOrNull import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.session.displayname.DisplayNameResolver +import org.matrix.android.sdk.internal.util.Normalizer import javax.inject.Inject /** @@ -40,6 +42,8 @@ import javax.inject.Inject */ internal class RoomDisplayNameResolver @Inject constructor( matrixConfiguration: MatrixConfiguration, + private val displayNameResolver: DisplayNameResolver, + private val normalizer: Normalizer, @UserId private val userId: String ) { @@ -52,7 +56,7 @@ internal class RoomDisplayNameResolver @Inject constructor( * @param roomId: the roomId to resolve the name of. * @return the room display name */ - fun resolve(realm: Realm, roomId: String): String { + fun resolve(realm: Realm, roomId: String): RoomName { // this algorithm is the one defined in // https://github.com/matrix-org/matrix-js-sdk/blob/develop/lib/models/room.js#L617 // calculateRoomName(room, userId) @@ -64,12 +68,12 @@ internal class RoomDisplayNameResolver @Inject constructor( val roomName = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_NAME, stateKey = "")?.root name = ContentMapper.map(roomName?.content).toModel()?.name if (!name.isNullOrEmpty()) { - return name + return name.toRoomName() } val canonicalAlias = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_CANONICAL_ALIAS, stateKey = "")?.root name = ContentMapper.map(canonicalAlias?.content).toModel()?.canonicalAlias if (!name.isNullOrEmpty()) { - return name + return name.toRoomName() } val roomMembers = RoomMemberHelper(realm, roomId) @@ -83,7 +87,8 @@ internal class RoomDisplayNameResolver @Inject constructor( activeMembers.where() .equalTo(RoomMemberSummaryEntityFields.USER_ID, it) .findFirst() - ?.getBestName() + ?.toMatrixItem() + ?.let { matrixItem -> displayNameResolver.getBestName(matrixItem) } } ?: roomDisplayNameFallbackProvider.getNameForRoomInvite() } else if (roomEntity?.membership == Membership.JOIN) { @@ -109,7 +114,7 @@ internal class RoomDisplayNameResolver @Inject constructor( // Get left members if any val leftMembersNames = roomMembers.queryLeftRoomMembersEvent() .findAll() - .map { it.getBestName() } + .map { displayNameResolver.getBestName(it.toMatrixItem()) } roomDisplayNameFallbackProvider.getNameForEmptyRoom(roomSummary?.isDirect.orFalse(), leftMembersNames) } 1 -> { @@ -149,7 +154,7 @@ internal class RoomDisplayNameResolver @Inject constructor( } } } - return name ?: roomId + return (name ?: roomId).toRoomName() } /** See [org.matrix.android.sdk.api.session.room.sender.SenderInfo.disambiguatedDisplayName] */ @@ -157,9 +162,13 @@ internal class RoomDisplayNameResolver @Inject constructor( roomMemberHelper: RoomMemberHelper): String { val isUnique = roomMemberHelper.isUniqueDisplayName(roomMemberSummary.displayName) return if (isUnique) { - roomMemberSummary.getBestName() + displayNameResolver.getBestName(roomMemberSummary.toMatrixItem()) } else { "${roomMemberSummary.displayName} (${roomMemberSummary.userId})" } } + + private fun String.toRoomName() = RoomName(this, normalizedName = normalizer.normalize(this)) } + +internal data class RoomName(val name: String, val normalizedName: String) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomMemberEntityFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomMemberEntityFactory.kt index f78b5d7992..d6a04de5e7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomMemberEntityFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomMemberEntityFactory.kt @@ -18,10 +18,11 @@ package org.matrix.android.sdk.internal.session.room.membership import org.matrix.android.sdk.api.session.room.model.RoomMemberContent import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity +import org.matrix.android.sdk.internal.database.model.presence.UserPresenceEntity internal object RoomMemberEntityFactory { - fun create(roomId: String, userId: String, roomMember: RoomMemberContent): RoomMemberSummaryEntity { + fun create(roomId: String, userId: String, roomMember: RoomMemberContent, presence: UserPresenceEntity?): RoomMemberSummaryEntity { val primaryKey = "${roomId}_$userId" return RoomMemberSummaryEntity( primaryKey = primaryKey, @@ -31,6 +32,7 @@ internal object RoomMemberEntityFactory { avatarUrl = roomMember.avatarUrl ).apply { membership = roomMember.membership + userPresenceEntity = presence } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomMemberEventHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomMemberEventHandler.kt index 7528f80cc2..25c124bd6b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomMemberEventHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomMemberEventHandler.kt @@ -20,6 +20,9 @@ import io.realm.Realm import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.room.model.RoomMemberContent +import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity +import org.matrix.android.sdk.internal.database.model.presence.UserPresenceEntity +import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.session.events.getFixedRoomMemberContent import org.matrix.android.sdk.internal.session.sync.SyncResponsePostTreatmentAggregator @@ -47,7 +50,13 @@ internal class RoomMemberEventHandler @Inject constructor( if (roomMember == null) { return false } - val roomMemberEntity = RoomMemberEntityFactory.create(roomId, userId, roomMember) + val roomMemberEntity = RoomMemberEntityFactory.create( + roomId, + userId, + roomMember, + // When an update is happening, insertOrUpdate replace existing values with null if they are not provided, + // but we want to preserve presence record value and not replace it with null + getExistingPresenceState(realm, roomId, userId)) realm.insertOrUpdate(roomMemberEntity) if (roomMember.membership.isActive()) { val userEntity = UserEntityFactory.create(userId, roomMember) @@ -60,7 +69,15 @@ internal class RoomMemberEventHandler @Inject constructor( if (mxId != null && mxId != myUserId) { aggregator?.directChatsToCheck?.put(roomId, mxId) } - return true } + + /** + * Get the already existing presence state for a specific user & room in order NOT to be replaced in RoomMemberSummaryEntity + * by NULL value. + */ + + private fun getExistingPresenceState(realm: Realm, roomId: String, userId: String): UserPresenceEntity? { + return RoomMemberSummaryEntity.where(realm, roomId, userId).findFirst()?.userPresenceEntity + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/joining/JoinRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/joining/JoinRoomTask.kt index 209a904fad..82fea237db 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/joining/JoinRoomTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/joining/JoinRoomTask.kt @@ -43,7 +43,7 @@ internal interface JoinRoomTask : Task { val roomIdOrAlias: String, val reason: String?, val viaServers: List = emptyList(), - val thirdPartySigned : SignInvitationResult? = null + val thirdPartySigned: SignInvitationResult? = null ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/notification/DefaultRoomPushRuleService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/notification/DefaultRoomPushRuleService.kt index 5486d96e28..8f1aefb731 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/notification/DefaultRoomPushRuleService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/notification/DefaultRoomPushRuleService.kt @@ -18,10 +18,10 @@ package org.matrix.android.sdk.internal.session.room.notification import androidx.lifecycle.LiveData import androidx.lifecycle.Transformations -import dagger.assisted.Assisted -import dagger.assisted.AssistedInject -import dagger.assisted.AssistedFactory import com.zhuinden.monarchy.Monarchy +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import org.matrix.android.sdk.api.pushrules.RuleScope import org.matrix.android.sdk.api.session.room.notification.RoomNotificationState import org.matrix.android.sdk.api.session.room.notification.RoomPushRuleService @@ -31,8 +31,8 @@ import org.matrix.android.sdk.internal.di.SessionDatabase internal class DefaultRoomPushRuleService @AssistedInject constructor(@Assisted private val roomId: String, private val setRoomNotificationStateTask: SetRoomNotificationStateTask, - @SessionDatabase private val monarchy: Monarchy) - : RoomPushRuleService { + @SessionDatabase private val monarchy: Monarchy) : + RoomPushRuleService { @AssistedFactory interface Factory { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/notification/SetRoomNotificationStateTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/notification/SetRoomNotificationStateTask.kt index 9cea1fe425..feb8c27b09 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/notification/SetRoomNotificationStateTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/notification/SetRoomNotificationStateTask.kt @@ -17,6 +17,7 @@ package org.matrix.android.sdk.internal.session.room.notification import com.zhuinden.monarchy.Monarchy +import io.realm.Realm import org.matrix.android.sdk.api.pushrules.RuleScope import org.matrix.android.sdk.api.session.room.notification.RoomNotificationState import org.matrix.android.sdk.internal.database.model.PushRuleEntity @@ -25,7 +26,6 @@ import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.session.pushers.AddPushRuleTask import org.matrix.android.sdk.internal.session.pushers.RemovePushRuleTask import org.matrix.android.sdk.internal.task.Task -import io.realm.Realm import javax.inject.Inject internal interface SetRoomNotificationStateTask : Task { @@ -37,8 +37,8 @@ internal interface SetRoomNotificationStateTask : Task { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/SendRelationWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/SendRelationWorker.kt deleted file mode 100644 index 5d0879d706..0000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/SendRelationWorker.kt +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright 2020 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.internal.session.room.relation - -import android.content.Context -import androidx.work.WorkerParameters -import com.squareup.moshi.JsonClass -import org.matrix.android.sdk.api.failure.Failure -import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.session.events.model.toModel -import org.matrix.android.sdk.api.session.room.model.relation.ReactionContent -import org.matrix.android.sdk.api.session.room.model.relation.ReactionInfo -import org.matrix.android.sdk.internal.network.GlobalErrorReceiver -import org.matrix.android.sdk.internal.network.executeRequest -import org.matrix.android.sdk.internal.session.SessionComponent -import org.matrix.android.sdk.internal.session.room.RoomAPI -import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository -import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker -import org.matrix.android.sdk.internal.worker.SessionWorkerParams -import javax.inject.Inject - -// TODO This is not used. Delete? -internal class SendRelationWorker(context: Context, params: WorkerParameters) - : SessionSafeCoroutineWorker(context, params, Params::class.java) { - - @JsonClass(generateAdapter = true) - internal data class Params( - override val sessionId: String, - val roomId: String, - val eventId: String, - val relationType: String? = null, - override val lastFailureMessage: String? = null - ) : SessionWorkerParams - - @Inject lateinit var roomAPI: RoomAPI - @Inject lateinit var globalErrorReceiver: GlobalErrorReceiver - @Inject lateinit var localEchoRepository: LocalEchoRepository - - override fun injectWith(injector: SessionComponent) { - injector.inject(this) - } - - override suspend fun doSafeWork(params: Params): Result { - val localEvent = localEchoRepository.getUpToDateEcho(params.eventId) - if (localEvent?.eventId == null) { - return Result.failure() - } - val relationContent = localEvent.content.toModel() - ?: return Result.failure() - val relatedEventId = relationContent.relatesTo?.eventId ?: return Result.failure() - val relationType = (relationContent.relatesTo as? ReactionInfo)?.type ?: params.relationType - ?: return Result.failure() - return try { - sendRelation(params.roomId, relationType, relatedEventId, localEvent) - Result.success() - } catch (exception: Throwable) { - when (exception) { - is Failure.NetworkConnection -> Result.retry() - else -> { - // TODO mark as failed to send? - // always return success, or the chain will be stuck for ever! - Result.success() - } - } - } - } - - override fun buildErrorParams(params: Params, message: String): Params { - return params.copy(lastFailureMessage = params.lastFailureMessage ?: message) - } - - private suspend fun sendRelation(roomId: String, relationType: String, relatedEventId: String, localEvent: Event) { - executeRequest(globalErrorReceiver) { - roomAPI.sendRelation( - roomId = roomId, - parentId = relatedEventId, - relationType = relationType, - eventType = localEvent.type!!, - content = localEvent.content - ) - } - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/UpdateQuickReactionTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/UpdateQuickReactionTask.kt index 32d6c5aa7e..d0ab430dad 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/UpdateQuickReactionTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/UpdateQuickReactionTask.kt @@ -16,6 +16,7 @@ package org.matrix.android.sdk.internal.session.room.relation import com.zhuinden.monarchy.Monarchy +import io.realm.Realm import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity import org.matrix.android.sdk.internal.database.model.EventEntity import org.matrix.android.sdk.internal.database.model.ReactionAggregatedSummaryEntityFields @@ -23,7 +24,6 @@ import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.task.Task -import io.realm.Realm import javax.inject.Inject internal interface UpdateQuickReactionTask : Task { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/reporting/DefaultReportingService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/reporting/DefaultReportingService.kt index add17a9fa5..c961f718ef 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/reporting/DefaultReportingService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/reporting/DefaultReportingService.kt @@ -17,8 +17,8 @@ package org.matrix.android.sdk.internal.session.room.reporting import dagger.assisted.Assisted -import dagger.assisted.AssistedInject import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import org.matrix.android.sdk.api.session.room.reporting.ReportingService internal class DefaultReportingService @AssistedInject constructor(@Assisted private val roomId: String, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt index 6dbb71e096..177c98541c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt @@ -22,9 +22,10 @@ import androidx.work.ExistingWorkPolicy import androidx.work.OneTimeWorkRequest import androidx.work.Operation import dagger.assisted.Assisted -import dagger.assisted.AssistedInject import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.MatrixUrls.isMxcUrl import org.matrix.android.sdk.api.session.content.ContentAttachmentData import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.isAttachmentMessage @@ -130,7 +131,7 @@ internal class DefaultSendService @AssistedInject constructor( val messageContent = clearContent?.toModel() as? MessageWithAttachmentContent ?: return NoOpCancellable val url = messageContent.getFileUrl() ?: return NoOpCancellable - if (url.startsWith("mxc://")) { + if (url.isMxcUrl()) { // We need to resend only the message as the attachment is ok localEchoRepository.updateSendState(localEcho.eventId, roomId, SendState.UNSENT) return sendEvent(localEcho.root) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt index c610326a94..8dd0c59387 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt @@ -165,8 +165,8 @@ internal class LocalEchoEventFactory @Inject constructor( newBodyAutoMarkdown: Boolean, msgType: String, compatibilityText: String): Event { - val permalink = permalinkFactory.createPermalink(roomId, originalEvent.root.eventId ?: "") - val userLink = originalEvent.root.senderId?.let { permalinkFactory.createPermalink(it) } ?: "" + val permalink = permalinkFactory.createPermalink(roomId, originalEvent.root.eventId ?: "", false) + val userLink = originalEvent.root.senderId?.let { permalinkFactory.createPermalink(it, false) } ?: "" val body = bodyForReply(originalEvent.getLastMessageContent(), originalEvent.isReply()) val replyFormatted = REPLY_PATTERN.format( @@ -350,9 +350,9 @@ internal class LocalEchoEventFactory @Inject constructor( autoMarkdown: Boolean): Event? { // Fallbacks and event representation // TODO Add error/warning logs when any of this is null - val permalink = permalinkFactory.createPermalink(eventReplied.root) ?: return null + val permalink = permalinkFactory.createPermalink(eventReplied.root, false) ?: return null val userId = eventReplied.root.senderId ?: return null - val userLink = permalinkFactory.createPermalink(userId) ?: return null + val userLink = permalinkFactory.createPermalink(userId, false) ?: return null val body = bodyForReply(eventReplied.getLastMessageContent(), eventReplied.isReply()) val replyFormatted = REPLY_PATTERN.format( diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MultipleEventSendingDispatcherWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MultipleEventSendingDispatcherWorker.kt index e889f1a61b..16a9eba363 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MultipleEventSendingDispatcherWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MultipleEventSendingDispatcherWorker.kt @@ -38,8 +38,8 @@ import javax.inject.Inject * Possible previous worker: Always [UploadContentWorker] * Possible next worker : None, but it will post new work to send events, encrypted or not */ -internal class MultipleEventSendingDispatcherWorker(context: Context, params: WorkerParameters) - : SessionSafeCoroutineWorker(context, params, Params::class.java) { +internal class MultipleEventSendingDispatcherWorker(context: Context, params: WorkerParameters) : + SessionSafeCoroutineWorker(context, params, Params::class.java) { @JsonClass(generateAdapter = true) internal data class Params( diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/RedactEventWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/RedactEventWorker.kt index 306f865408..b4436bfcbf 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/RedactEventWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/RedactEventWorker.kt @@ -32,8 +32,8 @@ import javax.inject.Inject * Possible previous worker: None * Possible next worker : None */ -internal class RedactEventWorker(context: Context, params: WorkerParameters) - : SessionSafeCoroutineWorker(context, params, Params::class.java) { +internal class RedactEventWorker(context: Context, params: WorkerParameters) : + SessionSafeCoroutineWorker(context, params, Params::class.java) { @JsonClass(generateAdapter = true) internal data class Params( diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/SendEventWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/SendEventWorker.kt index cd7911910d..8b7fe4b907 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/SendEventWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/SendEventWorker.kt @@ -39,8 +39,8 @@ import javax.inject.Inject * Possible next worker : None */ internal class SendEventWorker(context: Context, - params: WorkerParameters) - : SessionSafeCoroutineWorker(context, params, Params::class.java) { + params: WorkerParameters) : + SessionSafeCoroutineWorker(context, params, Params::class.java) { @JsonClass(generateAdapter = true) internal data class Params( diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/pills/TextPillsUtils.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/pills/TextPillsUtils.kt index faf966edf4..33cb0db243 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/pills/TextPillsUtils.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/pills/TextPillsUtils.kt @@ -17,6 +17,7 @@ package org.matrix.android.sdk.internal.session.room.send.pills import android.text.SpannableString import org.matrix.android.sdk.api.session.room.send.MatrixItemSpan +import org.matrix.android.sdk.internal.session.displayname.DisplayNameResolver import java.util.Collections import javax.inject.Inject @@ -25,7 +26,8 @@ import javax.inject.Inject * formatted text to send them as a Matrix messages. */ internal class TextPillsUtils @Inject constructor( - private val mentionLinkSpecComparator: MentionLinkSpecComparator + private val mentionLinkSpecComparator: MentionLinkSpecComparator, + private val displayNameResolver: DisplayNameResolver ) { /** @@ -63,7 +65,7 @@ internal class TextPillsUtils @Inject constructor( // append text before pill append(text, currIndex, start) // append the pill - append(String.format(template, urlSpan.matrixItem.id, urlSpan.matrixItem.getBestName())) + append(String.format(template, urlSpan.matrixItem.id, displayNameResolver.getBestName(urlSpan.matrixItem))) currIndex = end } // append text after the last pill diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessor.kt index cd5bf575db..050e321b9c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessor.kt @@ -16,11 +16,11 @@ package org.matrix.android.sdk.internal.session.room.send.queue +import org.matrix.android.sdk.api.session.SessionLifecycleObserver import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.util.Cancelable -import org.matrix.android.sdk.api.session.SessionLifecycleObserver -internal interface EventSenderProcessor: SessionLifecycleObserver { +internal interface EventSenderProcessor : SessionLifecycleObserver { fun postEvent(event: Event): Cancelable diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/StateEventDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/StateEventDataSource.kt index a25a362bfa..2114b9c590 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/StateEventDataSource.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/StateEventDataSource.kt @@ -31,11 +31,15 @@ import org.matrix.android.sdk.internal.database.mapper.asDomain import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntityFields import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.query.QueryStringValueProcessor import org.matrix.android.sdk.internal.query.process import javax.inject.Inject -internal class StateEventDataSource @Inject constructor(@SessionDatabase private val monarchy: Monarchy, - private val realmSessionProvider: RealmSessionProvider) { +internal class StateEventDataSource @Inject constructor( + @SessionDatabase private val monarchy: Monarchy, + private val realmSessionProvider: RealmSessionProvider, + private val queryStringValueProcessor: QueryStringValueProcessor +) { fun getStateEvent(roomId: String, eventType: String, stateKey: QueryStringValue): Event? { return realmSessionProvider.withRealm { realm -> @@ -78,13 +82,15 @@ internal class StateEventDataSource @Inject constructor(@SessionDatabase private eventTypes: Set, stateKey: QueryStringValue ): RealmQuery { - return realm.where() - .equalTo(CurrentStateEventEntityFields.ROOM_ID, roomId) - .apply { - if (eventTypes.isNotEmpty()) { - `in`(CurrentStateEventEntityFields.TYPE, eventTypes.toTypedArray()) + return with(queryStringValueProcessor) { + realm.where() + .equalTo(CurrentStateEventEntityFields.ROOM_ID, roomId) + .apply { + if (eventTypes.isNotEmpty()) { + `in`(CurrentStateEventEntityFields.TYPE, eventTypes.toTypedArray()) + } } - } - .process(CurrentStateEventEntityFields.STATE_KEY, stateKey) + .process(CurrentStateEventEntityFields.STATE_KEY, stateKey) + } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt index 0b8c6df806..c9fc3c9575 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt @@ -25,10 +25,10 @@ import androidx.paging.PagedList import com.zhuinden.monarchy.Monarchy import io.realm.Realm import io.realm.RealmQuery -import io.realm.Sort import io.realm.kotlin.where import org.matrix.android.sdk.api.query.ActiveSpaceFilter import org.matrix.android.sdk.api.query.RoomCategoryFilter +import org.matrix.android.sdk.api.query.isNormalized import org.matrix.android.sdk.api.session.room.ResultBoundaries import org.matrix.android.sdk.api.session.room.RoomSortOrder import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams @@ -48,12 +48,16 @@ import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields import org.matrix.android.sdk.internal.database.query.findByAlias import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.query.QueryStringValueProcessor import org.matrix.android.sdk.internal.query.process import org.matrix.android.sdk.internal.util.fetchCopyMap import javax.inject.Inject -internal class RoomSummaryDataSource @Inject constructor(@SessionDatabase private val monarchy: Monarchy, - private val roomSummaryMapper: RoomSummaryMapper) { +internal class RoomSummaryDataSource @Inject constructor( + @SessionDatabase private val monarchy: Monarchy, + private val roomSummaryMapper: RoomSummaryMapper, + private val queryStringValueProcessor: QueryStringValueProcessor +) { fun getRoomSummary(roomIdOrAlias: String): RoomSummary? { return monarchy @@ -80,25 +84,27 @@ internal class RoomSummaryDataSource @Inject constructor(@SessionDatabase privat } } - fun getRoomSummaries(queryParams: RoomSummaryQueryParams): List { + fun getRoomSummaries(queryParams: RoomSummaryQueryParams, + sortOrder: RoomSortOrder = RoomSortOrder.NONE): List { return monarchy.fetchAllMappedSync( - { roomSummariesQuery(it, queryParams) }, + { roomSummariesQuery(it, queryParams).process(sortOrder) }, { roomSummaryMapper.map(it) } ) } - fun getRoomSummariesLive(queryParams: RoomSummaryQueryParams): LiveData> { + fun getRoomSummariesLive(queryParams: RoomSummaryQueryParams, + sortOrder: RoomSortOrder = RoomSortOrder.NONE): LiveData> { return monarchy.findAllMappedWithChanges( { - roomSummariesQuery(it, queryParams) - .sort(RoomSummaryEntityFields.LAST_ACTIVITY_TIME, Sort.DESCENDING) + roomSummariesQuery(it, queryParams).process(sortOrder) }, { roomSummaryMapper.map(it) } ) } - fun getSpaceSummariesLive(queryParams: SpaceSummaryQueryParams): LiveData> { - return getRoomSummariesLive(queryParams) + fun getSpaceSummariesLive(queryParams: SpaceSummaryQueryParams, + sortOrder: RoomSortOrder = RoomSortOrder.NONE): LiveData> { + return getRoomSummariesLive(queryParams, sortOrder) } fun getSpaceSummary(roomIdOrAlias: String): RoomSummary? { @@ -122,8 +128,9 @@ internal class RoomSummaryDataSource @Inject constructor(@SessionDatabase privat } } - fun getSpaceSummaries(spaceSummaryQueryParams: SpaceSummaryQueryParams): List { - return getRoomSummaries(spaceSummaryQueryParams) + fun getSpaceSummaries(spaceSummaryQueryParams: SpaceSummaryQueryParams, + sortOrder: RoomSortOrder = RoomSortOrder.NONE): List { + return getRoomSummaries(spaceSummaryQueryParams, sortOrder) } fun getRootSpaceSummaries(): List { @@ -238,12 +245,20 @@ internal class RoomSummaryDataSource @Inject constructor(@SessionDatabase privat } private fun roomSummariesQuery(realm: Realm, queryParams: RoomSummaryQueryParams): RealmQuery { - val query = RoomSummaryEntity.where(realm) - query.process(RoomSummaryEntityFields.ROOM_ID, queryParams.roomId) - query.process(RoomSummaryEntityFields.DISPLAY_NAME, queryParams.displayName) - query.process(RoomSummaryEntityFields.CANONICAL_ALIAS, queryParams.canonicalAlias) - query.process(RoomSummaryEntityFields.MEMBERSHIP_STR, queryParams.memberships) - query.equalTo(RoomSummaryEntityFields.IS_HIDDEN_FROM_USER, false) + val query = with(queryStringValueProcessor) { + RoomSummaryEntity.where(realm) + .process(RoomSummaryEntityFields.ROOM_ID, queryParams.roomId) + .let { + if (queryParams.displayName.isNormalized()) { + it.process(RoomSummaryEntityFields.NORMALIZED_DISPLAY_NAME, queryParams.displayName) + } else { + it.process(RoomSummaryEntityFields.DISPLAY_NAME, queryParams.displayName) + } + } + .process(RoomSummaryEntityFields.CANONICAL_ALIAS, queryParams.canonicalAlias) + .process(RoomSummaryEntityFields.MEMBERSHIP_STR, queryParams.memberships) + .equalTo(RoomSummaryEntityFields.IS_HIDDEN_FROM_USER, false) + } queryParams.roomCategoryFilter?.let { when (it) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt index 4a6e27b7c0..3556cabb33 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt @@ -34,6 +34,8 @@ import org.matrix.android.sdk.api.session.room.model.VersioningState import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper import org.matrix.android.sdk.api.session.room.send.SendState +import org.matrix.android.sdk.api.session.sync.model.RoomSyncSummary +import org.matrix.android.sdk.api.session.sync.model.RoomSyncUnreadNotifications import org.matrix.android.sdk.internal.crypto.EventDecryptor import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM import org.matrix.android.sdk.internal.crypto.crosssigning.DefaultCrossSigningService @@ -63,8 +65,7 @@ import org.matrix.android.sdk.internal.session.room.accountdata.RoomAccountDataD import org.matrix.android.sdk.internal.session.room.membership.RoomDisplayNameResolver import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper import org.matrix.android.sdk.internal.session.room.relationship.RoomChildRelationInfo -import org.matrix.android.sdk.internal.session.sync.model.RoomSyncSummary -import org.matrix.android.sdk.internal.session.sync.model.RoomSyncUnreadNotifications +import org.matrix.android.sdk.internal.util.Normalizer import timber.log.Timber import javax.inject.Inject import kotlin.system.measureTimeMillis @@ -75,7 +76,8 @@ internal class RoomSummaryUpdater @Inject constructor( private val roomAvatarResolver: RoomAvatarResolver, private val eventDecryptor: EventDecryptor, private val crossSigningService: DefaultCrossSigningService, - private val roomAccountDataDataSource: RoomAccountDataDataSource) { + private val roomAccountDataDataSource: RoomAccountDataDataSource, + private val normalizer: Normalizer) { fun update(realm: Realm, roomId: String, @@ -105,8 +107,8 @@ internal class RoomSummaryUpdater @Inject constructor( } // Hard to filter from the app now we use PagedList... - roomSummaryEntity.isHiddenFromUser = roomSummaryEntity.versioningState == VersioningState.UPGRADED_ROOM_JOINED - || roomAccountDataDataSource.getAccountDataEvent(roomId, RoomAccountDataTypes.EVENT_TYPE_VIRTUAL_ROOM) != null + roomSummaryEntity.isHiddenFromUser = roomSummaryEntity.versioningState == VersioningState.UPGRADED_ROOM_JOINED || + roomAccountDataDataSource.getAccountDataEvent(roomId, RoomAccountDataTypes.EVENT_TYPE_VIRTUAL_ROOM) != null val lastNameEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_NAME, stateKey = "")?.root val lastTopicEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_TOPIC, stateKey = "")?.root @@ -132,11 +134,11 @@ internal class RoomSummaryUpdater @Inject constructor( roomSummaryEntity.lastActivityTime = lastActivityFromEvent } - roomSummaryEntity.hasUnreadMessages = roomSummaryEntity.notificationCount > 0 + roomSummaryEntity.hasUnreadMessages = roomSummaryEntity.notificationCount > 0 || // avoid this call if we are sure there are unread events - || !isEventRead(realm.configuration, userId, roomId, latestPreviewableEvent?.eventId) + !isEventRead(realm.configuration, userId, roomId, latestPreviewableEvent?.eventId) - roomSummaryEntity.displayName = roomDisplayNameResolver.resolve(realm, roomId) + roomSummaryEntity.setDisplayName(roomDisplayNameResolver.resolve(realm, roomId)) roomSummaryEntity.avatarUrl = roomAvatarResolver.resolve(realm, roomId) roomSummaryEntity.name = ContentMapper.map(lastNameEvent?.content).toModel()?.name roomSummaryEntity.topic = ContentMapper.map(lastTopicEvent?.content).toModel()?.topic diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/tags/DefaultTagsService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/tags/DefaultTagsService.kt index 02acaa0570..131bd40f1e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/tags/DefaultTagsService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/tags/DefaultTagsService.kt @@ -17,8 +17,8 @@ package org.matrix.android.sdk.internal.session.room.tags import dagger.assisted.Assisted -import dagger.assisted.AssistedInject import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import org.matrix.android.sdk.api.session.room.tags.TagsService internal class DefaultTagsService @AssistedInject constructor( diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt index ca86771f9b..b4e839ae21 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt @@ -32,7 +32,7 @@ import org.matrix.android.sdk.api.session.room.timeline.Timeline import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask -import org.matrix.android.sdk.internal.session.sync.ReadReceiptHandler +import org.matrix.android.sdk.internal.session.sync.handler.room.ReadReceiptHandler import org.matrix.android.sdk.internal.task.SemaphoreCoroutineSequencer import org.matrix.android.sdk.internal.util.createBackgroundHandler import timber.log.Timber diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt index 9c64dce388..a898510654 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt @@ -37,7 +37,7 @@ import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask -import org.matrix.android.sdk.internal.session.sync.ReadReceiptHandler +import org.matrix.android.sdk.internal.session.sync.handler.room.ReadReceiptHandler import org.matrix.android.sdk.internal.task.TaskExecutor internal class DefaultTimelineService @AssistedInject constructor( diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LiveTimelineEvent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LiveTimelineEvent.kt index eb4900553b..64b1a4ff1d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LiveTimelineEvent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LiveTimelineEvent.kt @@ -40,8 +40,8 @@ internal class LiveTimelineEvent(private val monarchy: Monarchy, private val coroutineScope: CoroutineScope, private val timelineEventMapper: TimelineEventMapper, private val roomId: String, - private val eventId: String) - : MediatorLiveData>() { + private val eventId: String) : + MediatorLiveData>() { init { buildAndObserveQuery() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/tombstone/RoomTombstoneEventProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/tombstone/RoomTombstoneEventProcessor.kt index 8022d98975..2b404775f0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/tombstone/RoomTombstoneEventProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/tombstone/RoomTombstoneEventProcessor.kt @@ -16,6 +16,7 @@ package org.matrix.android.sdk.internal.session.room.tombstone +import io.realm.Realm import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.toModel @@ -25,7 +26,6 @@ import org.matrix.android.sdk.internal.database.model.EventInsertType import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor -import io.realm.Realm import javax.inject.Inject internal class RoomTombstoneEventProcessor @Inject constructor() : EventInsertLiveProcessor { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/uploads/DefaultUploadsService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/uploads/DefaultUploadsService.kt index 6d841644dc..99cf36faec 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/uploads/DefaultUploadsService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/uploads/DefaultUploadsService.kt @@ -17,8 +17,8 @@ package org.matrix.android.sdk.internal.session.room.uploads import dagger.assisted.Assisted -import dagger.assisted.AssistedInject import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import org.matrix.android.sdk.api.session.crypto.CryptoService import org.matrix.android.sdk.api.session.room.uploads.GetUploadsResult import org.matrix.android.sdk.api.session.room.uploads.UploadsService diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/signout/SignInAgainTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/signout/SignInAgainTask.kt index 563e85aefc..42fdf30501 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/signout/SignInAgainTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/signout/SignInAgainTask.kt @@ -42,11 +42,12 @@ internal class DefaultSignInAgainTask @Inject constructor( signOutAPI.loginAgain( PasswordLoginParams.userIdentifier( // Reuse the same userId - sessionParams.userId, - params.password, + user = sessionParams.userId, + password = params.password, // The spec says the initial device name will be ignored // https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-login // but https://github.com/matrix-org/synapse/issues/6525 + deviceDisplayName = null, // Reuse the same deviceId deviceId = sessionParams.deviceId ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/signout/SignOutTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/signout/SignOutTask.kt index 9c25eccb3a..19f34746ab 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/signout/SignOutTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/signout/SignOutTask.kt @@ -50,9 +50,9 @@ internal class DefaultSignOutTask @Inject constructor( } } catch (throwable: Throwable) { // Maybe due to https://github.com/matrix-org/synapse/issues/5756 - if (throwable is Failure.ServerError - && throwable.httpCode == HttpURLConnection.HTTP_UNAUTHORIZED /* 401 */ - && throwable.error.code == MatrixError.M_UNKNOWN_TOKEN) { + if (throwable is Failure.ServerError && + throwable.httpCode == HttpURLConnection.HTTP_UNAUTHORIZED && /* 401 */ + throwable.error.code == MatrixError.M_UNKNOWN_TOKEN) { // Also throwable.error.isSoftLogout should be true // Ignore Timber.w("Ignore error due to https://github.com/matrix-org/synapse/issues/5755") diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt index ac20c79058..ebd5f2578e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt @@ -23,6 +23,7 @@ import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.RoomSortOrder 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 @@ -94,12 +95,14 @@ internal class DefaultSpaceService @Inject constructor( return spaceGetter.get(spaceId) } - override fun getSpaceSummariesLive(queryParams: SpaceSummaryQueryParams): LiveData> { - return roomSummaryDataSource.getSpaceSummariesLive(queryParams) + override fun getSpaceSummariesLive(queryParams: SpaceSummaryQueryParams, + sortOrder: RoomSortOrder): LiveData> { + return roomSummaryDataSource.getSpaceSummariesLive(queryParams, sortOrder) } - override fun getSpaceSummaries(spaceSummaryQueryParams: SpaceSummaryQueryParams): List { - return roomSummaryDataSource.getSpaceSummaries(spaceSummaryQueryParams) + override fun getSpaceSummaries(spaceSummaryQueryParams: SpaceSummaryQueryParams, + sortOrder: RoomSortOrder): List { + return roomSummaryDataSource.getSpaceSummaries(spaceSummaryQueryParams, sortOrder) } override fun getRootSpaceSummaries(): List { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/peeking/PeekSpaceTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/peeking/PeekSpaceTask.kt index 5cbaaa45c4..c45d4420ae 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/peeking/PeekSpaceTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/peeking/PeekSpaceTask.kt @@ -76,9 +76,9 @@ internal class DefaultPeekSpaceTask @Inject constructor( if (depth >= maxDepth) return emptyList() val childRoomsIds = stateEvents .filter { - it.type == EventType.STATE_SPACE_CHILD && !it.stateKey.isNullOrEmpty() + it.type == EventType.STATE_SPACE_CHILD && !it.stateKey.isNullOrEmpty() && // Children where via is not present are ignored. - && it.content?.toModel()?.via != null + it.content?.toModel()?.via != null } .map { it.stateKey to it.content?.toModel() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/peeking/SpacePeekResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/peeking/SpacePeekResult.kt index 44d879f05d..a2ffd8221a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/peeking/SpacePeekResult.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/peeking/SpacePeekResult.kt @@ -28,6 +28,7 @@ data class SpacePeekSummary( interface ISpaceChild { val id: String val roomPeekResult: PeekResult + // val default: Boolean? val order: String? } @@ -52,5 +53,5 @@ sealed class SpacePeekResult { data class FailedToResolve(val spaceId: String, val roomPeekResult: PeekResult) : SpacePeekError() data class NotSpaceType(val spaceId: String) : SpacePeekError() - data class Success(val summary: SpacePeekSummary): SpacePeekResult() + data class Success(val summary: SpacePeekSummary) : SpacePeekResult() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/InitialSyncStatusRepository.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/InitialSyncStatusRepository.kt index cf67bbd805..bd20ada28b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/InitialSyncStatusRepository.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/InitialSyncStatusRepository.kt @@ -63,8 +63,8 @@ internal class FileInitialSyncStatusRepository(directory: File) : InitialSyncSta override fun getStep(): Int { ensureCache() val state = cache?.step ?: InitialSyncStatus.STEP_INIT - return if (state >= InitialSyncStatus.STEP_DOWNLOADED - && System.currentTimeMillis() > (cache?.downloadedDate ?: 0) + INIT_SYNC_FILE_LIFETIME) { + return if (state >= InitialSyncStatus.STEP_DOWNLOADED && + System.currentTimeMillis() > (cache?.downloadedDate ?: 0) + INIT_SYNC_FILE_LIFETIME) { Timber.d("INIT_SYNC downloaded file is outdated, download it again") // The downloaded file is outdated setStep(InitialSyncStatus.STEP_INIT) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/InitialSyncStrategy.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/InitialSyncStrategy.kt index 7d93e30191..4bc866b36d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/InitialSyncStrategy.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/InitialSyncStrategy.kt @@ -39,7 +39,7 @@ sealed class InitialSyncStrategy { * Limit to reach to decide to split the init sync response into smaller files * Empiric value: 1 megabytes */ - val minSizeToSplit: Long = 1024 * 1024, + val minSizeToSplit: Long = 1_048_576, // 1024 * 1024 /** * Limit per room to reach to decide to store a join room ephemeral Events into a file * Empiric value: 1 kilobytes diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncEphemeralTemporaryStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncEphemeralTemporaryStore.kt index c6ff71cfcf..8c68e224dc 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncEphemeralTemporaryStore.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncEphemeralTemporaryStore.kt @@ -20,8 +20,8 @@ import com.squareup.moshi.JsonReader import com.squareup.moshi.Moshi import okio.buffer import okio.source +import org.matrix.android.sdk.api.session.sync.model.RoomSyncEphemeral import org.matrix.android.sdk.internal.di.SessionFilesDirectory -import org.matrix.android.sdk.internal.session.sync.model.RoomSyncEphemeral import org.matrix.android.sdk.internal.util.md5 import timber.log.Timber import java.io.File @@ -39,8 +39,11 @@ internal class RoomSyncEphemeralTemporaryStoreFile @Inject constructor( moshi: Moshi ) : RoomSyncEphemeralTemporaryStore { - private val workingDir = File(fileDirectory, "rr") - .also { it.mkdirs() } + private val workingDir: File by lazy { + File(fileDirectory, "rr").also { + it.mkdirs() + } + } private val roomSyncEphemeralAdapter = moshi.adapter(RoomSyncEphemeral::class.java) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncAPI.kt index 2616803463..73ec0aa7ef 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncAPI.kt @@ -17,9 +17,9 @@ package org.matrix.android.sdk.internal.session.sync import okhttp3.ResponseBody +import org.matrix.android.sdk.api.session.sync.model.SyncResponse import org.matrix.android.sdk.internal.network.NetworkConstants import org.matrix.android.sdk.internal.network.TimeOutInterceptor -import org.matrix.android.sdk.internal.session.sync.model.SyncResponse import retrofit2.Call import retrofit2.http.GET import retrofit2.http.Header diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt index a4468a96c9..335f619623 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt @@ -21,6 +21,9 @@ import com.zhuinden.monarchy.Monarchy import org.matrix.android.sdk.api.pushrules.PushRuleService import org.matrix.android.sdk.api.pushrules.RuleScope import org.matrix.android.sdk.api.session.initsync.InitSyncStep +import org.matrix.android.sdk.api.session.sync.model.GroupsSyncResponse +import org.matrix.android.sdk.api.session.sync.model.RoomsSyncResponse +import org.matrix.android.sdk.api.session.sync.model.SyncResponse import org.matrix.android.sdk.internal.crypto.DefaultCryptoService import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.SessionId @@ -30,9 +33,12 @@ import org.matrix.android.sdk.internal.session.group.GetGroupDataWorker import org.matrix.android.sdk.internal.session.initsync.ProgressReporter import org.matrix.android.sdk.internal.session.initsync.reportSubtask import org.matrix.android.sdk.internal.session.notification.ProcessEventForPushTask -import org.matrix.android.sdk.internal.session.sync.model.GroupsSyncResponse -import org.matrix.android.sdk.internal.session.sync.model.RoomsSyncResponse -import org.matrix.android.sdk.internal.session.sync.model.SyncResponse +import org.matrix.android.sdk.internal.session.sync.handler.CryptoSyncHandler +import org.matrix.android.sdk.internal.session.sync.handler.GroupSyncHandler +import org.matrix.android.sdk.internal.session.sync.handler.PresenceSyncHandler +import org.matrix.android.sdk.internal.session.sync.handler.SyncResponsePostTreatmentAggregatorHandler +import org.matrix.android.sdk.internal.session.sync.handler.UserAccountDataSyncHandler +import org.matrix.android.sdk.internal.session.sync.handler.room.RoomSyncHandler import org.matrix.android.sdk.internal.util.awaitTransaction import org.matrix.android.sdk.internal.worker.WorkerParamsFactory import timber.log.Timber @@ -55,7 +61,9 @@ internal class SyncResponseHandler @Inject constructor( private val cryptoService: DefaultCryptoService, private val tokenStore: SyncTokenStore, private val processEventForPushTask: ProcessEventForPushTask, - private val pushRuleService: PushRuleService) { + private val pushRuleService: PushRuleService, + private val presenceSyncHandler: PresenceSyncHandler +) { suspend fun handleResponse(syncResponse: SyncResponse, fromToken: String?, @@ -118,6 +126,13 @@ internal class SyncResponseHandler @Inject constructor( }.also { Timber.v("Finish handling accountData in $it ms") } + + measureTimeMillis { + Timber.v("Handle Presence") + presenceSyncHandler.handle(realm, syncResponse.presence) + }.also { + Timber.v("Finish handling Presence in $it ms") + } tokenStore.saveToken(realm, syncResponse.nextBatch) } @@ -145,7 +160,8 @@ internal class SyncResponseHandler @Inject constructor( private fun dispatchInvitedRoom(roomsSyncResponse: RoomsSyncResponse) { roomsSyncResponse.invite.keys.forEach { roomId -> sessionListeners.dispatch { session, listener -> - listener.onNewInvitedRoom(session, roomId) } + listener.onNewInvitedRoom(session, roomId) + } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponsePostTreatmentAggregator.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponsePostTreatmentAggregator.kt index 9bb2bfc9b1..fe44531390 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponsePostTreatmentAggregator.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponsePostTreatmentAggregator.kt @@ -5,7 +5,7 @@ * 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 + * 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, @@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.session.sync internal class SyncResponsePostTreatmentAggregator { // List of RoomId val ephemeralFilesToDelete = mutableListOf() + // Map of roomId to directUserId val directChatsToCheck = mutableMapOf() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTask.kt index c80fbe60c1..621a08a414 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTask.kt @@ -17,7 +17,11 @@ package org.matrix.android.sdk.internal.session.sync import okhttp3.ResponseBody +import org.matrix.android.sdk.api.logger.LoggerTag import org.matrix.android.sdk.api.session.initsync.InitSyncStep +import org.matrix.android.sdk.api.session.initsync.SyncStatusService +import org.matrix.android.sdk.api.session.sync.model.LazyRoomSyncEphemeral +import org.matrix.android.sdk.api.session.sync.model.SyncResponse import org.matrix.android.sdk.internal.di.SessionFilesDirectory import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.network.GlobalErrorReceiver @@ -26,9 +30,8 @@ import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.network.toFailure import org.matrix.android.sdk.internal.session.filter.FilterRepository import org.matrix.android.sdk.internal.session.homeserver.GetHomeServerCapabilitiesTask -import org.matrix.android.sdk.internal.session.initsync.DefaultInitialSyncProgressService +import org.matrix.android.sdk.internal.session.initsync.DefaultSyncStatusService import org.matrix.android.sdk.internal.session.initsync.reportSubtask -import org.matrix.android.sdk.internal.session.sync.model.LazyRoomSyncEphemeral import org.matrix.android.sdk.internal.session.sync.parsing.InitialSyncResponseParser import org.matrix.android.sdk.internal.session.user.UserStore import org.matrix.android.sdk.internal.task.Task @@ -40,7 +43,9 @@ import java.io.File import java.net.SocketTimeoutException import javax.inject.Inject -internal interface SyncTask : Task { +private val loggerTag = LoggerTag("SyncTask", LoggerTag.SYNC) + +internal interface SyncTask : Task { data class Params( val timeout: Long, @@ -53,7 +58,7 @@ internal class DefaultSyncTask @Inject constructor( @UserId private val userId: String, private val filterRepository: FilterRepository, private val syncResponseHandler: SyncResponseHandler, - private val initialSyncProgressService: DefaultInitialSyncProgressService, + private val defaultSyncStatusService: DefaultSyncStatusService, private val syncTokenStore: SyncTokenStore, private val getHomeServerCapabilitiesTask: GetHomeServerCapabilitiesTask, private val userStore: UserStore, @@ -68,14 +73,14 @@ internal class DefaultSyncTask @Inject constructor( private val workingDir = File(fileDirectory, "is") private val initialSyncStatusRepository: InitialSyncStatusRepository = FileInitialSyncStatusRepository(workingDir) - override suspend fun execute(params: SyncTask.Params) { - syncTaskSequencer.post { + override suspend fun execute(params: SyncTask.Params): SyncResponse { + return syncTaskSequencer.post { doSync(params) } } - private suspend fun doSync(params: SyncTask.Params) { - Timber.v("Sync task started on Thread: ${Thread.currentThread().name}") + private suspend fun doSync(params: SyncTask.Params): SyncResponse { + Timber.tag(loggerTag.value).d("Sync task started on Thread: ${Thread.currentThread().name}") val requestParams = HashMap() var timeout = 0L @@ -92,28 +97,29 @@ internal class DefaultSyncTask @Inject constructor( if (isInitialSync) { // We might want to get the user information in parallel too userStore.createOrUpdate(userId) - initialSyncProgressService.startRoot(InitSyncStep.ImportingAccount, 100) + defaultSyncStatusService.startRoot(InitSyncStep.ImportingAccount, 100) } // Maybe refresh the homeserver capabilities data we know getHomeServerCapabilitiesTask.execute(GetHomeServerCapabilitiesTask.Params(forceRefresh = false)) val readTimeOut = (params.timeout + TIMEOUT_MARGIN).coerceAtLeast(TimeOutInterceptor.DEFAULT_LONG_TIMEOUT) + var syncResponseToReturn: SyncResponse? = null if (isInitialSync) { - Timber.d("INIT_SYNC with filter: ${requestParams["filter"]}") + Timber.tag(loggerTag.value).d("INIT_SYNC with filter: ${requestParams["filter"]}") val initSyncStrategy = initialSyncStrategy - logDuration("INIT_SYNC strategy: $initSyncStrategy") { + logDuration("INIT_SYNC strategy: $initSyncStrategy", loggerTag) { if (initSyncStrategy is InitialSyncStrategy.Optimized) { roomSyncEphemeralTemporaryStore.reset() workingDir.mkdirs() val file = downloadInitSyncResponse(requestParams) - reportSubtask(initialSyncProgressService, InitSyncStep.ImportingAccount, 1, 0.7F) { + syncResponseToReturn = reportSubtask(defaultSyncStatusService, InitSyncStep.ImportingAccount, 1, 0.7F) { handleSyncFile(file, initSyncStrategy) } // Delete all files workingDir.deleteRecursively() } else { - val syncResponse = logDuration("INIT_SYNC Request") { + val syncResponse = logDuration("INIT_SYNC Request", loggerTag) { executeRequest(globalErrorReceiver) { syncAPI.sync( params = requestParams, @@ -121,44 +127,64 @@ internal class DefaultSyncTask @Inject constructor( ) } } - - logDuration("INIT_SYNC Database insertion") { - syncResponseHandler.handleResponse(syncResponse, token, initialSyncProgressService) + logDuration("INIT_SYNC Database insertion", loggerTag) { + syncResponseHandler.handleResponse(syncResponse, token, defaultSyncStatusService) } + syncResponseToReturn = syncResponse } } - initialSyncProgressService.endAll() + defaultSyncStatusService.endAll() } else { - val syncResponse = executeRequest(globalErrorReceiver) { - syncAPI.sync( - params = requestParams, - readTimeOut = readTimeOut - ) + Timber.tag(loggerTag.value).d("Start incremental sync request") + defaultSyncStatusService.setStatus(SyncStatusService.Status.IncrementalSyncIdle) + val syncResponse = try { + executeRequest(globalErrorReceiver) { + syncAPI.sync( + params = requestParams, + readTimeOut = readTimeOut + ) + } + } catch (throwable: Throwable) { + Timber.tag(loggerTag.value).e(throwable, "Incremental sync request error") + defaultSyncStatusService.setStatus(SyncStatusService.Status.IncrementalSyncError) + throw throwable } + val nbRooms = syncResponse.rooms?.invite.orEmpty().size + syncResponse.rooms?.join.orEmpty().size + syncResponse.rooms?.leave.orEmpty().size + val nbToDevice = syncResponse.toDevice?.events.orEmpty().size + Timber.tag(loggerTag.value).d("Incremental sync request parsing, $nbRooms room(s) $nbToDevice toDevice(s)") + defaultSyncStatusService.setStatus(SyncStatusService.Status.IncrementalSyncParsing( + rooms = nbRooms, + toDevice = nbToDevice + )) syncResponseHandler.handleResponse(syncResponse, token, null) + syncResponseToReturn = syncResponse + Timber.tag(loggerTag.value).d("Incremental sync done") + defaultSyncStatusService.setStatus(SyncStatusService.Status.IncrementalSyncDone) } - Timber.v("Sync task finished on Thread: ${Thread.currentThread().name}") + Timber.tag(loggerTag.value).d("Sync task finished on Thread: ${Thread.currentThread().name}") + // Should throw if null as it's a mandatory value. + return syncResponseToReturn!! } private suspend fun downloadInitSyncResponse(requestParams: Map): File { val workingFile = File(workingDir, "initSync.json") val status = initialSyncStatusRepository.getStep() if (workingFile.exists() && status >= InitialSyncStatus.STEP_DOWNLOADED) { - Timber.d("INIT_SYNC file is already here") - reportSubtask(initialSyncProgressService, InitSyncStep.Downloading, 1, 0.3f) { + Timber.tag(loggerTag.value).d("INIT_SYNC file is already here") + reportSubtask(defaultSyncStatusService, InitSyncStep.Downloading, 1, 0.3f) { // Empty task } } else { initialSyncStatusRepository.setStep(InitialSyncStatus.STEP_DOWNLOADING) - val syncResponse = logDuration("INIT_SYNC Perform server request") { - reportSubtask(initialSyncProgressService, InitSyncStep.ServerComputing, 1, 0.2f) { + val syncResponse = logDuration("INIT_SYNC Perform server request", loggerTag) { + reportSubtask(defaultSyncStatusService, InitSyncStep.ServerComputing, 1, 0.2f) { getSyncResponse(requestParams, MAX_NUMBER_OF_RETRY_AFTER_TIMEOUT) } } if (syncResponse.isSuccessful) { - logDuration("INIT_SYNC Download and save to file") { - reportSubtask(initialSyncProgressService, InitSyncStep.Downloading, 1, 0.1f) { + logDuration("INIT_SYNC Download and save to file", loggerTag) { + reportSubtask(defaultSyncStatusService, InitSyncStep.Downloading, 1, 0.1f) { syncResponse.body()?.byteStream()?.use { inputStream -> workingFile.outputStream().use { outputStream -> inputStream.copyTo(outputStream) @@ -168,7 +194,7 @@ internal class DefaultSyncTask @Inject constructor( } } else { throw syncResponse.toFailure(globalErrorReceiver) - .also { Timber.w("INIT_SYNC request failure: $this") } + .also { Timber.tag(loggerTag.value).w("INIT_SYNC request failure: $this") } } initialSyncStatusRepository.setStep(InitialSyncStatus.STEP_DOWNLOADED) } @@ -185,30 +211,31 @@ internal class DefaultSyncTask @Inject constructor( ).awaitResponse() } catch (throwable: Throwable) { if (throwable is SocketTimeoutException && retry > 0) { - Timber.w("INIT_SYNC timeout retry left: $retry") + Timber.tag(loggerTag.value).w("INIT_SYNC timeout retry left: $retry") } else { - Timber.e(throwable, "INIT_SYNC timeout, no retry left, or other error") + Timber.tag(loggerTag.value).e(throwable, "INIT_SYNC timeout, no retry left, or other error") throw throwable } } } } - private suspend fun handleSyncFile(workingFile: File, initSyncStrategy: InitialSyncStrategy.Optimized) { - logDuration("INIT_SYNC handleSyncFile()") { - val syncResponse = logDuration("INIT_SYNC Read file and parse") { + private suspend fun handleSyncFile(workingFile: File, initSyncStrategy: InitialSyncStrategy.Optimized): SyncResponse { + return logDuration("INIT_SYNC handleSyncFile()", loggerTag) { + val syncResponse = logDuration("INIT_SYNC Read file and parse", loggerTag) { syncResponseParser.parse(initSyncStrategy, workingFile) } initialSyncStatusRepository.setStep(InitialSyncStatus.STEP_PARSED) // Log some stats val nbOfJoinedRooms = syncResponse.rooms?.join?.size ?: 0 val nbOfJoinedRoomsInFile = syncResponse.rooms?.join?.values?.count { it.ephemeral is LazyRoomSyncEphemeral.Stored } - Timber.d("INIT_SYNC $nbOfJoinedRooms rooms, $nbOfJoinedRoomsInFile ephemeral stored into files") + Timber.tag(loggerTag.value).d("INIT_SYNC $nbOfJoinedRooms rooms, $nbOfJoinedRoomsInFile ephemeral stored into files") - logDuration("INIT_SYNC Database insertion") { - syncResponseHandler.handleResponse(syncResponse, null, initialSyncProgressService) + logDuration("INIT_SYNC Database insertion", loggerTag) { + syncResponseHandler.handleResponse(syncResponse, null, defaultSyncStatusService) } initialSyncStatusRepository.setStep(InitialSyncStatus.STEP_SUCCESS) + syncResponse } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTokenStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTokenStore.kt index cf061586b7..35e561a106 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTokenStore.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTokenStore.kt @@ -17,9 +17,9 @@ package org.matrix.android.sdk.internal.session.sync import com.zhuinden.monarchy.Monarchy +import io.realm.Realm import org.matrix.android.sdk.internal.database.model.SyncEntity import org.matrix.android.sdk.internal.di.SessionDatabase -import io.realm.Realm import javax.inject.Inject internal class SyncTokenStore @Inject constructor(@SessionDatabase private val monarchy: Monarchy) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/CryptoSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/CryptoSyncHandler.kt similarity index 92% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/CryptoSyncHandler.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/CryptoSyncHandler.kt index 411a9c5c06..c5ec34176c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/CryptoSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/CryptoSyncHandler.kt @@ -14,21 +14,21 @@ * limitations under the License. */ -package org.matrix.android.sdk.internal.session.sync +package org.matrix.android.sdk.internal.session.sync.handler import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.events.model.Event 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.message.MessageContent +import org.matrix.android.sdk.api.session.sync.model.SyncResponse +import org.matrix.android.sdk.api.session.sync.model.ToDeviceSyncResponse import org.matrix.android.sdk.internal.crypto.DefaultCryptoService import org.matrix.android.sdk.internal.crypto.MXEventDecryptionResult import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult import org.matrix.android.sdk.internal.crypto.model.event.OlmEventContent import org.matrix.android.sdk.internal.crypto.verification.DefaultVerificationService import org.matrix.android.sdk.internal.session.initsync.ProgressReporter -import org.matrix.android.sdk.internal.session.sync.model.SyncResponse -import org.matrix.android.sdk.internal.session.sync.model.ToDeviceSyncResponse import timber.log.Timber import javax.inject.Inject @@ -42,8 +42,8 @@ internal class CryptoSyncHandler @Inject constructor(private val cryptoService: // Decrypt event if necessary Timber.i("## CRYPTO | To device event from ${event.senderId} of type:${event.type}") decryptToDeviceEvent(event, null) - if (event.getClearType() == EventType.MESSAGE - && event.getClearContent()?.toModel()?.msgType == "m.bad.encrypted") { + if (event.getClearType() == EventType.MESSAGE && + event.getClearContent()?.toModel()?.msgType == "m.bad.encrypted") { Timber.e("## CRYPTO | handleToDeviceEvent() : Warning: Unable to decrypt to-device event : ${event.content}") } else { verificationService.onToDeviceEvent(event) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/GroupSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/GroupSyncHandler.kt similarity index 95% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/GroupSyncHandler.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/GroupSyncHandler.kt index 02362bf050..552462e25e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/GroupSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/GroupSyncHandler.kt @@ -14,19 +14,19 @@ * limitations under the License. */ -package org.matrix.android.sdk.internal.session.sync +package org.matrix.android.sdk.internal.session.sync.handler import io.realm.Realm import org.matrix.android.sdk.api.session.initsync.InitSyncStep import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.api.session.sync.model.GroupsSyncResponse +import org.matrix.android.sdk.api.session.sync.model.InvitedGroupSync import org.matrix.android.sdk.internal.database.model.GroupEntity import org.matrix.android.sdk.internal.database.model.GroupSummaryEntity import org.matrix.android.sdk.internal.database.query.getOrCreate import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.session.initsync.ProgressReporter import org.matrix.android.sdk.internal.session.initsync.mapWithProgress -import org.matrix.android.sdk.internal.session.sync.model.GroupsSyncResponse -import org.matrix.android.sdk.internal.session.sync.model.InvitedGroupSync import javax.inject.Inject internal class GroupSyncHandler @Inject constructor() { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/PresenceSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/PresenceSyncHandler.kt new file mode 100644 index 0000000000..fe173a35c3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/PresenceSyncHandler.kt @@ -0,0 +1,61 @@ +/* + * 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.internal.session.sync.handler + +import io.realm.Realm +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.getPresenceContent +import org.matrix.android.sdk.api.session.sync.model.PresenceSyncResponse +import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity +import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity +import org.matrix.android.sdk.internal.database.model.presence.UserPresenceEntity +import org.matrix.android.sdk.internal.database.query.updateDirectUserPresence +import org.matrix.android.sdk.internal.database.query.updateUserPresence +import javax.inject.Inject + +internal class PresenceSyncHandler @Inject constructor() { + + fun handle(realm: Realm, presenceSyncResponse: PresenceSyncResponse?) { + presenceSyncResponse?.events + ?.filter { event -> event.type == EventType.PRESENCE } + ?.forEach { event -> + val content = event.getPresenceContent() ?: return@forEach + val userId = event.senderId ?: return@forEach + val userPresenceEntity = UserPresenceEntity( + userId = userId, + lastActiveAgo = content.lastActiveAgo, + statusMessage = content.statusMessage, + isCurrentlyActive = content.isCurrentlyActive, + avatarUrl = content.avatarUrl, + displayName = content.displayName + ).also { + it.presence = content.presence + } + + storePresenceToDB(realm, userPresenceEntity) + } + } + + /** + * Store user presence to DB and update Direct Rooms and Room Member Summaries accordingly + */ + private fun storePresenceToDB(realm: Realm, userPresenceEntity: UserPresenceEntity) = + realm.copyToRealmOrUpdate(userPresenceEntity)?.apply { + RoomSummaryEntity.updateDirectUserPresence(realm, userPresenceEntity.userId, this) + RoomMemberSummaryEntity.updateUserPresence(realm, userPresenceEntity.userId, this) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponsePostTreatmentAggregatorHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/SyncResponsePostTreatmentAggregatorHandler.kt similarity index 93% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponsePostTreatmentAggregatorHandler.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/SyncResponsePostTreatmentAggregatorHandler.kt index db1100d76c..1e0e87a450 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponsePostTreatmentAggregatorHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/SyncResponsePostTreatmentAggregatorHandler.kt @@ -14,9 +14,11 @@ * limitations under the License. */ -package org.matrix.android.sdk.internal.session.sync +package org.matrix.android.sdk.internal.session.sync.handler import org.matrix.android.sdk.api.MatrixPatterns +import org.matrix.android.sdk.internal.session.sync.RoomSyncEphemeralTemporaryStore +import org.matrix.android.sdk.internal.session.sync.SyncResponsePostTreatmentAggregator import org.matrix.android.sdk.internal.session.sync.model.accountdata.toMutable import org.matrix.android.sdk.internal.session.user.accountdata.DirectChatsHelper import org.matrix.android.sdk.internal.session.user.accountdata.UpdateUserAccountDataTask diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/UserAccountDataSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/UserAccountDataSyncHandler.kt similarity index 96% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/UserAccountDataSyncHandler.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/UserAccountDataSyncHandler.kt index 110e77813d..7f80486c70 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/UserAccountDataSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/UserAccountDataSyncHandler.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.matrix.android.sdk.internal.session.sync +package org.matrix.android.sdk.internal.session.sync.handler import com.zhuinden.monarchy.Monarchy import io.realm.Realm @@ -29,6 +29,8 @@ import org.matrix.android.sdk.api.session.events.model.Content import org.matrix.android.sdk.api.session.events.model.toModel 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.sync.model.InvitedRoomSync +import org.matrix.android.sdk.api.session.sync.model.UserAccountDataSync import org.matrix.android.sdk.internal.database.mapper.ContentMapper import org.matrix.android.sdk.internal.database.mapper.PushRulesMapper import org.matrix.android.sdk.internal.database.mapper.asDomain @@ -48,11 +50,9 @@ import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.session.room.RoomAvatarResolver import org.matrix.android.sdk.internal.session.room.membership.RoomDisplayNameResolver import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper -import org.matrix.android.sdk.internal.session.sync.model.InvitedRoomSync import org.matrix.android.sdk.internal.session.sync.model.accountdata.BreadcrumbsContent import org.matrix.android.sdk.internal.session.sync.model.accountdata.DirectMessagesContent import org.matrix.android.sdk.internal.session.sync.model.accountdata.IgnoredUsersContent -import org.matrix.android.sdk.internal.session.sync.model.accountdata.UserAccountDataSync import org.matrix.android.sdk.internal.session.sync.model.accountdata.toMutable import org.matrix.android.sdk.internal.session.user.accountdata.DirectChatsHelper import org.matrix.android.sdk.internal.session.user.accountdata.UpdateUserAccountDataTask @@ -166,7 +166,7 @@ internal class UserAccountDataSyncHandler @Inject constructor( roomSummaryEntity.directUserId = userId // Also update the avatar and displayname, there is a specific treatment for DMs roomSummaryEntity.avatarUrl = roomAvatarResolver.resolve(realm, roomId) - roomSummaryEntity.displayName = roomDisplayNameResolver.resolve(realm, roomId) + roomSummaryEntity.setDisplayName(roomDisplayNameResolver.resolve(realm, roomId)) } } } @@ -178,7 +178,7 @@ internal class UserAccountDataSyncHandler @Inject constructor( it.directUserId = null // Also update the avatar and displayname, there was a specific treatment for DMs it.avatarUrl = roomAvatarResolver.resolve(realm, it.roomId) - it.displayName = roomDisplayNameResolver.resolve(realm, it.roomId) + it.setDisplayName(roomDisplayNameResolver.resolve(realm, it.roomId)) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/ReadReceiptHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ReadReceiptHandler.kt similarity index 96% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/ReadReceiptHandler.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ReadReceiptHandler.kt index fc1a2c3870..025ee329f8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/ReadReceiptHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ReadReceiptHandler.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.matrix.android.sdk.internal.session.sync +package org.matrix.android.sdk.internal.session.sync.handler.room import io.realm.Realm import org.matrix.android.sdk.api.session.events.model.EventType @@ -23,6 +23,8 @@ import org.matrix.android.sdk.internal.database.model.ReadReceiptsSummaryEntity import org.matrix.android.sdk.internal.database.query.createUnmanaged import org.matrix.android.sdk.internal.database.query.getOrCreate import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.session.sync.RoomSyncEphemeralTemporaryStore +import org.matrix.android.sdk.internal.session.sync.SyncResponsePostTreatmentAggregator import timber.log.Timber import javax.inject.Inject diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomFullyReadHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomFullyReadHandler.kt similarity index 95% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomFullyReadHandler.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomFullyReadHandler.kt index e8934fdf21..b5c8a099d3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomFullyReadHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomFullyReadHandler.kt @@ -14,13 +14,13 @@ * limitations under the License. */ -package org.matrix.android.sdk.internal.session.sync +package org.matrix.android.sdk.internal.session.sync.handler.room +import io.realm.Realm import org.matrix.android.sdk.internal.database.model.ReadMarkerEntity import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity import org.matrix.android.sdk.internal.database.query.getOrCreate import org.matrix.android.sdk.internal.session.room.read.FullyReadContent -import io.realm.Realm import timber.log.Timber import javax.inject.Inject diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt similarity index 97% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncHandler.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt index 830e666c95..8c4af81c99 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.matrix.android.sdk.internal.session.sync +package org.matrix.android.sdk.internal.session.sync.handler.room import io.realm.Realm import io.realm.kotlin.createObject @@ -26,6 +26,10 @@ import org.matrix.android.sdk.api.session.initsync.InitSyncStep import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.RoomMemberContent import org.matrix.android.sdk.api.session.room.send.SendState +import org.matrix.android.sdk.api.session.sync.model.InvitedRoomSync +import org.matrix.android.sdk.api.session.sync.model.LazyRoomSyncEphemeral +import org.matrix.android.sdk.api.session.sync.model.RoomSync +import org.matrix.android.sdk.api.session.sync.model.RoomsSyncResponse import org.matrix.android.sdk.internal.crypto.DefaultCryptoService import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult @@ -58,10 +62,9 @@ import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryUpdater import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection import org.matrix.android.sdk.internal.session.room.timeline.TimelineInput import org.matrix.android.sdk.internal.session.room.typing.TypingEventContent -import org.matrix.android.sdk.internal.session.sync.model.InvitedRoomSync -import org.matrix.android.sdk.internal.session.sync.model.LazyRoomSyncEphemeral -import org.matrix.android.sdk.internal.session.sync.model.RoomSync -import org.matrix.android.sdk.internal.session.sync.model.RoomsSyncResponse +import org.matrix.android.sdk.internal.session.sync.InitialSyncStrategy +import org.matrix.android.sdk.internal.session.sync.SyncResponsePostTreatmentAggregator +import org.matrix.android.sdk.internal.session.sync.initialSyncStrategy import org.matrix.android.sdk.internal.session.sync.parsing.RoomSyncAccountDataHandler import org.matrix.android.sdk.internal.util.computeBestChunkSize import timber.log.Timber diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomTagHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomTagHandler.kt similarity index 95% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomTagHandler.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomTagHandler.kt index add5d841d1..55b15624bc 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomTagHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomTagHandler.kt @@ -14,12 +14,12 @@ * limitations under the License. */ -package org.matrix.android.sdk.internal.session.sync +package org.matrix.android.sdk.internal.session.sync.handler.room +import io.realm.Realm import org.matrix.android.sdk.api.session.room.model.tag.RoomTagContent import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity import org.matrix.android.sdk.internal.database.model.RoomTagEntity -import io.realm.Realm import org.matrix.android.sdk.internal.database.query.getOrCreate import javax.inject.Inject diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomTypingUsersHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomTypingUsersHandler.kt similarity index 96% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomTypingUsersHandler.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomTypingUsersHandler.kt index b7851031ad..63db13a5b8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomTypingUsersHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomTypingUsersHandler.kt @@ -14,13 +14,13 @@ * limitations under the License. */ -package org.matrix.android.sdk.internal.session.sync +package org.matrix.android.sdk.internal.session.sync.handler.room +import io.realm.Realm import org.matrix.android.sdk.api.session.room.sender.SenderInfo import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper import org.matrix.android.sdk.internal.session.typing.DefaultTypingUsersTracker -import io.realm.Realm import javax.inject.Inject internal class RoomTypingUsersHandler @Inject constructor(@UserId private val userId: String, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncService.kt index cce169c246..9480cc73f1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncService.kt @@ -25,6 +25,7 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancelChildren import kotlinx.coroutines.launch import org.matrix.android.sdk.api.Matrix +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.isTokenError import org.matrix.android.sdk.api.session.Session @@ -34,7 +35,6 @@ import org.matrix.android.sdk.internal.session.sync.SyncPresence import org.matrix.android.sdk.internal.session.sync.SyncTask import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.util.BackgroundDetectionObserver -import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers import timber.log.Timber import java.net.SocketTimeoutException import java.util.concurrent.atomic.AtomicBoolean diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt index de8d009892..e1150f2c47 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt @@ -21,24 +21,28 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Observer import com.squareup.moshi.JsonEncodingException import kotlinx.coroutines.CancellationException -import org.matrix.android.sdk.api.failure.Failure -import org.matrix.android.sdk.api.failure.isTokenError -import org.matrix.android.sdk.api.session.sync.SyncState -import org.matrix.android.sdk.internal.network.NetworkConnectivityChecker -import org.matrix.android.sdk.internal.session.sync.SyncTask -import org.matrix.android.sdk.internal.util.BackgroundDetectionObserver -import org.matrix.android.sdk.internal.util.Debouncer -import org.matrix.android.sdk.internal.util.createUIHandler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancelChildren import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking +import org.matrix.android.sdk.api.failure.Failure +import org.matrix.android.sdk.api.failure.isTokenError +import org.matrix.android.sdk.api.logger.LoggerTag import org.matrix.android.sdk.api.session.call.MxCall +import org.matrix.android.sdk.api.session.sync.SyncState +import org.matrix.android.sdk.api.session.sync.model.SyncResponse +import org.matrix.android.sdk.internal.network.NetworkConnectivityChecker import org.matrix.android.sdk.internal.session.call.ActiveCallHandler import org.matrix.android.sdk.internal.session.sync.SyncPresence +import org.matrix.android.sdk.internal.session.sync.SyncTask +import org.matrix.android.sdk.internal.util.BackgroundDetectionObserver +import org.matrix.android.sdk.internal.util.Debouncer +import org.matrix.android.sdk.internal.util.createUIHandler import timber.log.Timber import java.net.SocketTimeoutException import java.util.Timer @@ -49,6 +53,8 @@ import kotlin.concurrent.schedule private const val RETRY_WAIT_TIME_MS = 10_000L private const val DEFAULT_LONG_POOL_TIMEOUT = 30_000L +private val loggerTag = LoggerTag("SyncThread", LoggerTag.SYNC) + internal class SyncThread @Inject constructor(private val syncTask: SyncTask, private val networkConnectivityChecker: NetworkConnectivityChecker, private val backgroundDetectionObserver: BackgroundDetectionObserver, @@ -72,6 +78,8 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask, } } + private val _syncFlow = MutableSharedFlow() + init { updateStateTo(SyncState.Idle) } @@ -83,7 +91,7 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask, fun restart() = synchronized(lock) { if (!isStarted) { - Timber.v("Resume sync...") + Timber.tag(loggerTag.value).d("Resume sync...") isStarted = true // Check again server availability and the token validity canReachServer = true @@ -94,7 +102,7 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask, fun pause() = synchronized(lock) { if (isStarted) { - Timber.v("Pause sync...") + Timber.tag(loggerTag.value).d("Pause sync...") isStarted = false retryNoNetworkTask?.cancel() syncScope.coroutineContext.cancelChildren() @@ -102,7 +110,7 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask, } fun kill() = synchronized(lock) { - Timber.v("Kill sync...") + Timber.tag(loggerTag.value).d("Kill sync...") updateStateTo(SyncState.Killing) retryNoNetworkTask?.cancel() syncScope.coroutineContext.cancelChildren() @@ -115,6 +123,8 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask, return liveState } + fun syncFlow(): SharedFlow = _syncFlow + override fun onConnectivityChanged() { retryNoNetworkTask?.cancel() synchronized(lock) { @@ -124,21 +134,21 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask, } override fun run() { - Timber.v("Start syncing...") + Timber.tag(loggerTag.value).d("Start syncing...") isStarted = true networkConnectivityChecker.register(this) backgroundDetectionObserver.register(this) registerActiveCallsObserver() while (state != SyncState.Killing) { - Timber.v("Entering loop, state: $state") + Timber.tag(loggerTag.value).d("Entering loop, state: $state") if (!isStarted) { - Timber.v("Sync is Paused. Waiting...") + Timber.tag(loggerTag.value).d("Sync is Paused. Waiting...") updateStateTo(SyncState.Paused) synchronized(lock) { lock.wait() } - Timber.v("...unlocked") + Timber.tag(loggerTag.value).d("...unlocked") } else if (!canReachServer) { - Timber.v("No network. Waiting...") + Timber.tag(loggerTag.value).d("No network. Waiting...") updateStateTo(SyncState.NoNetwork) // We force retrying in RETRY_WAIT_TIME_MS maximum. Otherwise it will be unlocked by onConnectivityChanged() or restart() retryNoNetworkTask = Timer(SyncState.NoNetwork.toString(), false).schedule(RETRY_WAIT_TIME_MS) { @@ -148,19 +158,19 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask, } } synchronized(lock) { lock.wait() } - Timber.v("...retry") + Timber.tag(loggerTag.value).d("...retry") } else if (!isTokenValid) { - Timber.v("Token is invalid. Waiting...") + Timber.tag(loggerTag.value).d("Token is invalid. Waiting...") updateStateTo(SyncState.InvalidToken) synchronized(lock) { lock.wait() } - Timber.v("...unlocked") + Timber.tag(loggerTag.value).d("...unlocked") } else { if (state !is SyncState.Running) { updateStateTo(SyncState.Running(afterPause = true)) } // No timeout after a pause val timeout = state.let { if (it is SyncState.Running && it.afterPause) 0 else DEFAULT_LONG_POOL_TIMEOUT } - Timber.v("Execute sync request with timeout $timeout") + Timber.tag(loggerTag.value).d("Execute sync request with timeout $timeout") val params = SyncTask.Params(timeout, SyncPresence.Online) val sync = syncScope.launch { doSync(params) @@ -168,10 +178,10 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask, runBlocking { sync.join() } - Timber.v("...Continue") + Timber.tag(loggerTag.value).d("...Continue") } } - Timber.v("Sync killed") + Timber.tag(loggerTag.value).d("Sync killed") updateStateTo(SyncState.Killed) backgroundDetectionObserver.unregister(this) networkConnectivityChecker.unregister(this) @@ -192,26 +202,27 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask, private suspend fun doSync(params: SyncTask.Params) { try { - syncTask.execute(params) + val syncResponse = syncTask.execute(params) + _syncFlow.emit(syncResponse) } catch (failure: Throwable) { if (failure is Failure.NetworkConnection) { canReachServer = false } if (failure is Failure.NetworkConnection && failure.cause is SocketTimeoutException) { // Timeout are not critical - Timber.v("Timeout") + Timber.tag(loggerTag.value).d("Timeout") } else if (failure is CancellationException) { - Timber.v("Cancelled") + Timber.tag(loggerTag.value).d("Cancelled") } else if (failure.isTokenError()) { // No token or invalid token, stop the thread - Timber.w(failure, "Token error") + Timber.tag(loggerTag.value).w(failure, "Token error") isStarted = false isTokenValid = false } else { - Timber.e(failure) + Timber.tag(loggerTag.value).e(failure) if (failure !is Failure.NetworkConnection || failure.cause is JsonEncodingException) { // Wait 10s before retrying - Timber.v("Wait 10s") + Timber.tag(loggerTag.value).d("Wait 10s") delay(RETRY_WAIT_TIME_MS) } } @@ -225,7 +236,7 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask, } private fun updateStateTo(newState: SyncState) { - Timber.v("Update state from $state to $newState") + Timber.tag(loggerTag.value).d("Update state from $state to $newState") if (newState == state) { return } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/parsing/DefaultLazyRoomSyncEphemeralJsonAdapter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/parsing/DefaultLazyRoomSyncEphemeralJsonAdapter.kt index 940ea219fb..012470a076 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/parsing/DefaultLazyRoomSyncEphemeralJsonAdapter.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/parsing/DefaultLazyRoomSyncEphemeralJsonAdapter.kt @@ -21,10 +21,10 @@ import com.squareup.moshi.JsonAdapter import com.squareup.moshi.JsonReader import com.squareup.moshi.JsonWriter import com.squareup.moshi.ToJson +import org.matrix.android.sdk.api.session.sync.model.LazyRoomSyncEphemeral +import org.matrix.android.sdk.api.session.sync.model.RoomSyncEphemeral import org.matrix.android.sdk.internal.session.sync.InitialSyncStrategy import org.matrix.android.sdk.internal.session.sync.RoomSyncEphemeralTemporaryStore -import org.matrix.android.sdk.internal.session.sync.model.LazyRoomSyncEphemeral -import org.matrix.android.sdk.internal.session.sync.model.RoomSyncEphemeral import timber.log.Timber internal class DefaultLazyRoomSyncEphemeralJsonAdapter { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/parsing/InitialSyncResponseParser.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/parsing/InitialSyncResponseParser.kt index 0b44887aed..f00cce2d5e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/parsing/InitialSyncResponseParser.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/parsing/InitialSyncResponseParser.kt @@ -19,9 +19,9 @@ package org.matrix.android.sdk.internal.session.sync.parsing import com.squareup.moshi.Moshi import okio.buffer import okio.source +import org.matrix.android.sdk.api.session.sync.model.SyncResponse import org.matrix.android.sdk.internal.session.sync.InitialSyncStrategy import org.matrix.android.sdk.internal.session.sync.RoomSyncEphemeralTemporaryStore -import org.matrix.android.sdk.internal.session.sync.model.SyncResponse import timber.log.Timber import java.io.File import javax.inject.Inject diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/parsing/RoomSyncAccountDataHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/parsing/RoomSyncAccountDataHandler.kt index 8bf9ad5b90..5e7bde87e7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/parsing/RoomSyncAccountDataHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/parsing/RoomSyncAccountDataHandler.kt @@ -20,6 +20,7 @@ import io.realm.Realm import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.accountdata.RoomAccountDataTypes import org.matrix.android.sdk.api.session.room.model.tag.RoomTagContent +import org.matrix.android.sdk.api.session.sync.model.RoomSyncAccountData import org.matrix.android.sdk.api.util.JsonDict import org.matrix.android.sdk.internal.database.mapper.ContentMapper import org.matrix.android.sdk.internal.database.model.RoomAccountDataEntity @@ -27,9 +28,8 @@ import org.matrix.android.sdk.internal.database.model.RoomAccountDataEntityField import org.matrix.android.sdk.internal.database.model.RoomEntity import org.matrix.android.sdk.internal.database.query.getOrCreate import org.matrix.android.sdk.internal.session.room.read.FullyReadContent -import org.matrix.android.sdk.internal.session.sync.RoomFullyReadHandler -import org.matrix.android.sdk.internal.session.sync.RoomTagHandler -import org.matrix.android.sdk.internal.session.sync.model.RoomSyncAccountData +import org.matrix.android.sdk.internal.session.sync.handler.room.RoomFullyReadHandler +import org.matrix.android.sdk.internal.session.sync.handler.room.RoomTagHandler import javax.inject.Inject internal class RoomSyncAccountDataHandler @Inject constructor(private val roomTagHandler: RoomTagHandler, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/DefaultTermsService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/DefaultTermsService.kt index 2c7dc92ddd..d40fd8d076 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/DefaultTermsService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/DefaultTermsService.kt @@ -30,8 +30,8 @@ import org.matrix.android.sdk.internal.session.identity.IdentityAuthAPI import org.matrix.android.sdk.internal.session.identity.IdentityRegisterTask import org.matrix.android.sdk.internal.session.openid.GetOpenIdTokenTask import org.matrix.android.sdk.internal.session.sync.model.accountdata.AcceptedTermsContent -import org.matrix.android.sdk.internal.session.user.accountdata.UserAccountDataDataSource import org.matrix.android.sdk.internal.session.user.accountdata.UpdateUserAccountDataTask +import org.matrix.android.sdk.internal.session.user.accountdata.UserAccountDataDataSource import org.matrix.android.sdk.internal.util.ensureTrailingSlash import javax.inject.Inject diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/TermsModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/TermsModule.kt index b7cd7a4a97..d7b6f68add 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/TermsModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/TermsModule.kt @@ -20,11 +20,11 @@ import dagger.Binds import dagger.Lazy import dagger.Module import dagger.Provides +import okhttp3.OkHttpClient import org.matrix.android.sdk.api.session.terms.TermsService import org.matrix.android.sdk.internal.di.UnauthenticatedWithCertificate import org.matrix.android.sdk.internal.network.RetrofitFactory import org.matrix.android.sdk.internal.session.SessionScope -import okhttp3.OkHttpClient @Module internal abstract class TermsModule { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/DefaultThirdPartyService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/DefaultThirdPartyService.kt index 13829c400a..fdd5524fc2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/DefaultThirdPartyService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/DefaultThirdPartyService.kt @@ -22,8 +22,8 @@ import org.matrix.android.sdk.api.session.thirdparty.model.ThirdPartyUser import javax.inject.Inject internal class DefaultThirdPartyService @Inject constructor(private val getThirdPartyProtocolTask: GetThirdPartyProtocolsTask, - private val getThirdPartyUserTask: GetThirdPartyUserTask) - : ThirdPartyService { + private val getThirdPartyUserTask: GetThirdPartyUserTask) : + ThirdPartyService { override suspend fun getThirdPartyProtocols(): Map { return getThirdPartyProtocolTask.execute(Unit) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DefaultSessionAccountDataService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DefaultSessionAccountDataService.kt index ff1750ce8e..ddcac475ee 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DefaultSessionAccountDataService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DefaultSessionAccountDataService.kt @@ -19,13 +19,13 @@ package org.matrix.android.sdk.internal.session.user.accountdata import androidx.lifecycle.LiveData import com.zhuinden.monarchy.Monarchy import org.matrix.android.sdk.api.session.accountdata.SessionAccountDataService +import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.session.room.accountdata.RoomAccountDataEvent import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.internal.di.SessionDatabase -import org.matrix.android.sdk.internal.session.sync.UserAccountDataSyncHandler -import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent -import org.matrix.android.sdk.api.session.room.accountdata.RoomAccountDataEvent import org.matrix.android.sdk.internal.session.room.accountdata.RoomAccountDataDataSource +import org.matrix.android.sdk.internal.session.sync.handler.UserAccountDataSyncHandler import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.task.configureWith import org.matrix.android.sdk.internal.util.awaitCallback diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DirectChatsHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DirectChatsHelper.kt index 3703057643..c7b125b5d6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DirectChatsHelper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DirectChatsHelper.kt @@ -16,11 +16,11 @@ package org.matrix.android.sdk.internal.session.user.accountdata +import io.realm.Realm +import io.realm.RealmConfiguration import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity import org.matrix.android.sdk.internal.database.query.getDirectRooms import org.matrix.android.sdk.internal.di.SessionDatabase -import io.realm.Realm -import io.realm.RealmConfiguration import org.matrix.android.sdk.internal.session.sync.model.accountdata.DirectMessagesContent import javax.inject.Inject diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/SaveBreadcrumbsTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/SaveBreadcrumbsTask.kt index d6c95d6b52..22af56a169 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/SaveBreadcrumbsTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/SaveBreadcrumbsTask.kt @@ -16,6 +16,7 @@ package org.matrix.android.sdk.internal.session.user.accountdata import com.zhuinden.monarchy.Monarchy +import io.realm.RealmList import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.internal.database.model.BreadcrumbsEntity import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity @@ -25,7 +26,6 @@ import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.task.Task import org.matrix.android.sdk.internal.util.awaitTransaction -import io.realm.RealmList import javax.inject.Inject /** diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/UpdateUserAccountDataTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/UpdateUserAccountDataTask.kt index 1a588d2245..88db381852 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/UpdateUserAccountDataTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/UpdateUserAccountDataTask.kt @@ -16,15 +16,15 @@ package org.matrix.android.sdk.internal.session.user.accountdata +import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.session.integrationmanager.AllowedWidgetsContent import org.matrix.android.sdk.internal.session.integrationmanager.IntegrationProvisioningContent import org.matrix.android.sdk.internal.session.sync.model.accountdata.AcceptedTermsContent import org.matrix.android.sdk.internal.session.sync.model.accountdata.BreadcrumbsContent import org.matrix.android.sdk.internal.session.sync.model.accountdata.IdentityServerContent -import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes -import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.task.Task import javax.inject.Inject diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/DefaultWidgetPostAPIMediator.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/DefaultWidgetPostAPIMediator.kt index ba7a2be2a5..07f7c7cb86 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/DefaultWidgetPostAPIMediator.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/DefaultWidgetPostAPIMediator.kt @@ -29,8 +29,8 @@ import java.util.HashMap import javax.inject.Inject internal class DefaultWidgetPostAPIMediator @Inject constructor(private val moshi: Moshi, - private val widgetPostMessageAPIProvider: WidgetPostMessageAPIProvider) - : WidgetPostAPIMediator { + private val widgetPostMessageAPIProvider: WidgetPostMessageAPIProvider) : + WidgetPostAPIMediator { private val jsonAdapter = moshi.adapter(JSON_DICT_PARAMETERIZED_TYPE) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/DefaultWidgetService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/DefaultWidgetService.kt index dfe4b6b810..89e827aea0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/DefaultWidgetService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/DefaultWidgetService.kt @@ -28,8 +28,8 @@ import javax.inject.Provider internal class DefaultWidgetService @Inject constructor(private val widgetManager: WidgetManager, private val widgetURLFormatter: WidgetURLFormatter, - private val widgetPostAPIMediator: Provider) - : WidgetService { + private val widgetPostAPIMediator: Provider) : + WidgetService { override fun getWidgetURLFormatter(): WidgetURLFormatter { return widgetURLFormatter diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/DefaultWidgetURLFormatter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/DefaultWidgetURLFormatter.kt index f7664bf3c2..5879b62446 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/DefaultWidgetURLFormatter.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/DefaultWidgetURLFormatter.kt @@ -18,12 +18,12 @@ package org.matrix.android.sdk.internal.session.widgets import org.matrix.android.sdk.api.MatrixConfiguration import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.SessionLifecycleObserver import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerConfig import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerService import org.matrix.android.sdk.api.session.widgets.WidgetURLFormatter import org.matrix.android.sdk.api.util.appendParamToUrl import org.matrix.android.sdk.api.util.appendParamsToUrl -import org.matrix.android.sdk.api.session.SessionLifecycleObserver import org.matrix.android.sdk.internal.session.SessionScope import org.matrix.android.sdk.internal.session.integrationmanager.IntegrationManager import org.matrix.android.sdk.internal.session.widgets.token.GetScalarTokenTask diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetManager.kt index e0f43a11c5..9f5f91d917 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetManager.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetManager.kt @@ -23,6 +23,7 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.Transformations import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.SessionLifecycleObserver import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes import org.matrix.android.sdk.api.session.events.model.Content @@ -35,7 +36,6 @@ import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper import org.matrix.android.sdk.api.session.widgets.WidgetManagementFailure import org.matrix.android.sdk.api.session.widgets.model.Widget import org.matrix.android.sdk.internal.di.UserId -import org.matrix.android.sdk.api.session.SessionLifecycleObserver import org.matrix.android.sdk.internal.session.SessionScope import org.matrix.android.sdk.internal.session.integrationmanager.IntegrationManager import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource @@ -51,9 +51,9 @@ internal class WidgetManager @Inject constructor(private val integrationManager: private val stateEventDataSource: StateEventDataSource, private val createWidgetTask: CreateWidgetTask, private val widgetFactory: WidgetFactory, - @UserId private val userId: String) + @UserId private val userId: String) : - : IntegrationManagerService.Listener, SessionLifecycleObserver { + IntegrationManagerService.Listener, SessionLifecycleObserver { private val lifecycleOwner: LifecycleOwner = LifecycleOwner { lifecycleRegistry } private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(lifecycleOwner) @@ -155,8 +155,8 @@ internal class WidgetManager @Inject constructor(private val integrationManager: return extractWidgetSequence(widgetFactory) .filter { val widgetType = it.widgetContent.type ?: return@filter false - (widgetTypes == null || widgetTypes.contains(widgetType)) - && (excludedTypes == null || !excludedTypes.contains(widgetType)) + (widgetTypes == null || widgetTypes.contains(widgetType)) && + (excludedTypes == null || !excludedTypes.contains(widgetType)) } .toList() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetsAPIProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetsAPIProvider.kt index 7f79f44767..48c8fcdb03 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetsAPIProvider.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetsAPIProvider.kt @@ -17,10 +17,10 @@ package org.matrix.android.sdk.internal.session.widgets import dagger.Lazy +import okhttp3.OkHttpClient import org.matrix.android.sdk.internal.di.Unauthenticated import org.matrix.android.sdk.internal.network.RetrofitFactory import org.matrix.android.sdk.internal.session.SessionScope -import okhttp3.OkHttpClient import javax.inject.Inject @SessionScope diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/helper/UserAccountWidgets.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/helper/UserAccountWidgets.kt index 6f423b38a0..21fbb77667 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/helper/UserAccountWidgets.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/helper/UserAccountWidgets.kt @@ -16,11 +16,11 @@ package org.matrix.android.sdk.internal.session.widgets.helper +import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.toModel -import org.matrix.android.sdk.api.util.JsonDict -import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent import org.matrix.android.sdk.api.session.widgets.model.Widget +import org.matrix.android.sdk.api.util.JsonDict internal fun UserAccountDataEvent.extractWidgetSequence(widgetFactory: WidgetFactory): Sequence { return content.asSequence() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/helper/WidgetFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/helper/WidgetFactory.kt index a469a9fe97..a5e74a8af0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/helper/WidgetFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/helper/WidgetFactory.kt @@ -20,11 +20,14 @@ import org.matrix.android.sdk.api.session.content.ContentUrlResolver import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.sender.SenderInfo +import org.matrix.android.sdk.api.session.user.model.User import org.matrix.android.sdk.api.session.widgets.model.Widget 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 org.matrix.android.sdk.internal.database.RealmSessionProvider import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.session.displayname.DisplayNameResolver import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper import org.matrix.android.sdk.internal.session.user.UserDataSource import java.net.URLEncoder @@ -32,6 +35,7 @@ import javax.inject.Inject internal class WidgetFactory @Inject constructor(private val userDataSource: UserDataSource, private val realmSessionProvider: RealmSessionProvider, + private val displayNameResolver: DisplayNameResolver, private val urlResolver: ContentUrlResolver, @UserId private val userId: String) { @@ -68,13 +72,13 @@ internal class WidgetFactory @Inject constructor(private val userDataSource: Use // Ref: https://github.com/matrix-org/matrix-widget-api/blob/master/src/templating/url-template.ts#L29-L33 fun computeURL(widget: Widget, isLightTheme: Boolean): String? { var computedUrl = widget.widgetContent.url ?: return null - val myUser = userDataSource.getUser(userId) + val myUser = userDataSource.getUser(userId) ?: User(userId) val keyValue = widget.widgetContent.data.mapKeys { "\$${it.key}" }.toMutableMap() keyValue[WIDGET_PATTERN_MATRIX_USER_ID] = userId - keyValue[WIDGET_PATTERN_MATRIX_DISPLAY_NAME] = myUser?.getBestName() ?: userId - keyValue[WIDGET_PATTERN_MATRIX_AVATAR_URL] = urlResolver.resolveFullSize(myUser?.avatarUrl) ?: "" + keyValue[WIDGET_PATTERN_MATRIX_DISPLAY_NAME] = displayNameResolver.getBestName(myUser.toMatrixItem()) + keyValue[WIDGET_PATTERN_MATRIX_AVATAR_URL] = urlResolver.resolveFullSize(myUser.avatarUrl) ?: "" keyValue[WIDGET_PATTERN_MATRIX_WIDGET_ID] = widget.widgetId keyValue[WIDGET_PATTERN_MATRIX_ROOM_ID] = widget.event.roomId ?: "" keyValue[WIDGET_PATTERN_THEME] = getTheme(isLightTheme) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/token/GetScalarTokenTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/token/GetScalarTokenTask.kt index 78a40d1977..17797cad52 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/token/GetScalarTokenTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/token/GetScalarTokenTask.kt @@ -18,9 +18,9 @@ package org.matrix.android.sdk.internal.session.widgets.token import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.MatrixError +import org.matrix.android.sdk.api.session.widgets.WidgetManagementFailure import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.session.openid.GetOpenIdTokenTask -import org.matrix.android.sdk.api.session.widgets.WidgetManagementFailure import org.matrix.android.sdk.internal.session.widgets.WidgetsAPI import org.matrix.android.sdk.internal.session.widgets.WidgetsAPIProvider import org.matrix.android.sdk.internal.task.Task diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/CoroutineToCallback.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/CoroutineToCallback.kt index ca4b092e07..de6981ec03 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/CoroutineToCallback.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/CoroutineToCallback.kt @@ -16,15 +16,15 @@ package org.matrix.android.sdk.internal.task -import org.matrix.android.sdk.api.MatrixCallback -import org.matrix.android.sdk.api.util.Cancelable -import org.matrix.android.sdk.internal.extensions.foldToCallback -import org.matrix.android.sdk.internal.util.toCancelable import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.internal.extensions.foldToCallback +import org.matrix.android.sdk.internal.util.toCancelable import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/Task.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/Task.kt index a5d031e02a..01066a6f7c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/Task.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/Task.kt @@ -25,7 +25,7 @@ internal interface Task { suspend fun execute(params: PARAMS): RESULT - suspend fun executeRetry(params: PARAMS, remainingRetry: Int) : RESULT { + suspend fun executeRetry(params: PARAMS, remainingRetry: Int): RESULT { return try { execute(params) } catch (failure: Throwable) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/TaskExecutor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/TaskExecutor.kt index 4da16eff22..57574a96fa 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/TaskExecutor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/TaskExecutor.kt @@ -16,16 +16,16 @@ package org.matrix.android.sdk.internal.task -import org.matrix.android.sdk.api.util.Cancelable -import org.matrix.android.sdk.internal.di.MatrixScope -import org.matrix.android.sdk.internal.extensions.foldToCallback -import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers -import org.matrix.android.sdk.internal.util.toCancelable import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancelChildren import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers +import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.internal.di.MatrixScope +import org.matrix.android.sdk.internal.extensions.foldToCallback +import org.matrix.android.sdk.internal.util.toCancelable import timber.log.Timber import javax.inject.Inject import kotlin.coroutines.EmptyCoroutineContext diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/CancelableCoroutine.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/CancelableCoroutine.kt index 860cf66c78..f398ee25d8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/CancelableCoroutine.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/CancelableCoroutine.kt @@ -16,8 +16,8 @@ package org.matrix.android.sdk.internal.util -import org.matrix.android.sdk.api.util.Cancelable import kotlinx.coroutines.Job +import org.matrix.android.sdk.api.util.Cancelable internal fun Job.toCancelable(): Cancelable { return CancelableCoroutine(this) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/JsonCanonicalizer.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/JsonCanonicalizer.kt index 7b45bab365..a34b91a70b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/JsonCanonicalizer.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/JsonCanonicalizer.kt @@ -17,10 +17,10 @@ package org.matrix.android.sdk.internal.util import androidx.annotation.VisibleForTesting -import org.matrix.android.sdk.internal.di.MoshiProvider import org.json.JSONArray import org.json.JSONException import org.json.JSONObject +import org.matrix.android.sdk.internal.di.MoshiProvider import timber.log.Timber import java.util.TreeSet diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/LogUtil.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/LogUtil.kt index 4656856bf7..6fd907d397 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/LogUtil.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/LogUtil.kt @@ -17,6 +17,7 @@ package org.matrix.android.sdk.internal.util import org.matrix.android.sdk.BuildConfig +import org.matrix.android.sdk.api.logger.LoggerTag import timber.log.Timber internal fun Collection.logLimit(maxQuantity: Int = 5): String { @@ -32,14 +33,15 @@ internal fun Collection.logLimit(maxQuantity: Int = 5): String { } internal suspend fun logDuration(message: String, + loggerTag: LoggerTag, block: suspend () -> T): T { - Timber.d("$message -- BEGIN") + Timber.tag(loggerTag.value).d("$message -- BEGIN") val start = System.currentTimeMillis() val result = logRamUsage(message) { block() } val duration = System.currentTimeMillis() - start - Timber.d("$message -- END duration: $duration ms") + Timber.tag(loggerTag.value).d("$message -- END duration: $duration ms") return result } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Normalizer.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Normalizer.kt new file mode 100644 index 0000000000..0e9c885394 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Normalizer.kt @@ -0,0 +1,27 @@ +/* + * 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.internal.util + +import java.text.Normalizer +import javax.inject.Inject + +class Normalizer @Inject constructor() { + + fun normalize(input: String): String { + return Normalizer.normalize(input.lowercase(), Normalizer.Form.NFD) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/StringUtils.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/StringUtils.kt index aa0b92aa45..8a6ec18986 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/StringUtils.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/StringUtils.kt @@ -78,7 +78,7 @@ internal val spaceChars = "[\u00A0\u2000-\u200B\u2800\u3000]".toRegex() /** * Strip all the UTF-8 chars which are actually spaces */ -internal fun String.replaceSpaceChars() = replace(spaceChars, "") +internal fun String.replaceSpaceChars(replacement: String = "") = replace(spaceChars, replacement) // String.capitalize is now deprecated internal fun String.safeCapitalize(): String { @@ -90,3 +90,5 @@ internal fun String.safeCapitalize(): String { } } } + +internal fun String.removeInvalidRoomNameChars() = "[^a-z0-9._%#@=+-]".toRegex().replace(this, "") diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/DefaultBuildVersionSdkIntProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/DefaultBuildVersionSdkIntProvider.kt index d9f0064f38..2d5e4b944a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/DefaultBuildVersionSdkIntProvider.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/DefaultBuildVersionSdkIntProvider.kt @@ -19,7 +19,7 @@ package org.matrix.android.sdk.internal.util.system import android.os.Build import javax.inject.Inject -internal class DefaultBuildVersionSdkIntProvider @Inject constructor() - : BuildVersionSdkIntProvider { +internal class DefaultBuildVersionSdkIntProvider @Inject constructor() : + BuildVersionSdkIntProvider { override fun get() = Build.VERSION.SDK_INT } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/wellknown/GetWellknownTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/wellknown/GetWellknownTask.kt index f11e87e1e7..82ff9a321f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/wellknown/GetWellknownTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/wellknown/GetWellknownTask.kt @@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.wellknown import android.util.MalformedJsonException import dagger.Lazy +import okhttp3.OkHttpClient import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig import org.matrix.android.sdk.api.auth.data.WellKnown import org.matrix.android.sdk.api.auth.wellknown.WellknownResult @@ -31,7 +32,6 @@ import org.matrix.android.sdk.internal.session.homeserver.CapabilitiesAPI import org.matrix.android.sdk.internal.session.identity.IdentityAuthAPI import org.matrix.android.sdk.internal.task.Task import org.matrix.android.sdk.internal.util.isValidUrl -import okhttp3.OkHttpClient import java.io.EOFException import javax.inject.Inject import javax.net.ssl.HttpsURLConnection diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/AlwaysSuccessfulWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/AlwaysSuccessfulWorker.kt index 3506a76f75..856d3debcf 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/AlwaysSuccessfulWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/AlwaysSuccessfulWorker.kt @@ -19,8 +19,8 @@ import android.content.Context import androidx.work.Worker import androidx.work.WorkerParameters -internal class AlwaysSuccessfulWorker(context: Context, params: WorkerParameters) - : Worker(context, params) { +internal class AlwaysSuccessfulWorker(context: Context, params: WorkerParameters) : + Worker(context, params) { override fun doWork(): Result { return Result.success() diff --git a/matrix-sdk-android/src/release/java/org/matrix/android/sdk/internal/network/interceptors/CurlLoggingInterceptor.kt b/matrix-sdk-android/src/release/java/org/matrix/android/sdk/internal/network/interceptors/CurlLoggingInterceptor.kt index f46e2ca35b..47f6869479 100644 --- a/matrix-sdk-android/src/release/java/org/matrix/android/sdk/internal/network/interceptors/CurlLoggingInterceptor.kt +++ b/matrix-sdk-android/src/release/java/org/matrix/android/sdk/internal/network/interceptors/CurlLoggingInterceptor.kt @@ -26,8 +26,8 @@ import javax.inject.Inject * No op interceptor */ @MatrixScope -internal class CurlLoggingInterceptor @Inject constructor() - : Interceptor { +internal class CurlLoggingInterceptor @Inject constructor() : + Interceptor { @Throws(IOException::class) override fun intercept(chain: Interceptor.Chain): Response { diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/MatrixTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/MatrixTest.kt index f6a7f525db..84f09eb184 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/MatrixTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/MatrixTest.kt @@ -16,8 +16,8 @@ package org.matrix.android.sdk -import org.matrix.android.sdk.test.shared.createTimberTestRule import org.junit.Rule +import org.matrix.android.sdk.test.shared.createTimberTestRule interface MatrixTest { diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/auth/data/VersionsKtTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/auth/data/VersionsKtTest.kt index c413d9ccae..96655b849d 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/auth/data/VersionsKtTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/auth/data/VersionsKtTest.kt @@ -16,10 +16,10 @@ package org.matrix.android.sdk.api.auth.data -import org.matrix.android.sdk.internal.auth.version.Versions -import org.matrix.android.sdk.internal.auth.version.isSupportedBySdk import org.amshove.kluent.shouldBe import org.junit.Test +import org.matrix.android.sdk.internal.auth.version.Versions +import org.matrix.android.sdk.internal.auth.version.isSupportedBySdk class VersionsKtTest { diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/pushrules/PushRuleActionsTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/pushrules/PushRuleActionsTest.kt index b734444990..9bfdea5414 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/pushrules/PushRuleActionsTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/pushrules/PushRuleActionsTest.kt @@ -16,15 +16,15 @@ package org.matrix.android.sdk.api.pushrules -import org.matrix.android.sdk.MatrixTest -import org.matrix.android.sdk.api.pushrules.rest.PushRule -import org.matrix.android.sdk.internal.di.MoshiProvider import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull import org.junit.Assert.assertTrue import org.junit.Test +import org.matrix.android.sdk.MatrixTest +import org.matrix.android.sdk.api.pushrules.rest.PushRule +import org.matrix.android.sdk.internal.di.MoshiProvider -class PushRuleActionsTest: MatrixTest { +class PushRuleActionsTest : MatrixTest { @Test fun test_action_parsing() { diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/keysbackup/util/Base58Test.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/keysbackup/util/Base58Test.kt index 2f01a43a67..a93883a344 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/keysbackup/util/Base58Test.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/keysbackup/util/Base58Test.kt @@ -16,15 +16,15 @@ package org.matrix.android.sdk.internal.crypto.keysbackup.util -import org.matrix.android.sdk.MatrixTest import org.junit.Assert.assertArrayEquals import org.junit.Assert.assertEquals import org.junit.FixMethodOrder import org.junit.Test import org.junit.runners.MethodSorters +import org.matrix.android.sdk.MatrixTest @FixMethodOrder(MethodSorters.JVM) -class Base58Test: MatrixTest { +class Base58Test : MatrixTest { @Test fun encode() { diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/keysbackup/util/RecoveryKeyTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/keysbackup/util/RecoveryKeyTest.kt index 64ffe52acd..4e4548b197 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/keysbackup/util/RecoveryKeyTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/keysbackup/util/RecoveryKeyTest.kt @@ -16,14 +16,14 @@ package org.matrix.android.sdk.internal.crypto.keysbackup.util -import org.matrix.android.sdk.MatrixTest import org.junit.Assert.assertArrayEquals import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Test +import org.matrix.android.sdk.MatrixTest -class RecoveryKeyTest: MatrixTest { +class RecoveryKeyTest : MatrixTest { private val curve25519Key = byteArrayOf( 0x77.toByte(), 0x07.toByte(), 0x6D.toByte(), 0x0A.toByte(), 0x73.toByte(), 0x18.toByte(), 0xA5.toByte(), 0x7D.toByte(), diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/store/db/HelperTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/store/db/HelperTest.kt index 0bcc7983c5..b50d0581b0 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/store/db/HelperTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/store/db/HelperTest.kt @@ -16,12 +16,12 @@ package org.matrix.android.sdk.internal.crypto.store.db -import org.matrix.android.sdk.MatrixTest -import org.matrix.android.sdk.internal.util.md5 import org.junit.Assert.assertEquals import org.junit.Test +import org.matrix.android.sdk.MatrixTest +import org.matrix.android.sdk.internal.util.md5 -class HelperTest: MatrixTest { +class HelperTest : MatrixTest { @Test fun testHash_ok() { diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/BinaryStringTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/BinaryStringTest.kt index b04834f9f4..5a82052d1a 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/BinaryStringTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/BinaryStringTest.kt @@ -16,11 +16,11 @@ package org.matrix.android.sdk.internal.crypto.verification.qrcode -import org.matrix.android.sdk.MatrixTest import org.amshove.kluent.shouldBeEqualTo import org.junit.FixMethodOrder import org.junit.Test import org.junit.runners.MethodSorters +import org.matrix.android.sdk.MatrixTest @FixMethodOrder(MethodSorters.JVM) class BinaryStringTest : MatrixTest { diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigrationTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigrationTest.kt new file mode 100644 index 0000000000..3a0574e95a --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigrationTest.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 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.internal.database + +import io.mockk.mockk +import org.amshove.kluent.shouldBeEqualTo +import org.junit.Test + +class RealmSessionStoreMigrationTest { + + @Test + fun `when creating multiple migration instances then they are equal`() { + RealmSessionStoreMigration(normalizer = mockk()) shouldBeEqualTo RealmSessionStoreMigration(normalizer = mockk()) + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/pushers/DefaultAddPusherTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/pushers/DefaultAddPusherTaskTest.kt new file mode 100644 index 0000000000..c8be0f5487 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/pushers/DefaultAddPusherTaskTest.kt @@ -0,0 +1,103 @@ +/* + * Copyright (c) 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.internal.session.pushers + +import kotlinx.coroutines.runBlocking +import org.amshove.kluent.internal.assertFailsWith +import org.amshove.kluent.shouldBeEqualTo +import org.junit.Test +import org.matrix.android.sdk.api.session.pushers.PusherState +import org.matrix.android.sdk.internal.database.model.PusherEntity +import org.matrix.android.sdk.test.fakes.FakeGlobalErrorReceiver +import org.matrix.android.sdk.test.fakes.FakeMonarchy +import org.matrix.android.sdk.test.fakes.FakePushersAPI +import org.matrix.android.sdk.test.fakes.FakeRequestExecutor +import java.net.SocketException + +private val A_JSON_PUSHER = JsonPusher( + pushKey = "push-key", + kind = "http", + appId = "m.email", + appDisplayName = "Element", + deviceDisplayName = null, + profileTag = "", + lang = "en-GB", + data = JsonPusherData(brand = "Element") +) + +class DefaultAddPusherTaskTest { + + private val pushersAPI = FakePushersAPI() + private val monarchy = FakeMonarchy() + + private val addPusherTask = DefaultAddPusherTask( + pushersAPI = pushersAPI, + monarchy = monarchy.instance, + requestExecutor = FakeRequestExecutor(), + globalErrorReceiver = FakeGlobalErrorReceiver() + ) + + @Test + fun `given no persisted pusher when adding Pusher then updates api and inserts result with Registered state`() { + monarchy.givenWhereReturns(result = null) + + runBlocking { addPusherTask.execute(AddPusherTask.Params(A_JSON_PUSHER)) } + + pushersAPI.verifySetPusher(A_JSON_PUSHER) + monarchy.verifyInsertOrUpdate { + withArg { actual -> + actual.state shouldBeEqualTo PusherState.REGISTERED + } + } + } + + @Test + fun `given a persisted pusher when adding Pusher then updates api and mutates persisted result with Registered state`() { + val realmResult = PusherEntity(appDisplayName = null) + monarchy.givenWhereReturns(result = realmResult) + + runBlocking { addPusherTask.execute(AddPusherTask.Params(A_JSON_PUSHER)) } + + pushersAPI.verifySetPusher(A_JSON_PUSHER) + + realmResult.appDisplayName shouldBeEqualTo A_JSON_PUSHER.appDisplayName + realmResult.state shouldBeEqualTo PusherState.REGISTERED + } + + @Test + fun `given a persisted push entity and SetPush API fails when adding Pusher then mutates persisted result with Failed registration state and rethrows error`() { + val realmResult = PusherEntity() + monarchy.givenWhereReturns(result = realmResult) + pushersAPI.givenSetPusherErrors(SocketException()) + + assertFailsWith { + runBlocking { addPusherTask.execute(AddPusherTask.Params(A_JSON_PUSHER)) } + } + + realmResult.state shouldBeEqualTo PusherState.FAILED_TO_REGISTER + } + + @Test + fun `given no persisted push entity and SetPush API fails when adding Pusher then rethrows error`() { + monarchy.givenWhereReturns(result = null) + pushersAPI.givenSetPusherErrors(SocketException()) + + assertFailsWith { + runBlocking { addPusherTask.execute(AddPusherTask.Params(A_JSON_PUSHER)) } + } + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/task/CoroutineSequencersTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/task/CoroutineSequencersTest.kt index 3572a1a546..0abca8bee3 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/task/CoroutineSequencersTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/task/CoroutineSequencersTest.kt @@ -16,7 +16,6 @@ package org.matrix.android.sdk.internal.task -import org.matrix.android.sdk.MatrixTest import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.delay @@ -25,9 +24,10 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals import org.junit.Test +import org.matrix.android.sdk.MatrixTest import java.util.concurrent.Executors -class CoroutineSequencersTest: MatrixTest { +class CoroutineSequencersTest : MatrixTest { private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeGlobalErrorReceiver.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeGlobalErrorReceiver.kt new file mode 100644 index 0000000000..ebddb3fafa --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeGlobalErrorReceiver.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 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.test.fakes + +import org.matrix.android.sdk.api.failure.GlobalError +import org.matrix.android.sdk.internal.network.GlobalErrorReceiver + +internal class FakeGlobalErrorReceiver : GlobalErrorReceiver { + override fun handleGlobalError(globalError: GlobalError) { + // do nothing + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeMonarchy.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeMonarchy.kt new file mode 100644 index 0000000000..0a22ef8996 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeMonarchy.kt @@ -0,0 +1,55 @@ +/* + * Copyright (c) 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.test.fakes + +import com.zhuinden.monarchy.Monarchy +import io.mockk.MockKVerificationScope +import io.mockk.coEvery +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.verify +import io.realm.Realm +import io.realm.RealmModel +import io.realm.RealmQuery +import io.realm.kotlin.where +import org.matrix.android.sdk.internal.util.awaitTransaction + +internal class FakeMonarchy { + + val instance = mockk() + private val realm = mockk(relaxed = true) + + init { + mockkStatic("org.matrix.android.sdk.internal.util.MonarchyKt") + coEvery { + instance.awaitTransaction(any Any>()) + } coAnswers { + secondArg Any>().invoke(realm) + } + } + + inline fun givenWhereReturns(result: T?) { + val queryResult = mockk>(relaxed = true) + every { queryResult.findFirst() } returns result + every { realm.where() } returns queryResult + } + + inline fun verifyInsertOrUpdate(crossinline verification: MockKVerificationScope.() -> T) { + verify { realm.insertOrUpdate(verification()) } + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakePushersAPI.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakePushersAPI.kt new file mode 100644 index 0000000000..29f93c2faf --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakePushersAPI.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 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.test.fakes + +import org.amshove.kluent.shouldBeEqualTo +import org.matrix.android.sdk.internal.session.pushers.GetPushersResponse +import org.matrix.android.sdk.internal.session.pushers.JsonPusher +import org.matrix.android.sdk.internal.session.pushers.PushersAPI + +internal class FakePushersAPI : PushersAPI { + + private var setRequestPayload: JsonPusher? = null + private var error: Throwable? = null + + override suspend fun getPushers(): GetPushersResponse { + TODO("Not yet implemented") + } + + override suspend fun setPusher(jsonPusher: JsonPusher) { + error?.let { throw it } + setRequestPayload = jsonPusher + } + + fun verifySetPusher(payload: JsonPusher) { + this.setRequestPayload shouldBeEqualTo payload + } + + fun givenSetPusherErrors(error: Throwable) { + this.error = error + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRequestExecutor.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRequestExecutor.kt new file mode 100644 index 0000000000..2f332a89a8 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRequestExecutor.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 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.test.fakes + +import org.matrix.android.sdk.internal.network.GlobalErrorReceiver +import org.matrix.android.sdk.internal.network.RequestExecutor + +internal class FakeRequestExecutor : RequestExecutor { + + override suspend fun executeRequest(globalErrorReceiver: GlobalErrorReceiver?, + canRetry: Boolean, + maxDelayBeforeRetry: Long, + maxRetriesCount: Int, + requestBlock: suspend () -> DATA): DATA { + return requestBlock() + } +} diff --git a/multipicker/build.gradle b/multipicker/build.gradle index 437499df7b..bb98a2f852 100644 --- a/multipicker/build.gradle +++ b/multipicker/build.gradle @@ -38,7 +38,6 @@ android { } dependencies { - implementation libs.jetbrains.kotlinStdlibJdk7 implementation libs.androidx.appCompat implementation libs.androidx.fragmentKtx implementation libs.androidx.exifinterface diff --git a/settings.gradle b/settings.gradle index b88ea99b05..e3b84b4733 100644 --- a/settings.gradle +++ b/settings.gradle @@ -5,3 +5,4 @@ include ':diff-match-patch' include ':attachment-viewer' include ':multipicker' include ':library:ui-styles' +include ':matrix-sdk-android-flow' diff --git a/tools/benchmark/benchmark.profile b/tools/benchmark/benchmark.profile new file mode 100644 index 0000000000..ae27dc9f59 --- /dev/null +++ b/tools/benchmark/benchmark.profile @@ -0,0 +1,39 @@ +# +# Copyright 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. +# + +clean_assemble { + tasks = ["clean", ":vector:assembleGPlayDebug"] +} + +clean_assemble_build_cache { + tasks = ["clean", ":vector:assembleGPlayDebug"] + gradle-args = ["--build-cache"] +} + +clean_assemble_without_cache { + tasks = ["clean", ":vector:assembleGPlayDebug"] + gradle-args = ["--no-build-cache"] +} + +incremental_assemble_sdk_abi { + tasks = [":vector:assembleGPlayDebug"] + apply-abi-change-to = "matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt" +} + +incremental_assemble_sdk_no_abi { + tasks = [":vector:assembleGPlayDebug"] + apply-non-abi-change-to = "matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt" +} diff --git a/tools/benchmark/run_benchmark.sh b/tools/benchmark/run_benchmark.sh new file mode 100755 index 0000000000..b6c81ee513 --- /dev/null +++ b/tools/benchmark/run_benchmark.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash + +# +# Copyright 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. +# + +if ! command -v gradle-profiler &> /dev/null +then + echo "gradle-profiler could not be found https://github.com/gradle/gradle-profiler" + exit +fi + +gradle-profiler \ + --benchmark \ + --project-dir . \ + --scenario-file tools/benchmark/benchmark.profile \ + --output-dir benchmark-out/output \ + --gradle-user-home benchmark-out/gradle-home \ + --warmups 3 \ + --iterations 3 \ + $1 diff --git a/tools/check/forbidden_strings_in_code.txt b/tools/check/forbidden_strings_in_code.txt index 4b0dd1f0a3..29077c3a76 100644 --- a/tools/check/forbidden_strings_in_code.txt +++ b/tools/check/forbidden_strings_in_code.txt @@ -141,8 +141,6 @@ android\.app\.AlertDialog androidx\.appcompat\.app\.AlertDialog===4 ### Put the operator at the beginning of next line -&&$ -\|\|$ ==$ ### Use JsonUtils.getBasicGson() @@ -162,7 +160,7 @@ Formatter\.formatShortFileSize===1 # android\.text\.TextUtils ### This is not a rule, but a warning: the number of "enum class" has changed. For Json classes, it is mandatory that they have `@JsonClass(generateAdapter = false)`. If the enum is not used as a Json class, change the value in file forbidden_strings_in_code.txt -enum class===106 +enum class===107 ### Do not import temporary legacy classes import org.matrix.android.sdk.internal.legacy.riot===3 diff --git a/tools/emojis/emoji_picker_datasource_formatted.json b/tools/emojis/emoji_picker_datasource_formatted.json index 33fc9be128..4f3038d53c 100644 --- a/tools/emojis/emoji_picker_datasource_formatted.json +++ b/tools/emojis/emoji_picker_datasource_formatted.json @@ -15,6 +15,7 @@ "face-with-tears-of-joy", "slightly-smiling-face", "upsidedown-face", + "melting-face", "winking-face", "smiling-face-with-smiling-eyes", "smiling-face-with-halo", @@ -33,15 +34,19 @@ "zany-face", "squinting-face-with-tongue", "moneymouth-face", - "hugging-face", + "smiling-face-with-open-hands", "face-with-hand-over-mouth", + "face-with-open-eyes-and-hand-over-mouth", + "face-with-peeking-eye", "shushing-face", "thinking-face", + "saluting-face", "zippermouth-face", "face-with-raised-eyebrow", "neutral-face", "expressionless-face", "face-without-mouth", + "dotted-line-face", "face-in-clouds", "smirking-face", "unamused-face", @@ -63,7 +68,7 @@ "hot-face", "cold-face", "woozy-face", - "knockedout-face", + "face-with-crossedout-eyes", "face-with-spiral-eyes", "exploding-head", "cowboy-hat-face", @@ -73,6 +78,7 @@ "nerd-face", "face-with-monocle", "confused-face", + "face-with-diagonal-mouth", "worried-face", "slightly-frowning-face", "frowning-face", @@ -81,6 +87,7 @@ "astonished-face", "flushed-face", "pleading-face", + "face-holding-back-tears", "frowning-face-with-open-mouth", "anguished-face", "fearful-face", @@ -172,11 +179,16 @@ "hand-with-fingers-splayed", "raised-hand", "vulcan-salute", + "rightwards-hand", + "leftwards-hand", + "palm-down-hand", + "palm-up-hand", "ok-hand", "pinched-fingers", "pinching-hand", "victory-hand", "crossed-fingers", + "hand-with-index-finger-and-thumb-crossed", "loveyou-gesture", "sign-of-the-horns", "call-me-hand", @@ -186,6 +198,7 @@ "middle-finger", "backhand-index-pointing-down", "index-pointing-up", + "index-pointing-at-the-viewer", "thumbs-up", "thumbs-down", "raised-fist", @@ -194,6 +207,7 @@ "rightfacing-fist", "clapping-hands", "raising-hands", + "heart-hands", "open-hands", "palms-up-together", "handshake", @@ -218,6 +232,7 @@ "eye", "tongue", "mouth", + "biting-lip", "baby", "child", "boy", @@ -337,6 +352,7 @@ "construction-worker", "man-construction-worker", "woman-construction-worker", + "person-with-crown", "prince", "princess", "person-wearing-turban", @@ -351,6 +367,8 @@ "man-with-veil", "woman-with-veil", "pregnant-woman", + "pregnant-man", + "pregnant-person", "breastfeeding", "woman-feeding-baby", "man-feeding-baby", @@ -386,6 +404,7 @@ "zombie", "man-zombie", "woman-zombie", + "troll", "person-getting-massage", "man-getting-massage", "woman-getting-massage", @@ -623,6 +642,7 @@ "shark", "octopus", "spiral-shell", + "coral", "snail", "butterfly", "bug", @@ -642,6 +662,7 @@ "bouquet", "cherry-blossom", "white-flower", + "lotus", "rosette", "rose", "wilted-flower", @@ -661,7 +682,9 @@ "four-leaf-clover", "maple-leaf", "fallen-leaf", - "leaf-fluttering-in-wind" + "leaf-fluttering-in-wind", + "empty-nest", + "nest-with-eggs" ] }, { @@ -701,6 +724,7 @@ "onion", "mushroom", "peanuts", + "beans", "chestnut", "bread", "croissant", @@ -786,6 +810,7 @@ "clinking-beer-mugs", "clinking-glasses", "tumbler-glass", + "pouring-liquid", "cup-with-straw", "bubble-tea", "beverage-box", @@ -796,6 +821,7 @@ "fork-and-knife", "spoon", "kitchen-knife", + "jar", "amphora" ] }, @@ -864,6 +890,7 @@ "bridge-at-night", "hot-springs", "carousel-horse", + "playground-slide", "ferris-wheel", "roller-coaster", "barber-pole", @@ -912,12 +939,14 @@ "railway-track", "oil-drum", "fuel-pump", + "wheel", "police-car-light", "horizontal-traffic-light", "vertical-traffic-light", "stop-sign", "construction", "anchor", + "ring-buoy", "sailboat", "canoe", "speedboat", @@ -1085,6 +1114,7 @@ "crystal-ball", "magic-wand", "nazar-amulet", + "hamsa", "video-game", "joystick", "slot-machine", @@ -1092,6 +1122,7 @@ "puzzle-piece", "teddy-bear", "piata", + "mirror-ball", "nesting-dolls", "spade-suit", "heart-suit", @@ -1193,6 +1224,7 @@ "pager", "fax-machine", "battery", + "low-battery", "electric-plug", "laptop", "desktop-computer", @@ -1333,7 +1365,9 @@ "drop-of-blood", "pill", "adhesive-bandage", + "crutch", "stethoscope", + "xray", "door", "elevator", "mirror", @@ -1354,6 +1388,7 @@ "roll-of-paper", "bucket", "soap", + "bubbles", "toothbrush", "sponge", "fire-extinguisher", @@ -1363,7 +1398,8 @@ "headstone", "funeral-urn", "moai", - "placard" + "placard", + "identification-card" ] }, { @@ -1473,6 +1509,7 @@ "plus", "minus", "divide", + "heavy-equals-sign", "infinity", "double-exclamation-mark", "exclamation-question-mark", @@ -2012,6 +2049,16 @@ "smile" ] }, + "melting-face": { + "a": "⊛ Melting Face", + "b": "1FAE0", + "j": [ + "disappear", + "dissolve", + "liquid", + "melt" + ] + }, "winking-face": { "a": "Winking Face", "b": "1F609", @@ -2271,13 +2318,16 @@ "dollar" ] }, - "hugging-face": { - "a": "Hugging Face", + "smiling-face-with-open-hands": { + "a": "Smiling Face with Open Hands", "b": "1F917", "j": [ "face", "hug", "hugging", + "open hands", + "smiling face", + "hugging_face", "smile" ] }, @@ -2292,6 +2342,27 @@ "face" ] }, + "face-with-open-eyes-and-hand-over-mouth": { + "a": "⊛ Face with Open Eyes and Hand over Mouth", + "b": "1FAE2", + "j": [ + "amazement", + "awe", + "disbelief", + "embarrass", + "scared", + "surprise" + ] + }, + "face-with-peeking-eye": { + "a": "⊛ Face with Peeking Eye", + "b": "1FAE3", + "j": [ + "captivated", + "peep", + "stare" + ] + }, "shushing-face": { "a": "Shushing Face", "b": "1F92B", @@ -2313,6 +2384,17 @@ "consider" ] }, + "saluting-face": { + "a": "⊛ Saluting Face", + "b": "1FAE1", + "j": [ + "ok", + "salute", + "sunny", + "troops", + "yes" + ] + }, "zippermouth-face": { "a": "Zipper-Mouth Face", "b": "1F910", @@ -2377,8 +2459,19 @@ "hellokitty" ] }, + "dotted-line-face": { + "a": "⊛ Dotted Line Face", + "b": "1FAE5", + "j": [ + "depressed", + "disappear", + "hide", + "introvert", + "invisible" + ] + }, "face-in-clouds": { - "a": "⊛ Face in Clouds", + "a": "Face in Clouds", "b": "1F636-200D-1F32B-FE0F", "j": [ "absentminded", @@ -2439,7 +2532,7 @@ ] }, "face-exhaling": { - "a": "⊛ Face Exhaling", + "a": "Face Exhaling", "b": "1F62E-200D-1F4A8", "j": [ "exhale", @@ -2631,14 +2724,15 @@ "wavy" ] }, - "knockedout-face": { - "a": "Knocked-out Face", + "face-with-crossedout-eyes": { + "a": "Face with Crossed-out Eyes", "b": "1F635", "j": [ + "crossed-out eyes", "dead", "face", + "face with crossed-out eyes", "knocked out", - "knocked-out face", "dizzy_face", "spent", "unconscious", @@ -2647,7 +2741,7 @@ ] }, "face-with-spiral-eyes": { - "a": "⊛ Face with Spiral Eyes", + "a": "Face with Spiral Eyes", "b": "1F635-200D-1F4AB", "j": [ "dizzy", @@ -2754,6 +2848,16 @@ ":/" ] }, + "face-with-diagonal-mouth": { + "a": "⊛ Face with Diagonal Mouth", + "b": "1FAE4", + "j": [ + "disappointed", + "meh", + "skeptical", + "unsure" + ] + }, "worried-face": { "a": "Worried Face", "b": "1F61F", @@ -2849,6 +2953,17 @@ "face" ] }, + "face-holding-back-tears": { + "a": "⊛ Face Holding Back Tears", + "b": "1F979", + "j": [ + "angry", + "cry", + "proud", + "resist", + "sad" + ] + }, "frowning-face-with-open-mouth": { "a": "Frowning Face with Open Mouth", "b": "1F626", @@ -3584,7 +3699,7 @@ ] }, "heart-on-fire": { - "a": "⊛ Heart on Fire", + "a": "Heart on Fire", "b": "2764-FE0F-200D-1F525", "j": [ "burn", @@ -3596,7 +3711,7 @@ ] }, "mending-heart": { - "a": "⊛ Mending Heart", + "a": "Mending Heart", "b": "2764-FE0F-200D-1FA79", "j": [ "healthier", @@ -3933,6 +4048,43 @@ "star trek" ] }, + "rightwards-hand": { + "a": "⊛ Rightwards Hand", + "b": "1FAF1", + "j": [ + "hand", + "right", + "rightward" + ] + }, + "leftwards-hand": { + "a": "⊛ Leftwards Hand", + "b": "1FAF2", + "j": [ + "hand", + "left", + "leftward" + ] + }, + "palm-down-hand": { + "a": "⊛ Palm Down Hand", + "b": "1FAF3", + "j": [ + "dismiss", + "drop", + "shoo" + ] + }, + "palm-up-hand": { + "a": "⊛ Palm Up Hand", + "b": "1FAF4", + "j": [ + "beckon", + "catch", + "come", + "offer" + ] + }, "ok-hand": { "a": "Ok Hand", "b": "1F44C", @@ -3995,6 +4147,17 @@ "lucky" ] }, + "hand-with-index-finger-and-thumb-crossed": { + "a": "⊛ Hand with Index Finger and Thumb Crossed", + "b": "1FAF0", + "j": [ + "expensive", + "heart", + "love", + "money", + "snap" + ] + }, "loveyou-gesture": { "a": "Love-You Gesture", "b": "1F91F", @@ -4110,6 +4273,14 @@ "direction" ] }, + "index-pointing-at-the-viewer": { + "a": "⊛ Index Pointing at the Viewer", + "b": "1FAF5", + "j": [ + "point", + "you" + ] + }, "thumbs-up": { "a": "Thumbs Up", "b": "1F44D", @@ -4217,6 +4388,13 @@ "hands" ] }, + "heart-hands": { + "a": "⊛ Heart Hands", + "b": "1FAF6", + "j": [ + "love" + ] + }, "open-hands": { "a": "Open Hands", "b": "1F450", @@ -4463,6 +4641,18 @@ "kiss" ] }, + "biting-lip": { + "a": "⊛ Biting Lip", + "b": "1FAE6", + "j": [ + "anxious", + "fear", + "flirting", + "nervous", + "uncomfortable", + "worried" + ] + }, "baby": { "a": "Baby", "b": "1F476", @@ -4552,7 +4742,7 @@ ] }, "man-beard": { - "a": "⊛ Man: Beard", + "a": "Man: Beard", "b": "1F9D4-200D-2642-FE0F", "j": [ "beard", @@ -4561,7 +4751,7 @@ ] }, "woman-beard": { - "a": "⊛ Woman: Beard", + "a": "Woman: Beard", "b": "1F9D4-200D-2640-FE0F", "j": [ "beard", @@ -5847,6 +6037,16 @@ "labor" ] }, + "person-with-crown": { + "a": "⊛ Person with Crown", + "b": "1FAC5", + "j": [ + "monarch", + "noble", + "regal", + "royalty" + ] + }, "prince": { "a": "Prince", "b": "1F934", @@ -6010,6 +6210,26 @@ "baby" ] }, + "pregnant-man": { + "a": "⊛ Pregnant Man", + "b": "1FAC3", + "j": [ + "belly", + "bloated", + "full", + "pregnant" + ] + }, + "pregnant-person": { + "a": "⊛ Pregnant Person", + "b": "1FAC4", + "j": [ + "belly", + "bloated", + "full", + "pregnant" + ] + }, "breastfeeding": { "a": "Breast-Feeding", "b": "1F931", @@ -6395,6 +6615,15 @@ "female" ] }, + "troll": { + "a": "⊛ Troll", + "b": "1F9CC", + "j": [ + "fairy tale", + "fantasy", + "monster" + ] + }, "person-getting-massage": { "a": "Person Getting Massage", "b": "1F486", @@ -8625,7 +8854,8 @@ "a": "Koala", "b": "1F428", "j": [ - "bear", + "face", + "marsupial", "animal", "nature" ] @@ -9125,6 +9355,14 @@ "beach" ] }, + "coral": { + "a": "⊛ Coral", + "b": "1FAB8", + "j": [ + "ocean", + "reef" + ] + }, "snail": { "a": "Snail", "b": "1F40C", @@ -9326,6 +9564,18 @@ "spring" ] }, + "lotus": { + "a": "⊛ Lotus", + "b": "1FAB7", + "j": [ + "Buddhism", + "flower", + "Hinduism", + "India", + "purity", + "Vietnam" + ] + }, "rosette": { "a": "Rosette", "b": "1F3F5", @@ -9564,6 +9814,20 @@ "spring" ] }, + "empty-nest": { + "a": "⊛ Empty Nest", + "b": "1FAB9", + "j": [ + "nesting" + ] + }, + "nest-with-eggs": { + "a": "⊛ Nest with Eggs", + "b": "1FABA", + "j": [ + "nesting" + ] + }, "grapes": { "a": "Grapes", "b": "1F347", @@ -9894,6 +10158,15 @@ "vegetable" ] }, + "beans": { + "a": "⊛ Beans", + "b": "1FAD8", + "j": [ + "food", + "kidney", + "legume" + ] + }, "chestnut": { "a": "Chestnut", "b": "1F330", @@ -10893,6 +11166,16 @@ "scotch" ] }, + "pouring-liquid": { + "a": "⊛ Pouring Liquid", + "b": "1FAD7", + "j": [ + "drink", + "empty", + "glass", + "spill" + ] + }, "cup-with-straw": { "a": "Cup with Straw", "b": "1F964", @@ -11010,6 +11293,17 @@ "kitchen" ] }, + "jar": { + "a": "⊛ Jar", + "b": "1FAD9", + "j": [ + "condiment", + "container", + "empty", + "sauce", + "store" + ] + }, "amphora": { "a": "Amphora", "b": "1F3FA", @@ -11690,6 +11984,14 @@ "carnival" ] }, + "playground-slide": { + "a": "⊛ Playground Slide", + "b": "1F6DD", + "j": [ + "amusement park", + "play" + ] + }, "ferris-wheel": { "a": "Ferris Wheel", "b": "1F3A1", @@ -12164,7 +12466,6 @@ "b": "1F68F", "j": [ "bus", - "busstop", "stop", "transportation", "wait" @@ -12212,6 +12513,15 @@ "petroleum" ] }, + "wheel": { + "a": "⊛ Wheel", + "b": "1F6DE", + "j": [ + "circle", + "tire", + "turn" + ] + }, "police-car-light": { "a": "Police Car Light", "b": "1F6A8", @@ -12283,6 +12593,17 @@ "boat" ] }, + "ring-buoy": { + "a": "⊛ Ring Buoy", + "b": "1F6DF", + "j": [ + "float", + "life preserver", + "life saver", + "rescue", + "safety" + ] + }, "sailboat": { "a": "Sailboat", "b": "26F5", @@ -14322,6 +14643,18 @@ "talisman" ] }, + "hamsa": { + "a": "⊛ Hamsa", + "b": "1FAAC", + "j": [ + "amulet", + "Fatima", + "hand", + "Mary", + "Miriam", + "protection" + ] + }, "video-game": { "a": "Video Game", "b": "1F3AE", @@ -14402,6 +14735,16 @@ "candy" ] }, + "mirror-ball": { + "a": "⊛ Mirror Ball", + "b": "1FAA9", + "j": [ + "dance", + "disco", + "glitter", + "party" + ] + }, "nesting-dolls": { "a": "Nesting Dolls", "b": "1FA86", @@ -15502,6 +15845,14 @@ "sustain" ] }, + "low-battery": { + "a": "⊛ Low Battery", + "b": "1FAAB", + "j": [ + "electronic", + "low energy" + ] + }, "electric-plug": { "a": "Electric Plug", "b": "1F50C", @@ -17135,6 +17486,17 @@ "heal" ] }, + "crutch": { + "a": "⊛ Crutch", + "b": "1FA7C", + "j": [ + "cane", + "disability", + "hurt", + "mobility aid", + "stick" + ] + }, "stethoscope": { "a": "Stethoscope", "b": "1FA7A", @@ -17145,6 +17507,16 @@ "health" ] }, + "xray": { + "a": "⊛ X-Ray", + "b": "1FA7B", + "j": [ + "bones", + "doctor", + "medical", + "skeleton" + ] + }, "door": { "a": "Door", "b": "1F6AA", @@ -17340,6 +17712,16 @@ "soapdish" ] }, + "bubbles": { + "a": "⊛ Bubbles", + "b": "1FAE7", + "j": [ + "burp", + "clean", + "soap", + "underwater" + ] + }, "toothbrush": { "a": "Toothbrush", "b": "1FAA5", @@ -17453,6 +17835,16 @@ "announcement" ] }, + "identification-card": { + "a": "⊛ Identification Card", + "b": "1FAAA", + "j": [ + "credentials", + "ID", + "license", + "security" + ] + }, "atm-sign": { "a": "Atm Sign", "b": "1F3E7", @@ -18715,6 +19107,14 @@ "calculation" ] }, + "heavy-equals-sign": { + "a": "⊛ Heavy Equals Sign", + "b": "1F7F0", + "j": [ + "equality", + "math" + ] + }, "infinity": { "a": "Infinity", "b": "267E", @@ -20247,7 +20647,8 @@ "ad", "nation", "country", - "banner" + "banner", + "andorra" ] }, "flag-united-arab-emirates": { @@ -20260,7 +20661,8 @@ "emirates", "nation", "country", - "banner" + "banner", + "united_arab_emirates" ] }, "flag-afghanistan": { @@ -20271,7 +20673,8 @@ "af", "nation", "country", - "banner" + "banner", + "afghanistan" ] }, "flag-antigua--barbuda": { @@ -20284,7 +20687,8 @@ "barbuda", "nation", "country", - "banner" + "banner", + "antigua_barbuda" ] }, "flag-anguilla": { @@ -20295,7 +20699,8 @@ "ai", "nation", "country", - "banner" + "banner", + "anguilla" ] }, "flag-albania": { @@ -20306,7 +20711,8 @@ "al", "nation", "country", - "banner" + "banner", + "albania" ] }, "flag-armenia": { @@ -20317,7 +20723,8 @@ "am", "nation", "country", - "banner" + "banner", + "armenia" ] }, "flag-angola": { @@ -20328,7 +20735,8 @@ "ao", "nation", "country", - "banner" + "banner", + "angola" ] }, "flag-antarctica": { @@ -20339,7 +20747,8 @@ "aq", "nation", "country", - "banner" + "banner", + "antarctica" ] }, "flag-argentina": { @@ -20350,7 +20759,8 @@ "ar", "nation", "country", - "banner" + "banner", + "argentina" ] }, "flag-american-samoa": { @@ -20362,7 +20772,8 @@ "ws", "nation", "country", - "banner" + "banner", + "american_samoa" ] }, "flag-austria": { @@ -20373,7 +20784,8 @@ "at", "nation", "country", - "banner" + "banner", + "austria" ] }, "flag-australia": { @@ -20384,7 +20796,8 @@ "au", "nation", "country", - "banner" + "banner", + "australia" ] }, "flag-aruba": { @@ -20395,7 +20808,8 @@ "aw", "nation", "country", - "banner" + "banner", + "aruba" ] }, "flag-land-islands": { @@ -20408,7 +20822,8 @@ "islands", "nation", "country", - "banner" + "banner", + "aland_islands" ] }, "flag-azerbaijan": { @@ -20419,7 +20834,8 @@ "az", "nation", "country", - "banner" + "banner", + "azerbaijan" ] }, "flag-bosnia--herzegovina": { @@ -20432,7 +20848,8 @@ "herzegovina", "nation", "country", - "banner" + "banner", + "bosnia_herzegovina" ] }, "flag-barbados": { @@ -20443,7 +20860,8 @@ "bb", "nation", "country", - "banner" + "banner", + "barbados" ] }, "flag-bangladesh": { @@ -20454,7 +20872,8 @@ "bd", "nation", "country", - "banner" + "banner", + "bangladesh" ] }, "flag-belgium": { @@ -20465,7 +20884,8 @@ "be", "nation", "country", - "banner" + "banner", + "belgium" ] }, "flag-burkina-faso": { @@ -20477,7 +20897,8 @@ "faso", "nation", "country", - "banner" + "banner", + "burkina_faso" ] }, "flag-bulgaria": { @@ -20488,7 +20909,8 @@ "bg", "nation", "country", - "banner" + "banner", + "bulgaria" ] }, "flag-bahrain": { @@ -20499,7 +20921,8 @@ "bh", "nation", "country", - "banner" + "banner", + "bahrain" ] }, "flag-burundi": { @@ -20510,7 +20933,8 @@ "bi", "nation", "country", - "banner" + "banner", + "burundi" ] }, "flag-benin": { @@ -20521,7 +20945,8 @@ "bj", "nation", "country", - "banner" + "banner", + "benin" ] }, "flag-st-barthlemy": { @@ -20534,7 +20959,8 @@ "barthélemy", "nation", "country", - "banner" + "banner", + "st_barthelemy" ] }, "flag-bermuda": { @@ -20545,7 +20971,8 @@ "bm", "nation", "country", - "banner" + "banner", + "bermuda" ] }, "flag-brunei": { @@ -20557,7 +20984,8 @@ "darussalam", "nation", "country", - "banner" + "banner", + "brunei" ] }, "flag-bolivia": { @@ -20568,7 +20996,8 @@ "bo", "nation", "country", - "banner" + "banner", + "bolivia" ] }, "flag-caribbean-netherlands": { @@ -20579,7 +21008,8 @@ "bonaire", "nation", "country", - "banner" + "banner", + "caribbean_netherlands" ] }, "flag-brazil": { @@ -20590,7 +21020,8 @@ "br", "nation", "country", - "banner" + "banner", + "brazil" ] }, "flag-bahamas": { @@ -20601,7 +21032,8 @@ "bs", "nation", "country", - "banner" + "banner", + "bahamas" ] }, "flag-bhutan": { @@ -20612,7 +21044,8 @@ "bt", "nation", "country", - "banner" + "banner", + "bhutan" ] }, "flag-bouvet-island": { @@ -20631,7 +21064,8 @@ "bw", "nation", "country", - "banner" + "banner", + "botswana" ] }, "flag-belarus": { @@ -20642,7 +21076,8 @@ "by", "nation", "country", - "banner" + "banner", + "belarus" ] }, "flag-belize": { @@ -20653,7 +21088,8 @@ "bz", "nation", "country", - "banner" + "banner", + "belize" ] }, "flag-canada": { @@ -20664,7 +21100,8 @@ "ca", "nation", "country", - "banner" + "banner", + "canada" ] }, "flag-cocos-keeling-islands": { @@ -20678,7 +21115,8 @@ "islands", "nation", "country", - "banner" + "banner", + "cocos_islands" ] }, "flag-congo--kinshasa": { @@ -20692,7 +21130,8 @@ "republic", "nation", "country", - "banner" + "banner", + "congo_kinshasa" ] }, "flag-central-african-republic": { @@ -20705,7 +21144,8 @@ "republic", "nation", "country", - "banner" + "banner", + "central_african_republic" ] }, "flag-congo--brazzaville": { @@ -20717,7 +21157,8 @@ "congo", "nation", "country", - "banner" + "banner", + "congo_brazzaville" ] }, "flag-switzerland": { @@ -20728,7 +21169,8 @@ "ch", "nation", "country", - "banner" + "banner", + "switzerland" ] }, "flag-cte-divoire": { @@ -20741,7 +21183,8 @@ "coast", "nation", "country", - "banner" + "banner", + "cote_d_ivoire" ] }, "flag-cook-islands": { @@ -20753,7 +21196,8 @@ "islands", "nation", "country", - "banner" + "banner", + "cook_islands" ] }, "flag-chile": { @@ -20763,7 +21207,8 @@ "flag", "nation", "country", - "banner" + "banner", + "chile" ] }, "flag-cameroon": { @@ -20774,7 +21219,8 @@ "cm", "nation", "country", - "banner" + "banner", + "cameroon" ] }, "flag-china": { @@ -20798,7 +21244,8 @@ "co", "nation", "country", - "banner" + "banner", + "colombia" ] }, "flag-clipperton-island": { @@ -20817,7 +21264,8 @@ "rica", "nation", "country", - "banner" + "banner", + "costa_rica" ] }, "flag-cuba": { @@ -20828,7 +21276,8 @@ "cu", "nation", "country", - "banner" + "banner", + "cuba" ] }, "flag-cape-verde": { @@ -20840,7 +21289,8 @@ "verde", "nation", "country", - "banner" + "banner", + "cape_verde" ] }, "flag-curaao": { @@ -20852,7 +21302,8 @@ "curaçao", "nation", "country", - "banner" + "banner", + "curacao" ] }, "flag-christmas-island": { @@ -20864,7 +21315,8 @@ "island", "nation", "country", - "banner" + "banner", + "christmas_island" ] }, "flag-cyprus": { @@ -20875,7 +21327,8 @@ "cy", "nation", "country", - "banner" + "banner", + "cyprus" ] }, "flag-czechia": { @@ -20886,7 +21339,8 @@ "cz", "nation", "country", - "banner" + "banner", + "czechia" ] }, "flag-germany": { @@ -20897,7 +21351,8 @@ "german", "nation", "country", - "banner" + "banner", + "germany" ] }, "flag-diego-garcia": { @@ -20915,7 +21370,8 @@ "dj", "nation", "country", - "banner" + "banner", + "djibouti" ] }, "flag-denmark": { @@ -20926,7 +21382,8 @@ "dk", "nation", "country", - "banner" + "banner", + "denmark" ] }, "flag-dominica": { @@ -20937,7 +21394,8 @@ "dm", "nation", "country", - "banner" + "banner", + "dominica" ] }, "flag-dominican-republic": { @@ -20949,7 +21407,8 @@ "republic", "nation", "country", - "banner" + "banner", + "dominican_republic" ] }, "flag-algeria": { @@ -20960,7 +21419,8 @@ "dz", "nation", "country", - "banner" + "banner", + "algeria" ] }, "flag-ceuta--melilla": { @@ -20979,7 +21439,8 @@ "ec", "nation", "country", - "banner" + "banner", + "ecuador" ] }, "flag-estonia": { @@ -20990,7 +21451,8 @@ "ee", "nation", "country", - "banner" + "banner", + "estonia" ] }, "flag-egypt": { @@ -21001,7 +21463,8 @@ "eg", "nation", "country", - "banner" + "banner", + "egypt" ] }, "flag-western-sahara": { @@ -21013,7 +21476,8 @@ "sahara", "nation", "country", - "banner" + "banner", + "western_sahara" ] }, "flag-eritrea": { @@ -21024,7 +21488,8 @@ "er", "nation", "country", - "banner" + "banner", + "eritrea" ] }, "flag-spain": { @@ -21046,7 +21511,8 @@ "et", "nation", "country", - "banner" + "banner", + "ethiopia" ] }, "flag-european-union": { @@ -21067,7 +21533,8 @@ "fi", "nation", "country", - "banner" + "banner", + "finland" ] }, "flag-fiji": { @@ -21078,7 +21545,8 @@ "fj", "nation", "country", - "banner" + "banner", + "fiji" ] }, "flag-falkland-islands": { @@ -21091,7 +21559,8 @@ "malvinas", "nation", "country", - "banner" + "banner", + "falkland_islands" ] }, "flag-micronesia": { @@ -21116,7 +21585,8 @@ "islands", "nation", "country", - "banner" + "banner", + "faroe_islands" ] }, "flag-france": { @@ -21139,7 +21609,8 @@ "ga", "nation", "country", - "banner" + "banner", + "gabon" ] }, "flag-united-kingdom": { @@ -21160,7 +21631,8 @@ "UK", "english", "england", - "union jack" + "union jack", + "united_kingdom" ] }, "flag-grenada": { @@ -21171,7 +21643,8 @@ "gd", "nation", "country", - "banner" + "banner", + "grenada" ] }, "flag-georgia": { @@ -21182,7 +21655,8 @@ "ge", "nation", "country", - "banner" + "banner", + "georgia" ] }, "flag-french-guiana": { @@ -21194,7 +21668,8 @@ "guiana", "nation", "country", - "banner" + "banner", + "french_guiana" ] }, "flag-guernsey": { @@ -21205,7 +21680,8 @@ "gg", "nation", "country", - "banner" + "banner", + "guernsey" ] }, "flag-ghana": { @@ -21216,7 +21692,8 @@ "gh", "nation", "country", - "banner" + "banner", + "ghana" ] }, "flag-gibraltar": { @@ -21227,7 +21704,8 @@ "gi", "nation", "country", - "banner" + "banner", + "gibraltar" ] }, "flag-greenland": { @@ -21238,7 +21716,8 @@ "gl", "nation", "country", - "banner" + "banner", + "greenland" ] }, "flag-gambia": { @@ -21249,7 +21728,8 @@ "gm", "nation", "country", - "banner" + "banner", + "gambia" ] }, "flag-guinea": { @@ -21260,7 +21740,8 @@ "gn", "nation", "country", - "banner" + "banner", + "guinea" ] }, "flag-guadeloupe": { @@ -21271,7 +21752,8 @@ "gp", "nation", "country", - "banner" + "banner", + "guadeloupe" ] }, "flag-equatorial-guinea": { @@ -21283,7 +21765,8 @@ "gn", "nation", "country", - "banner" + "banner", + "equatorial_guinea" ] }, "flag-greece": { @@ -21294,7 +21777,8 @@ "gr", "nation", "country", - "banner" + "banner", + "greece" ] }, "flag-south-georgia--south-sandwich-islands": { @@ -21309,7 +21793,8 @@ "islands", "nation", "country", - "banner" + "banner", + "south_georgia_south_sandwich_islands" ] }, "flag-guatemala": { @@ -21320,7 +21805,8 @@ "gt", "nation", "country", - "banner" + "banner", + "guatemala" ] }, "flag-guam": { @@ -21331,7 +21817,8 @@ "gu", "nation", "country", - "banner" + "banner", + "guam" ] }, "flag-guineabissau": { @@ -21344,7 +21831,8 @@ "bissau", "nation", "country", - "banner" + "banner", + "guinea_bissau" ] }, "flag-guyana": { @@ -21355,7 +21843,8 @@ "gy", "nation", "country", - "banner" + "banner", + "guyana" ] }, "flag-hong-kong-sar-china": { @@ -21367,7 +21856,8 @@ "kong", "nation", "country", - "banner" + "banner", + "hong_kong_sar_china" ] }, "flag-heard--mcdonald-islands": { @@ -21386,7 +21876,8 @@ "hn", "nation", "country", - "banner" + "banner", + "honduras" ] }, "flag-croatia": { @@ -21397,7 +21888,8 @@ "hr", "nation", "country", - "banner" + "banner", + "croatia" ] }, "flag-haiti": { @@ -21408,7 +21900,8 @@ "ht", "nation", "country", - "banner" + "banner", + "haiti" ] }, "flag-hungary": { @@ -21419,7 +21912,8 @@ "hu", "nation", "country", - "banner" + "banner", + "hungary" ] }, "flag-canary-islands": { @@ -21431,7 +21925,8 @@ "islands", "nation", "country", - "banner" + "banner", + "canary_islands" ] }, "flag-indonesia": { @@ -21441,7 +21936,8 @@ "flag", "nation", "country", - "banner" + "banner", + "indonesia" ] }, "flag-ireland": { @@ -21452,7 +21948,8 @@ "ie", "nation", "country", - "banner" + "banner", + "ireland" ] }, "flag-israel": { @@ -21463,7 +21960,8 @@ "il", "nation", "country", - "banner" + "banner", + "israel" ] }, "flag-isle-of-man": { @@ -21475,7 +21973,8 @@ "man", "nation", "country", - "banner" + "banner", + "isle_of_man" ] }, "flag-india": { @@ -21486,7 +21985,8 @@ "in", "nation", "country", - "banner" + "banner", + "india" ] }, "flag-british-indian-ocean-territory": { @@ -21500,7 +22000,8 @@ "territory", "nation", "country", - "banner" + "banner", + "british_indian_ocean_territory" ] }, "flag-iraq": { @@ -21511,7 +22012,8 @@ "iq", "nation", "country", - "banner" + "banner", + "iraq" ] }, "flag-iran": { @@ -21535,7 +22037,8 @@ "is", "nation", "country", - "banner" + "banner", + "iceland" ] }, "flag-italy": { @@ -21557,7 +22060,8 @@ "je", "nation", "country", - "banner" + "banner", + "jersey" ] }, "flag-jamaica": { @@ -21568,7 +22072,8 @@ "jm", "nation", "country", - "banner" + "banner", + "jamaica" ] }, "flag-jordan": { @@ -21579,7 +22084,8 @@ "jo", "nation", "country", - "banner" + "banner", + "jordan" ] }, "flag-japan": { @@ -21590,7 +22096,8 @@ "japanese", "nation", "country", - "banner" + "banner", + "japan" ] }, "flag-kenya": { @@ -21601,7 +22108,8 @@ "ke", "nation", "country", - "banner" + "banner", + "kenya" ] }, "flag-kyrgyzstan": { @@ -21612,7 +22120,8 @@ "kg", "nation", "country", - "banner" + "banner", + "kyrgyzstan" ] }, "flag-cambodia": { @@ -21623,7 +22132,8 @@ "kh", "nation", "country", - "banner" + "banner", + "cambodia" ] }, "flag-kiribati": { @@ -21634,7 +22144,8 @@ "ki", "nation", "country", - "banner" + "banner", + "kiribati" ] }, "flag-comoros": { @@ -21645,7 +22156,8 @@ "km", "nation", "country", - "banner" + "banner", + "comoros" ] }, "flag-st-kitts--nevis": { @@ -21659,7 +22171,8 @@ "nevis", "nation", "country", - "banner" + "banner", + "st_kitts_nevis" ] }, "flag-north-korea": { @@ -21671,7 +22184,8 @@ "korea", "nation", "country", - "banner" + "banner", + "north_korea" ] }, "flag-south-korea": { @@ -21683,7 +22197,8 @@ "korea", "nation", "country", - "banner" + "banner", + "south_korea" ] }, "flag-kuwait": { @@ -21694,7 +22209,8 @@ "kw", "nation", "country", - "banner" + "banner", + "kuwait" ] }, "flag-cayman-islands": { @@ -21706,7 +22222,8 @@ "islands", "nation", "country", - "banner" + "banner", + "cayman_islands" ] }, "flag-kazakhstan": { @@ -21717,7 +22234,8 @@ "kz", "nation", "country", - "banner" + "banner", + "kazakhstan" ] }, "flag-laos": { @@ -21730,7 +22248,8 @@ "republic", "nation", "country", - "banner" + "banner", + "laos" ] }, "flag-lebanon": { @@ -21741,7 +22260,8 @@ "lb", "nation", "country", - "banner" + "banner", + "lebanon" ] }, "flag-st-lucia": { @@ -21753,7 +22273,8 @@ "lucia", "nation", "country", - "banner" + "banner", + "st_lucia" ] }, "flag-liechtenstein": { @@ -21764,7 +22285,8 @@ "li", "nation", "country", - "banner" + "banner", + "liechtenstein" ] }, "flag-sri-lanka": { @@ -21776,7 +22298,8 @@ "lanka", "nation", "country", - "banner" + "banner", + "sri_lanka" ] }, "flag-liberia": { @@ -21787,7 +22310,8 @@ "lr", "nation", "country", - "banner" + "banner", + "liberia" ] }, "flag-lesotho": { @@ -21798,7 +22322,8 @@ "ls", "nation", "country", - "banner" + "banner", + "lesotho" ] }, "flag-lithuania": { @@ -21809,7 +22334,8 @@ "lt", "nation", "country", - "banner" + "banner", + "lithuania" ] }, "flag-luxembourg": { @@ -21820,7 +22346,8 @@ "lu", "nation", "country", - "banner" + "banner", + "luxembourg" ] }, "flag-latvia": { @@ -21831,7 +22358,8 @@ "lv", "nation", "country", - "banner" + "banner", + "latvia" ] }, "flag-libya": { @@ -21842,7 +22370,8 @@ "ly", "nation", "country", - "banner" + "banner", + "libya" ] }, "flag-morocco": { @@ -21853,7 +22382,8 @@ "ma", "nation", "country", - "banner" + "banner", + "morocco" ] }, "flag-monaco": { @@ -21864,7 +22394,8 @@ "mc", "nation", "country", - "banner" + "banner", + "monaco" ] }, "flag-moldova": { @@ -21887,7 +22418,8 @@ "me", "nation", "country", - "banner" + "banner", + "montenegro" ] }, "flag-st-martin": { @@ -21905,7 +22437,8 @@ "mg", "nation", "country", - "banner" + "banner", + "madagascar" ] }, "flag-marshall-islands": { @@ -21917,7 +22450,8 @@ "islands", "nation", "country", - "banner" + "banner", + "marshall_islands" ] }, "flag-north-macedonia": { @@ -21928,7 +22462,8 @@ "macedonia", "nation", "country", - "banner" + "banner", + "north_macedonia" ] }, "flag-mali": { @@ -21939,7 +22474,8 @@ "ml", "nation", "country", - "banner" + "banner", + "mali" ] }, "flag-myanmar-burma": { @@ -21951,7 +22487,8 @@ "mm", "nation", "country", - "banner" + "banner", + "myanmar" ] }, "flag-mongolia": { @@ -21962,7 +22499,8 @@ "mn", "nation", "country", - "banner" + "banner", + "mongolia" ] }, "flag-macao-sar-china": { @@ -21973,7 +22511,8 @@ "macao", "nation", "country", - "banner" + "banner", + "macao_sar_china" ] }, "flag-northern-mariana-islands": { @@ -21986,7 +22525,8 @@ "islands", "nation", "country", - "banner" + "banner", + "northern_mariana_islands" ] }, "flag-martinique": { @@ -21997,7 +22537,8 @@ "mq", "nation", "country", - "banner" + "banner", + "martinique" ] }, "flag-mauritania": { @@ -22008,7 +22549,8 @@ "mr", "nation", "country", - "banner" + "banner", + "mauritania" ] }, "flag-montserrat": { @@ -22019,7 +22561,8 @@ "ms", "nation", "country", - "banner" + "banner", + "montserrat" ] }, "flag-malta": { @@ -22030,7 +22573,8 @@ "mt", "nation", "country", - "banner" + "banner", + "malta" ] }, "flag-mauritius": { @@ -22041,7 +22585,8 @@ "mu", "nation", "country", - "banner" + "banner", + "mauritius" ] }, "flag-maldives": { @@ -22052,7 +22597,8 @@ "mv", "nation", "country", - "banner" + "banner", + "maldives" ] }, "flag-malawi": { @@ -22063,7 +22609,8 @@ "mw", "nation", "country", - "banner" + "banner", + "malawi" ] }, "flag-mexico": { @@ -22074,7 +22621,8 @@ "mx", "nation", "country", - "banner" + "banner", + "mexico" ] }, "flag-malaysia": { @@ -22085,7 +22633,8 @@ "my", "nation", "country", - "banner" + "banner", + "malaysia" ] }, "flag-mozambique": { @@ -22096,7 +22645,8 @@ "mz", "nation", "country", - "banner" + "banner", + "mozambique" ] }, "flag-namibia": { @@ -22107,7 +22657,8 @@ "na", "nation", "country", - "banner" + "banner", + "namibia" ] }, "flag-new-caledonia": { @@ -22119,7 +22670,8 @@ "caledonia", "nation", "country", - "banner" + "banner", + "new_caledonia" ] }, "flag-niger": { @@ -22130,7 +22682,8 @@ "ne", "nation", "country", - "banner" + "banner", + "niger" ] }, "flag-norfolk-island": { @@ -22142,7 +22695,8 @@ "island", "nation", "country", - "banner" + "banner", + "norfolk_island" ] }, "flag-nigeria": { @@ -22152,7 +22706,8 @@ "flag", "nation", "country", - "banner" + "banner", + "nigeria" ] }, "flag-nicaragua": { @@ -22163,7 +22718,8 @@ "ni", "nation", "country", - "banner" + "banner", + "nicaragua" ] }, "flag-netherlands": { @@ -22174,7 +22730,8 @@ "nl", "nation", "country", - "banner" + "banner", + "netherlands" ] }, "flag-norway": { @@ -22185,7 +22742,8 @@ "no", "nation", "country", - "banner" + "banner", + "norway" ] }, "flag-nepal": { @@ -22196,7 +22754,8 @@ "np", "nation", "country", - "banner" + "banner", + "nepal" ] }, "flag-nauru": { @@ -22207,7 +22766,8 @@ "nr", "nation", "country", - "banner" + "banner", + "nauru" ] }, "flag-niue": { @@ -22218,7 +22778,8 @@ "nu", "nation", "country", - "banner" + "banner", + "niue" ] }, "flag-new-zealand": { @@ -22230,7 +22791,8 @@ "zealand", "nation", "country", - "banner" + "banner", + "new_zealand" ] }, "flag-oman": { @@ -22241,7 +22803,8 @@ "om_symbol", "nation", "country", - "banner" + "banner", + "oman" ] }, "flag-panama": { @@ -22252,7 +22815,8 @@ "pa", "nation", "country", - "banner" + "banner", + "panama" ] }, "flag-peru": { @@ -22263,7 +22827,8 @@ "pe", "nation", "country", - "banner" + "banner", + "peru" ] }, "flag-french-polynesia": { @@ -22275,7 +22840,8 @@ "polynesia", "nation", "country", - "banner" + "banner", + "french_polynesia" ] }, "flag-papua-new-guinea": { @@ -22288,7 +22854,8 @@ "guinea", "nation", "country", - "banner" + "banner", + "papua_new_guinea" ] }, "flag-philippines": { @@ -22299,7 +22866,8 @@ "ph", "nation", "country", - "banner" + "banner", + "philippines" ] }, "flag-pakistan": { @@ -22310,7 +22878,8 @@ "pk", "nation", "country", - "banner" + "banner", + "pakistan" ] }, "flag-poland": { @@ -22321,7 +22890,8 @@ "pl", "nation", "country", - "banner" + "banner", + "poland" ] }, "flag-st-pierre--miquelon": { @@ -22335,7 +22905,8 @@ "miquelon", "nation", "country", - "banner" + "banner", + "st_pierre_miquelon" ] }, "flag-pitcairn-islands": { @@ -22346,7 +22917,8 @@ "pitcairn", "nation", "country", - "banner" + "banner", + "pitcairn_islands" ] }, "flag-puerto-rico": { @@ -22358,7 +22930,8 @@ "rico", "nation", "country", - "banner" + "banner", + "puerto_rico" ] }, "flag-palestinian-territories": { @@ -22371,7 +22944,8 @@ "territories", "nation", "country", - "banner" + "banner", + "palestinian_territories" ] }, "flag-portugal": { @@ -22382,7 +22956,8 @@ "pt", "nation", "country", - "banner" + "banner", + "portugal" ] }, "flag-palau": { @@ -22393,7 +22968,8 @@ "pw", "nation", "country", - "banner" + "banner", + "palau" ] }, "flag-paraguay": { @@ -22404,7 +22980,8 @@ "py", "nation", "country", - "banner" + "banner", + "paraguay" ] }, "flag-qatar": { @@ -22415,7 +22992,8 @@ "qa", "nation", "country", - "banner" + "banner", + "qatar" ] }, "flag-runion": { @@ -22427,7 +23005,8 @@ "réunion", "nation", "country", - "banner" + "banner", + "reunion" ] }, "flag-romania": { @@ -22438,7 +23017,8 @@ "ro", "nation", "country", - "banner" + "banner", + "romania" ] }, "flag-serbia": { @@ -22449,7 +23029,8 @@ "rs", "nation", "country", - "banner" + "banner", + "serbia" ] }, "flag-russia": { @@ -22461,7 +23042,8 @@ "federation", "nation", "country", - "banner" + "banner", + "russia" ] }, "flag-rwanda": { @@ -22472,7 +23054,8 @@ "rw", "nation", "country", - "banner" + "banner", + "rwanda" ] }, "flag-saudi-arabia": { @@ -22482,7 +23065,8 @@ "flag", "nation", "country", - "banner" + "banner", + "saudi_arabia" ] }, "flag-solomon-islands": { @@ -22494,7 +23078,8 @@ "islands", "nation", "country", - "banner" + "banner", + "solomon_islands" ] }, "flag-seychelles": { @@ -22505,7 +23090,8 @@ "sc", "nation", "country", - "banner" + "banner", + "seychelles" ] }, "flag-sudan": { @@ -22516,7 +23102,8 @@ "sd", "nation", "country", - "banner" + "banner", + "sudan" ] }, "flag-sweden": { @@ -22527,7 +23114,8 @@ "se", "nation", "country", - "banner" + "banner", + "sweden" ] }, "flag-singapore": { @@ -22538,7 +23126,8 @@ "sg", "nation", "country", - "banner" + "banner", + "singapore" ] }, "flag-st-helena": { @@ -22553,7 +23142,8 @@ "cunha", "nation", "country", - "banner" + "banner", + "st_helena" ] }, "flag-slovenia": { @@ -22564,7 +23154,8 @@ "si", "nation", "country", - "banner" + "banner", + "slovenia" ] }, "flag-svalbard--jan-mayen": { @@ -22583,7 +23174,8 @@ "sk", "nation", "country", - "banner" + "banner", + "slovakia" ] }, "flag-sierra-leone": { @@ -22595,7 +23187,8 @@ "leone", "nation", "country", - "banner" + "banner", + "sierra_leone" ] }, "flag-san-marino": { @@ -22607,7 +23200,8 @@ "marino", "nation", "country", - "banner" + "banner", + "san_marino" ] }, "flag-senegal": { @@ -22618,7 +23212,8 @@ "sn", "nation", "country", - "banner" + "banner", + "senegal" ] }, "flag-somalia": { @@ -22629,7 +23224,8 @@ "so", "nation", "country", - "banner" + "banner", + "somalia" ] }, "flag-suriname": { @@ -22640,7 +23236,8 @@ "sr", "nation", "country", - "banner" + "banner", + "suriname" ] }, "flag-south-sudan": { @@ -22652,7 +23249,8 @@ "sd", "nation", "country", - "banner" + "banner", + "south_sudan" ] }, "flag-so-tom--prncipe": { @@ -22666,7 +23264,8 @@ "principe", "nation", "country", - "banner" + "banner", + "sao_tome_principe" ] }, "flag-el-salvador": { @@ -22678,7 +23277,8 @@ "salvador", "nation", "country", - "banner" + "banner", + "el_salvador" ] }, "flag-sint-maarten": { @@ -22691,7 +23291,8 @@ "dutch", "nation", "country", - "banner" + "banner", + "sint_maarten" ] }, "flag-syria": { @@ -22704,7 +23305,8 @@ "republic", "nation", "country", - "banner" + "banner", + "syria" ] }, "flag-eswatini": { @@ -22715,7 +23317,8 @@ "sz", "nation", "country", - "banner" + "banner", + "eswatini" ] }, "flag-tristan-da-cunha": { @@ -22736,7 +23339,8 @@ "islands", "nation", "country", - "banner" + "banner", + "turks_caicos_islands" ] }, "flag-chad": { @@ -22747,7 +23351,8 @@ "td", "nation", "country", - "banner" + "banner", + "chad" ] }, "flag-french-southern-territories": { @@ -22760,7 +23365,8 @@ "territories", "nation", "country", - "banner" + "banner", + "french_southern_territories" ] }, "flag-togo": { @@ -22771,7 +23377,8 @@ "tg", "nation", "country", - "banner" + "banner", + "togo" ] }, "flag-thailand": { @@ -22782,7 +23389,8 @@ "th", "nation", "country", - "banner" + "banner", + "thailand" ] }, "flag-tajikistan": { @@ -22793,7 +23401,8 @@ "tj", "nation", "country", - "banner" + "banner", + "tajikistan" ] }, "flag-tokelau": { @@ -22804,7 +23413,8 @@ "tk", "nation", "country", - "banner" + "banner", + "tokelau" ] }, "flag-timorleste": { @@ -22817,7 +23427,8 @@ "leste", "nation", "country", - "banner" + "banner", + "timor_leste" ] }, "flag-turkmenistan": { @@ -22827,7 +23438,8 @@ "flag", "nation", "country", - "banner" + "banner", + "turkmenistan" ] }, "flag-tunisia": { @@ -22838,7 +23450,8 @@ "tn", "nation", "country", - "banner" + "banner", + "tunisia" ] }, "flag-tonga": { @@ -22849,7 +23462,8 @@ "to", "nation", "country", - "banner" + "banner", + "tonga" ] }, "flag-turkey": { @@ -22873,7 +23487,8 @@ "tobago", "nation", "country", - "banner" + "banner", + "trinidad_tobago" ] }, "flag-tuvalu": { @@ -22883,7 +23498,8 @@ "flag", "nation", "country", - "banner" + "banner", + "tuvalu" ] }, "flag-taiwan": { @@ -22894,7 +23510,8 @@ "tw", "nation", "country", - "banner" + "banner", + "taiwan" ] }, "flag-tanzania": { @@ -22918,7 +23535,8 @@ "ua", "nation", "country", - "banner" + "banner", + "ukraine" ] }, "flag-uganda": { @@ -22929,7 +23547,8 @@ "ug", "nation", "country", - "banner" + "banner", + "uganda" ] }, "flag-us-outlying-islands": { @@ -22959,7 +23578,8 @@ "america", "nation", "country", - "banner" + "banner", + "united_states" ] }, "flag-uruguay": { @@ -22970,7 +23590,8 @@ "uy", "nation", "country", - "banner" + "banner", + "uruguay" ] }, "flag-uzbekistan": { @@ -22981,7 +23602,8 @@ "uz", "nation", "country", - "banner" + "banner", + "uzbekistan" ] }, "flag-vatican-city": { @@ -22993,7 +23615,8 @@ "city", "nation", "country", - "banner" + "banner", + "vatican_city" ] }, "flag-st-vincent--grenadines": { @@ -23007,7 +23630,8 @@ "grenadines", "nation", "country", - "banner" + "banner", + "st_vincent_grenadines" ] }, "flag-venezuela": { @@ -23020,7 +23644,8 @@ "republic", "nation", "country", - "banner" + "banner", + "venezuela" ] }, "flag-british-virgin-islands": { @@ -23034,7 +23659,8 @@ "bvi", "nation", "country", - "banner" + "banner", + "british_virgin_islands" ] }, "flag-us-virgin-islands": { @@ -23048,7 +23674,8 @@ "us", "nation", "country", - "banner" + "banner", + "u_s_virgin_islands" ] }, "flag-vietnam": { @@ -23060,7 +23687,8 @@ "nam", "nation", "country", - "banner" + "banner", + "vietnam" ] }, "flag-vanuatu": { @@ -23071,7 +23699,8 @@ "vu", "nation", "country", - "banner" + "banner", + "vanuatu" ] }, "flag-wallis--futuna": { @@ -23084,7 +23713,8 @@ "futuna", "nation", "country", - "banner" + "banner", + "wallis_futuna" ] }, "flag-samoa": { @@ -23095,7 +23725,8 @@ "ws", "nation", "country", - "banner" + "banner", + "samoa" ] }, "flag-kosovo": { @@ -23106,7 +23737,8 @@ "xk", "nation", "country", - "banner" + "banner", + "kosovo" ] }, "flag-yemen": { @@ -23117,7 +23749,8 @@ "ye", "nation", "country", - "banner" + "banner", + "yemen" ] }, "flag-mayotte": { @@ -23128,7 +23761,8 @@ "yt", "nation", "country", - "banner" + "banner", + "mayotte" ] }, "flag-south-africa": { @@ -23140,7 +23774,8 @@ "africa", "nation", "country", - "banner" + "banner", + "south_africa" ] }, "flag-zambia": { @@ -23151,7 +23786,8 @@ "zm", "nation", "country", - "banner" + "banner", + "zambia" ] }, "flag-zimbabwe": { @@ -23162,7 +23798,8 @@ "zw", "nation", "country", - "banner" + "banner", + "zimbabwe" ] }, "flag-england": { diff --git a/tools/release/pushPlayStoreMetaData.sh b/tools/release/pushPlayStoreMetaData.sh index 35833bfcc6..fe7cf7eca6 100755 --- a/tools/release/pushPlayStoreMetaData.sh +++ b/tools/release/pushPlayStoreMetaData.sh @@ -59,24 +59,6 @@ else removeShortDes_si=1 fi -if [[ -f "./fastlane/metadata/android/sq/short_description.txt" ]]; then - echo "It appears that file ./fastlane/metadata/android/sq/short_description.txt now exists. This can be removed." - removeShortDes_sq=0 -else - echo "Copy default short description to ./fastlane/metadata/android/sq" - cp ./fastlane/metadata/android/en-US/short_description.txt ./fastlane/metadata/android/sq - removeShortDes_sq=1 -fi - -if [[ -f "./fastlane/metadata/android/sq/full_description.txt" ]]; then - echo "It appears that file ./fastlane/metadata/android/sq/full_description.txt now exists. This can be removed." - removeFullDes_sq=0 -else - echo "Copy default full description to ./fastlane/metadata/android/sq" - cp ./fastlane/metadata/android/en-US/full_description.txt ./fastlane/metadata/android/sq - removeFullDes_sq=1 -fi - if [[ -f "./fastlane/metadata/android/th/full_description.txt" ]]; then echo "It appears that file ./fastlane/metadata/android/th/full_description.txt now exists. This can be removed." removeFullDes_th=0 @@ -117,18 +99,10 @@ if [[ ${removeShortDes_si} -eq 1 ]]; then rm ./fastlane/metadata/android/si-LK/short_description.txt fi -if [[ ${removeShortDes_sq} -eq 1 ]]; then - rm ./fastlane/metadata/android/sq/short_description.txt -fi - if [[ ${removeFullDes_th} -eq 1 ]]; then rm ./fastlane/metadata/android/th/full_description.txt fi -if [[ ${removeFullDes_sq} -eq 1 ]]; then - rm ./fastlane/metadata/android/sq/full_description.txt -fi - if [[ ${removeFullDes_vi} -eq 1 ]]; then rm ./fastlane/metadata/android/vi/full_description.txt fi diff --git a/tools/templates/ElementFeature/root/src/app_package/ViewModel.kt.ftl b/tools/templates/ElementFeature/root/src/app_package/ViewModel.kt.ftl index 8c25f9f9a8..64e6a0f83f 100644 --- a/tools/templates/ElementFeature/root/src/app_package/ViewModel.kt.ftl +++ b/tools/templates/ElementFeature/root/src/app_package/ViewModel.kt.ftl @@ -2,7 +2,7 @@ package ${escapeKotlinIdentifiers(packageName)} import com.airbnb.mvrx.ActivityViewModelContext import com.airbnb.mvrx.FragmentViewModelContext -import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.ViewModelContext import dagger.assisted.Assisted import dagger.assisted.AssistedInject @@ -27,7 +27,7 @@ class ${viewModelClass} @AssistedInject constructor(@Assisted initialState: ${vi fun create(initialState: ${viewStateClass}): ${viewModelClass} } - companion object : MvRxViewModelFactory<${viewModelClass}, ${viewStateClass}> { + companion object : MavericksViewModelFactory<${viewModelClass}, ${viewStateClass}> { @JvmStatic override fun create(viewModelContext: ViewModelContext, state: ${viewStateClass}): ${viewModelClass}? { diff --git a/tools/templates/ElementFeature/root/src/app_package/ViewState.kt.ftl b/tools/templates/ElementFeature/root/src/app_package/ViewState.kt.ftl index 55e1f5f549..0acb3f78da 100644 --- a/tools/templates/ElementFeature/root/src/app_package/ViewState.kt.ftl +++ b/tools/templates/ElementFeature/root/src/app_package/ViewState.kt.ftl @@ -1,5 +1,5 @@ package ${escapeKotlinIdentifiers(packageName)} -import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MavericksState -data class ${viewStateClass}() : MvRxState \ No newline at end of file +data class ${viewStateClass}() : MavericksState \ No newline at end of file diff --git a/vector/build.gradle b/vector/build.gradle index afc6ac6a25..03b35465bf 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -6,6 +6,7 @@ apply plugin: 'kotlin-android' apply plugin: 'kotlin-parcelize' apply plugin: 'kotlin-kapt' apply plugin: 'placeholder-resolver' +apply plugin: 'dagger.hilt.android.plugin' kapt { correctErrorTypes = true @@ -13,8 +14,8 @@ kapt { // Note: 2 digits max for each value ext.versionMajor = 1 -ext.versionMinor = 2 -ext.versionPatch = 3 +ext.versionMinor = 3 +ext.versionPatch = 7 static def getGitTimestamp() { def cmd = 'git show -s --format=%ct' @@ -298,6 +299,16 @@ android { kotlinOptions { jvmTarget = "11" + freeCompilerArgs += [ + "-Xopt-in=kotlin.RequiresOptIn", + // Fixes false positive "This is an internal Mavericks API. It is not intended for external use." + // of MvRx `by viewModel()` calls. Maybe due to the inlining of code... This is a temporary fix... + "-Xopt-in=com.airbnb.mvrx.InternalMavericksApi", + // Opt in for kotlinx.coroutines.FlowPreview too + "-Xopt-in=kotlinx.coroutines.FlowPreview", + // Opt in for kotlinx.coroutines.ExperimentalCoroutinesApi too + "-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", + ] } sourceSets { @@ -314,17 +325,22 @@ android { } } +configurations { + // videocache includes a sl4j logger which causes mockk to attempt to call the static android Log + testImplementation.exclude group: 'org.slf4j', module: 'slf4j-android' +} + dependencies { implementation project(":matrix-sdk-android") implementation project(":matrix-sdk-android-rx") + implementation project(":matrix-sdk-android-flow") implementation project(":diff-match-patch") implementation project(":multipicker") implementation project(":attachment-viewer") implementation project(":library:ui-styles") implementation 'androidx.multidex:multidex:2.0.1' - implementation libs.jetbrains.kotlinStdlibJdk7 implementation libs.jetbrains.coroutinesCore implementation libs.jetbrains.coroutinesAndroid @@ -334,7 +350,7 @@ dependencies { implementation libs.androidx.constraintLayout implementation "androidx.sharetarget:sharetarget:1.1.0" implementation libs.androidx.core - implementation "androidx.media:media:1.4.1" + implementation "androidx.media:media:1.4.3" implementation "androidx.transition:transition:1.4.1" implementation "org.threeten:threetenbp:1.4.0:no-tzdb" @@ -345,6 +361,9 @@ dependencies { implementation libs.androidx.lifecycleExtensions implementation libs.androidx.lifecycleLivedata + implementation libs.androidx.datastore + implementation libs.androidx.datastorepreferences + // Log implementation libs.jakewharton.timber @@ -353,7 +372,7 @@ dependencies { implementation 'com.facebook.stetho:stetho:1.6.0' // Phone number https://github.com/google/libphonenumber - implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.32' + implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.36' // rx implementation libs.rx.rxKotlin @@ -368,7 +387,10 @@ dependencies { implementation libs.airbnb.epoxyGlide kapt libs.airbnb.epoxyProcessor implementation libs.airbnb.epoxyPaging - implementation libs.airbnb.mvrx + implementation libs.airbnb.mavericks + //TODO: remove when entirely migrated to Flow + implementation libs.airbnb.mavericksRx + // Work implementation libs.androidx.work @@ -404,15 +426,15 @@ dependencies { implementation 'com.nulab-inc:zxcvbn:1.5.2' // To convert voice message on old platforms - implementation 'com.arthenica:ffmpeg-kit-audio:4.4.LTS' + implementation 'com.arthenica:ffmpeg-kit-audio:4.5.LTS' - //Alerter + // Alerter implementation 'com.tapadoo.android:alerter:7.0.1' implementation 'com.otaliastudios:autocomplete:1.1.0' // Shake detection - implementation 'com.squareup:seismic:1.0.2' + implementation 'com.squareup:seismic:1.0.3' // Image Loading implementation libs.github.bigImageViewer @@ -435,11 +457,11 @@ dependencies { implementation 'nl.dionsegijn:konfetti:1.3.2' implementation 'com.github.jetradarmobile:android-snowfall:1.2.1' // DI - implementation libs.dagger.dagger - kapt libs.dagger.daggerCompiler + implementation libs.dagger.hilt + kapt libs.dagger.hiltCompiler // gplay flavor only - gplayImplementation('com.google.firebase:firebase-messaging:22.0.0') { + gplayImplementation('com.google.firebase:firebase-messaging:23.0.0') { exclude group: 'com.google.firebase', module: 'firebase-core' exclude group: 'com.google.firebase', module: 'firebase-analytics' exclude group: 'com.google.firebase', module: 'firebase-measurement-connector' @@ -449,8 +471,7 @@ dependencies { gplayImplementation 'com.google.android.gms:play-services-oss-licenses:17.0.0' implementation "androidx.emoji:emoji-appcompat:1.1.0" - - implementation 'com.github.BillCarsonFr:JsonViewer:0.6' + implementation ('com.github.BillCarsonFr:JsonViewer:0.7') // WebRTC // org.webrtc:google-webrtc is for development purposes only @@ -466,12 +487,12 @@ dependencies { // QR-code // Stick to 3.3.3 because of https://github.com/zxing/zxing/issues/1170 - implementation 'com.google.zxing:core:3.4.1' + implementation 'com.google.zxing:core:3.3.3' implementation 'me.dm7.barcodescanner:zxing:1.9.13' // Emoji Keyboard - implementation 'com.vanniktech:emoji-material:0.7.0' - implementation 'com.vanniktech:emoji-google:0.7.0' + implementation libs.vanniktech.emojiMaterial + implementation libs.vanniktech.emojiGoogle implementation 'im.dlg:android-dialer:1.2.5' @@ -487,8 +508,10 @@ dependencies { // TESTS testImplementation libs.tests.junit testImplementation libs.tests.kluent + testImplementation libs.mockk.mockk // Plant Timber tree for test testImplementation libs.tests.timberJunitRule + testImplementation libs.airbnb.mavericksTesting // Activate when you want to check for leaks, from time to time. //debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.3' @@ -505,7 +528,8 @@ dependencies { // Plant Timber tree for test androidTestImplementation libs.tests.timberJunitRule // "The one who serves a great Espresso" - androidTestImplementation('com.adevinta.android:barista:4.1.0') { + androidTestImplementation('com.adevinta.android:barista:4.2.0') { exclude group: 'org.jetbrains.kotlin' } + androidTestUtil libs.androidx.orchestrator } diff --git a/vector/lint.xml b/vector/lint.xml index 3fca617dee..dde29af62e 100644 --- a/vector/lint.xml +++ b/vector/lint.xml @@ -21,12 +21,12 @@ + - diff --git a/vector/src/androidTest/java/im/vector/app/EspressoExt.kt b/vector/src/androidTest/java/im/vector/app/EspressoExt.kt index d247d88caa..823ce83015 100644 --- a/vector/src/androidTest/java/im/vector/app/EspressoExt.kt +++ b/vector/src/androidTest/java/im/vector/app/EspressoExt.kt @@ -129,7 +129,7 @@ fun activityIdlingResource(activityClass: Class<*>): IdlingResource { private var callback: IdlingResource.ResourceCallback? = null var hasResumed = false - private var currentActivity : Activity? = null + private var currentActivity: Activity? = null val uniqTS = System.currentTimeMillis() override fun getName() = "activityIdlingResource_${activityClass.name}_$uniqTS" diff --git a/vector/src/androidTest/java/im/vector/app/VerifySessionInteractiveTest.kt b/vector/src/androidTest/java/im/vector/app/VerifySessionInteractiveTest.kt index 571bcf474c..982a421425 100644 --- a/vector/src/androidTest/java/im/vector/app/VerifySessionInteractiveTest.kt +++ b/vector/src/androidTest/java/im/vector/app/VerifySessionInteractiveTest.kt @@ -44,14 +44,14 @@ import org.junit.runner.RunWith import org.matrix.android.sdk.api.Matrix import org.matrix.android.sdk.api.auth.UIABaseAuth import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor +import org.matrix.android.sdk.api.auth.UserPasswordAuth +import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod import org.matrix.android.sdk.api.session.crypto.verification.VerificationService import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState -import org.matrix.android.sdk.api.auth.UserPasswordAuth -import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse import kotlin.coroutines.Continuation import kotlin.coroutines.resume diff --git a/vector/src/androidTest/java/im/vector/app/espresso/tools/EspressoPreference.kt b/vector/src/androidTest/java/im/vector/app/espresso/tools/EspressoPreference.kt index bf60ad681f..178b9fb9f6 100644 --- a/vector/src/androidTest/java/im/vector/app/espresso/tools/EspressoPreference.kt +++ b/vector/src/androidTest/java/im/vector/app/espresso/tools/EspressoPreference.kt @@ -30,9 +30,9 @@ import androidx.test.espresso.matcher.ViewMatchers.withClassName import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withText import im.vector.app.R -import org.hamcrest.Matchers.`is` import org.hamcrest.Matchers.allOf import org.hamcrest.Matchers.instanceOf +import org.hamcrest.Matchers.`is` fun clickOnPreference(@StringRes textResId: Int) { onView(withId(R.id.recycler_view)) diff --git a/vector/src/androidTest/java/im/vector/app/features/reactions/data/EmojiDataSourceTest.kt b/vector/src/androidTest/java/im/vector/app/features/reactions/data/EmojiDataSourceTest.kt index 79090c42dd..a880b17e0c 100644 --- a/vector/src/androidTest/java/im/vector/app/features/reactions/data/EmojiDataSourceTest.kt +++ b/vector/src/androidTest/java/im/vector/app/features/reactions/data/EmojiDataSourceTest.kt @@ -17,6 +17,10 @@ package im.vector.app.features.reactions.data import im.vector.app.InstrumentedTest +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.FixMethodOrder @@ -30,64 +34,80 @@ import kotlin.system.measureTimeMillis @FixMethodOrder(MethodSorters.JVM) class EmojiDataSourceTest : InstrumentedTest { + private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main) + @Test fun checkParsingTime() { val time = measureTimeMillis { - EmojiDataSource(context().resources) + createEmojiDataSource() } - assertTrue("Too long to parse", time < 100) } @Test fun checkNumberOfResult() { - val emojiDataSource = EmojiDataSource(context().resources) - assertTrue("Wrong number of emojis", emojiDataSource.rawData.emojis.size >= 500) - assertTrue("Wrong number of categories", emojiDataSource.rawData.categories.size >= 8) + val emojiDataSource = createEmojiDataSource() + val rawData = runBlocking { + emojiDataSource.rawData.await() + } + assertTrue("Wrong number of emojis", rawData.emojis.size >= 500) + assertTrue("Wrong number of categories", rawData.categories.size >= 8) } @Test fun searchTestEmptySearch() { - val emojiDataSource = EmojiDataSource(context().resources) - - assertTrue("Empty search should return at least 500 results", emojiDataSource.filterWith("").size >= 500) + val emojiDataSource = createEmojiDataSource() + val result = runBlocking { + emojiDataSource.filterWith("") + } + assertTrue("Empty search should return at least 500 results", result.size >= 500) } @Test fun searchTestNoResult() { - val emojiDataSource = EmojiDataSource(context().resources) - - assertTrue("Should not have result", emojiDataSource.filterWith("noresult").isEmpty()) + val emojiDataSource = createEmojiDataSource() + val result = runBlocking { + emojiDataSource.filterWith("noresult") + } + assertTrue("Should not have result", result.isEmpty()) } @Test fun searchTestOneResult() { - val emojiDataSource = EmojiDataSource(context().resources) - - assertEquals("Should have 1 result", 1, emojiDataSource.filterWith("france").size) + val emojiDataSource = createEmojiDataSource() + val result = runBlocking { + emojiDataSource.filterWith("france") + } + assertEquals("Should have 1 result", 1, result.size) } @Test fun searchTestManyResult() { - val emojiDataSource = EmojiDataSource(context().resources) - - assertTrue("Should have many result", emojiDataSource.filterWith("fra").size > 1) + val emojiDataSource = createEmojiDataSource() + val result = runBlocking { + emojiDataSource.filterWith("fra") + } + assertTrue("Should have many result", result.size > 1) } @Test fun testTada() { - val emojiDataSource = EmojiDataSource(context().resources) - - val result = emojiDataSource.filterWith("tada") - + val emojiDataSource = createEmojiDataSource() + val result = runBlocking { + emojiDataSource.filterWith("tada") + } assertEquals("Should find tada emoji", 1, result.size) assertEquals("Should find tada emoji", "🎉", result[0].emoji) } @Test fun testQuickReactions() { - val emojiDataSource = EmojiDataSource(context().resources) - - assertEquals("Should have 8 quick reactions", 8, emojiDataSource.getQuickReactions().size) + val emojiDataSource = createEmojiDataSource() + val result = runBlocking { + emojiDataSource.getQuickReactions() + } + assertEquals("Should have 8 quick reactions", 8, result.size) } + + private fun createEmojiDataSource() = EmojiDataSource(coroutineScope, context().resources) } diff --git a/vector/src/androidTest/java/im/vector/app/ui/UiAllScreensSanityTest.kt b/vector/src/androidTest/java/im/vector/app/ui/UiAllScreensSanityTest.kt index bad5d29e06..a9cb5274ed 100644 --- a/vector/src/androidTest/java/im/vector/app/ui/UiAllScreensSanityTest.kt +++ b/vector/src/androidTest/java/im/vector/app/ui/UiAllScreensSanityTest.kt @@ -225,6 +225,8 @@ class UiAllScreensSanityTest { clickOn(R.string.message_add_reaction) // Filter // TODO clickMenu(R.id.search) + // Wait for emoji to load, it's async now + sleep(2_000) clickListItem(R.id.emojiRecyclerView, 4) // Test Edit mode @@ -283,6 +285,7 @@ class UiAllScreensSanityTest { clickListItem(R.id.matrixProfileRecyclerView, 9) // File tab clickOn(R.string.uploads_files_title) + sleep(1000) pressBack() assertDisplayed(R.id.roomProfileAvatarView) @@ -334,6 +337,7 @@ class UiAllScreensSanityTest { private fun navigateToRoomPeople() { // Open first user clickListItem(R.id.roomSettingsRecyclerView, 1) + sleep(1000) assertDisplayed(R.id.memberProfilePowerLevelView) // Verification @@ -342,8 +346,9 @@ class UiAllScreensSanityTest { // Role clickListItem(R.id.matrixProfileRecyclerView, 3) + sleep(1000) clickDialogNegativeButton() - + sleep(1000) clickBack() } diff --git a/vector/src/debug/java/im/vector/app/features/debug/DebugMenuActivity.kt b/vector/src/debug/java/im/vector/app/features/debug/DebugMenuActivity.kt index 4b5228d199..960994b169 100644 --- a/vector/src/debug/java/im/vector/app/features/debug/DebugMenuActivity.kt +++ b/vector/src/debug/java/im/vector/app/features/debug/DebugMenuActivity.kt @@ -24,9 +24,9 @@ import android.os.Build import androidx.core.app.NotificationCompat import androidx.core.app.Person import androidx.core.content.getSystemService +import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder -import im.vector.app.core.di.ScreenComponent import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO @@ -50,6 +50,7 @@ import org.matrix.android.sdk.internal.crypto.verification.qrcode.toQrCodeData import timber.log.Timber import javax.inject.Inject +@AndroidEntryPoint class DebugMenuActivity : VectorBaseActivity() { override fun getBinding() = ActivityDebugMenuBinding.inflate(layoutInflater) @@ -57,10 +58,6 @@ class DebugMenuActivity : VectorBaseActivity() { @Inject lateinit var activeSessionHolder: ActiveSessionHolder - override fun injectWith(injector: ScreenComponent) { - injector.inject(this) - } - private lateinit var buffer: ByteArray override fun initUiAndData() { @@ -235,8 +232,8 @@ class DebugMenuActivity : VectorBaseActivity() { private val qrStartForActivityResult = registerStartForActivityResult { activityResult -> if (activityResult.resultCode == Activity.RESULT_OK) { - toast("QrCode: " + QrCodeScannerActivity.getResultText(activityResult.data) - + " is QRCode: " + QrCodeScannerActivity.getResultIsQrCode(activityResult.data)) + toast("QrCode: " + QrCodeScannerActivity.getResultText(activityResult.data) + + " is QRCode: " + QrCodeScannerActivity.getResultIsQrCode(activityResult.data)) // Also update the current QR Code (reverse operation) // renderQrCode(QrCodeScannerActivity.getResultText(data) ?: "") diff --git a/vector/src/debug/java/im/vector/app/features/debug/DebugPermissionActivity.kt b/vector/src/debug/java/im/vector/app/features/debug/DebugPermissionActivity.kt index 048c64bc3a..a35bb40f8f 100644 --- a/vector/src/debug/java/im/vector/app/features/debug/DebugPermissionActivity.kt +++ b/vector/src/debug/java/im/vector/app/features/debug/DebugPermissionActivity.kt @@ -22,6 +22,7 @@ import android.os.Build import android.widget.Toast import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat +import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.utils.checkPermissions @@ -31,6 +32,7 @@ import im.vector.app.core.utils.registerForPermissionsResult import im.vector.app.databinding.ActivityDebugPermissionBinding import timber.log.Timber +@AndroidEntryPoint class DebugPermissionActivity : VectorBaseActivity() { override fun getBinding() = ActivityDebugPermissionBinding.inflate(layoutInflater) diff --git a/vector/src/debug/java/im/vector/app/features/debug/sas/SasEmojiItem.kt b/vector/src/debug/java/im/vector/app/features/debug/sas/SasEmojiItem.kt index 408aebb186..a7b74f3b59 100644 --- a/vector/src/debug/java/im/vector/app/features/debug/sas/SasEmojiItem.kt +++ b/vector/src/debug/java/im/vector/app/features/debug/sas/SasEmojiItem.kt @@ -23,9 +23,9 @@ import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyModel -import org.matrix.android.sdk.api.session.crypto.verification.EmojiRepresentation import me.gujun.android.span.image import me.gujun.android.span.span +import org.matrix.android.sdk.api.session.crypto.verification.EmojiRepresentation @EpoxyModelClass(layout = im.vector.app.R.layout.item_sas_emoji) abstract class SasEmojiItem : VectorEpoxyModel() { diff --git a/vector/src/fdroid/java/im/vector/app/fdroid/features/settings/troubleshoot/TestAutoStartBoot.kt b/vector/src/fdroid/java/im/vector/app/fdroid/features/settings/troubleshoot/TestAutoStartBoot.kt index e46a07f712..27a3f09ddc 100644 --- a/vector/src/fdroid/java/im/vector/app/fdroid/features/settings/troubleshoot/TestAutoStartBoot.kt +++ b/vector/src/fdroid/java/im/vector/app/fdroid/features/settings/troubleshoot/TestAutoStartBoot.kt @@ -27,8 +27,8 @@ import javax.inject.Inject * Test that the application is started on boot */ class TestAutoStartBoot @Inject constructor(private val vectorPreferences: VectorPreferences, - private val stringProvider: StringProvider) - : TroubleshootTest(R.string.settings_troubleshoot_test_service_boot_title) { + private val stringProvider: StringProvider) : + TroubleshootTest(R.string.settings_troubleshoot_test_service_boot_title) { override fun perform(activityResultLauncher: ActivityResultLauncher) { if (vectorPreferences.autoStartOnBoot()) { diff --git a/vector/src/fdroid/java/im/vector/app/fdroid/features/settings/troubleshoot/TestBackgroundRestrictions.kt b/vector/src/fdroid/java/im/vector/app/fdroid/features/settings/troubleshoot/TestBackgroundRestrictions.kt index abdd696724..b5635e186c 100644 --- a/vector/src/fdroid/java/im/vector/app/fdroid/features/settings/troubleshoot/TestBackgroundRestrictions.kt +++ b/vector/src/fdroid/java/im/vector/app/fdroid/features/settings/troubleshoot/TestBackgroundRestrictions.kt @@ -18,17 +18,17 @@ package im.vector.app.fdroid.features.settings.troubleshoot import android.content.Intent import android.net.ConnectivityManager import androidx.activity.result.ActivityResultLauncher -import androidx.appcompat.app.AppCompatActivity import androidx.core.content.getSystemService import androidx.core.net.ConnectivityManagerCompat +import androidx.fragment.app.FragmentActivity import im.vector.app.R import im.vector.app.core.resources.StringProvider import im.vector.app.features.settings.troubleshoot.TroubleshootTest import javax.inject.Inject -class TestBackgroundRestrictions @Inject constructor(private val context: AppCompatActivity, - private val stringProvider: StringProvider) - : TroubleshootTest(R.string.settings_troubleshoot_test_bg_restricted_title) { +class TestBackgroundRestrictions @Inject constructor(private val context: FragmentActivity, + private val stringProvider: StringProvider) : + TroubleshootTest(R.string.settings_troubleshoot_test_bg_restricted_title) { override fun perform(activityResultLauncher: ActivityResultLauncher) { context.getSystemService()!!.apply { diff --git a/vector/src/fdroid/java/im/vector/app/fdroid/features/settings/troubleshoot/TestBatteryOptimization.kt b/vector/src/fdroid/java/im/vector/app/fdroid/features/settings/troubleshoot/TestBatteryOptimization.kt index b1eeae6681..a5154c7483 100644 --- a/vector/src/fdroid/java/im/vector/app/fdroid/features/settings/troubleshoot/TestBatteryOptimization.kt +++ b/vector/src/fdroid/java/im/vector/app/fdroid/features/settings/troubleshoot/TestBatteryOptimization.kt @@ -17,7 +17,7 @@ package im.vector.app.fdroid.features.settings.troubleshoot import android.content.Intent import androidx.activity.result.ActivityResultLauncher -import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.FragmentActivity import im.vector.app.R import im.vector.app.core.resources.StringProvider import im.vector.app.core.utils.isIgnoringBatteryOptimizations @@ -26,7 +26,7 @@ import im.vector.app.features.settings.troubleshoot.TroubleshootTest import javax.inject.Inject class TestBatteryOptimization @Inject constructor( - private val context: AppCompatActivity, + private val context: FragmentActivity, private val stringProvider: StringProvider ) : TroubleshootTest(R.string.settings_troubleshoot_test_battery_title) { diff --git a/vector/src/fdroid/java/im/vector/app/fdroid/receiver/AlarmSyncBroadcastReceiver.kt b/vector/src/fdroid/java/im/vector/app/fdroid/receiver/AlarmSyncBroadcastReceiver.kt index b94e99208b..0f375561b2 100644 --- a/vector/src/fdroid/java/im/vector/app/fdroid/receiver/AlarmSyncBroadcastReceiver.kt +++ b/vector/src/fdroid/java/im/vector/app/fdroid/receiver/AlarmSyncBroadcastReceiver.kt @@ -24,7 +24,7 @@ import android.content.Intent import android.os.Build import androidx.core.content.ContextCompat import androidx.core.content.getSystemService -import im.vector.app.core.di.HasVectorInjector +import im.vector.app.core.extensions.singletonEntryPoint import im.vector.app.core.services.VectorSyncService import org.matrix.android.sdk.internal.session.sync.job.SyncService import timber.log.Timber @@ -33,9 +33,8 @@ class AlarmSyncBroadcastReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { Timber.d("## Sync: AlarmSyncBroadcastReceiver received intent") - val vectorPreferences = (context.applicationContext as? HasVectorInjector) - ?.injector() - ?.takeIf { it.activeSessionHolder().getSafeActiveSession() != null } + val vectorPreferences = context.singletonEntryPoint() + .takeIf { it.activeSessionHolder().getSafeActiveSession() != null } ?.vectorPreferences() ?: return Unit.also { Timber.v("No active session, so don't launch sync service.") } diff --git a/vector/src/fdroid/java/im/vector/app/fdroid/receiver/OnApplicationUpgradeOrRebootReceiver.kt b/vector/src/fdroid/java/im/vector/app/fdroid/receiver/OnApplicationUpgradeOrRebootReceiver.kt index 797b5734a2..935d7e6e13 100644 --- a/vector/src/fdroid/java/im/vector/app/fdroid/receiver/OnApplicationUpgradeOrRebootReceiver.kt +++ b/vector/src/fdroid/java/im/vector/app/fdroid/receiver/OnApplicationUpgradeOrRebootReceiver.kt @@ -20,8 +20,7 @@ package im.vector.app.fdroid.receiver import android.content.BroadcastReceiver import android.content.Context import android.content.Intent -import im.vector.app.core.di.HasVectorInjector -import im.vector.app.core.extensions.vectorComponent +import im.vector.app.core.extensions.singletonEntryPoint import im.vector.app.fdroid.BackgroundSyncStarter import timber.log.Timber @@ -29,13 +28,11 @@ class OnApplicationUpgradeOrRebootReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { Timber.v("## onReceive() ${intent.action}") - val appContext = context.applicationContext - if (appContext is HasVectorInjector) { + val singletonEntryPoint = context.singletonEntryPoint() BackgroundSyncStarter.start( context, - appContext.vectorComponent().vectorPreferences(), - appContext.injector().activeSessionHolder() + singletonEntryPoint.vectorPreferences(), + singletonEntryPoint.activeSessionHolder() ) - } } } diff --git a/vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestFirebaseToken.kt b/vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestFirebaseToken.kt index 1107737888..89270cce55 100644 --- a/vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestFirebaseToken.kt +++ b/vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestFirebaseToken.kt @@ -17,7 +17,7 @@ package im.vector.app.gplay.features.settings.troubleshoot import android.content.Intent import androidx.activity.result.ActivityResultLauncher -import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.FragmentActivity import com.google.firebase.messaging.FirebaseMessaging import im.vector.app.R import im.vector.app.core.resources.StringProvider @@ -30,7 +30,7 @@ import javax.inject.Inject /* * Test that app can successfully retrieve a token via firebase */ -class TestFirebaseToken @Inject constructor(private val context: AppCompatActivity, +class TestFirebaseToken @Inject constructor(private val context: FragmentActivity, private val stringProvider: StringProvider) : TroubleshootTest(R.string.settings_troubleshoot_test_fcm_title) { override fun perform(activityResultLauncher: ActivityResultLauncher) { diff --git a/vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestPlayServices.kt b/vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestPlayServices.kt index 92e713de81..cc682e7a5f 100644 --- a/vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestPlayServices.kt +++ b/vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestPlayServices.kt @@ -17,7 +17,7 @@ package im.vector.app.gplay.features.settings.troubleshoot import android.content.Intent import androidx.activity.result.ActivityResultLauncher -import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.FragmentActivity import com.google.android.gms.common.ConnectionResult import com.google.android.gms.common.GoogleApiAvailability import im.vector.app.R @@ -29,9 +29,9 @@ import javax.inject.Inject /* * Check that the play services APK is available an up-to-date. If needed provide quick fix to install it. */ -class TestPlayServices @Inject constructor(private val context: AppCompatActivity, - private val stringProvider: StringProvider) - : TroubleshootTest(R.string.settings_troubleshoot_test_play_services_title) { +class TestPlayServices @Inject constructor(private val context: FragmentActivity, + private val stringProvider: StringProvider) : + TroubleshootTest(R.string.settings_troubleshoot_test_play_services_title) { override fun perform(activityResultLauncher: ActivityResultLauncher) { val apiAvailability = GoogleApiAvailability.getInstance() diff --git a/vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestPushFromPushGateway.kt b/vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestPushFromPushGateway.kt index d429b293b2..7ae68b201b 100644 --- a/vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestPushFromPushGateway.kt +++ b/vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestPushFromPushGateway.kt @@ -17,7 +17,7 @@ package im.vector.app.gplay.features.settings.troubleshoot import android.content.Intent import androidx.activity.result.ActivityResultLauncher -import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.FragmentActivity import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.error.ErrorFormatter @@ -36,12 +36,12 @@ import javax.inject.Inject /** * Test Push by asking the Push Gateway to send a Push back */ -class TestPushFromPushGateway @Inject constructor(private val context: AppCompatActivity, +class TestPushFromPushGateway @Inject constructor(private val context: FragmentActivity, private val stringProvider: StringProvider, private val errorFormatter: ErrorFormatter, private val pushersManager: PushersManager, - private val activeSessionHolder: ActiveSessionHolder) - : TroubleshootTest(R.string.settings_troubleshoot_test_push_loop_title) { + private val activeSessionHolder: ActiveSessionHolder) : + TroubleshootTest(R.string.settings_troubleshoot_test_push_loop_title) { private var action: Job? = null private var pushReceived: Boolean = false diff --git a/vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestTokenRegistration.kt b/vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestTokenRegistration.kt index f400c17d46..913b5491ea 100644 --- a/vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestTokenRegistration.kt +++ b/vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestTokenRegistration.kt @@ -17,27 +17,27 @@ package im.vector.app.gplay.features.settings.troubleshoot import android.content.Intent import androidx.activity.result.ActivityResultLauncher -import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.FragmentActivity import androidx.lifecycle.Observer import androidx.work.WorkInfo import androidx.work.WorkManager -import org.matrix.android.sdk.api.session.pushers.PusherState import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.pushers.PushersManager import im.vector.app.core.resources.StringProvider import im.vector.app.features.settings.troubleshoot.TroubleshootTest import im.vector.app.push.fcm.FcmHelper +import org.matrix.android.sdk.api.session.pushers.PusherState import javax.inject.Inject /** * Force registration of the token to HomeServer */ -class TestTokenRegistration @Inject constructor(private val context: AppCompatActivity, +class TestTokenRegistration @Inject constructor(private val context: FragmentActivity, private val stringProvider: StringProvider, private val pushersManager: PushersManager, - private val activeSessionHolder: ActiveSessionHolder) - : TroubleshootTest(R.string.settings_troubleshoot_test_token_registration_title) { + private val activeSessionHolder: ActiveSessionHolder) : + TroubleshootTest(R.string.settings_troubleshoot_test_token_registration_title) { override fun perform(activityResultLauncher: ActivityResultLauncher) { // Check if we have a registered pusher for this token @@ -57,7 +57,7 @@ class TestTokenRegistration @Inject constructor(private val context: AppCompatAc stringProvider.getString(R.string.sas_error_unknown)) quickFix = object : TroubleshootQuickFix(R.string.settings_troubleshoot_test_token_registration_quick_fix) { override fun doFix() { - val workId = pushersManager.registerPusherWithFcmKey(fcmToken) + val workId = pushersManager.enqueueRegisterPusherWithFcmKey(fcmToken) WorkManager.getInstance(context).getWorkInfoByIdLiveData(workId).observe(context, Observer { workInfo -> if (workInfo != null) { if (workInfo.state == WorkInfo.State.SUCCEEDED) { diff --git a/vector/src/gplay/java/im/vector/app/gplay/push/fcm/VectorFirebaseMessagingService.kt b/vector/src/gplay/java/im/vector/app/gplay/push/fcm/VectorFirebaseMessagingService.kt index 4cefeadb62..63d50d4f97 100755 --- a/vector/src/gplay/java/im/vector/app/gplay/push/fcm/VectorFirebaseMessagingService.kt +++ b/vector/src/gplay/java/im/vector/app/gplay/push/fcm/VectorFirebaseMessagingService.kt @@ -27,40 +27,43 @@ import androidx.lifecycle.ProcessLifecycleOwner import androidx.localbroadcastmanager.content.LocalBroadcastManager import com.google.firebase.messaging.FirebaseMessagingService import com.google.firebase.messaging.RemoteMessage +import dagger.hilt.android.AndroidEntryPoint import im.vector.app.BuildConfig -import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder -import im.vector.app.core.extensions.vectorComponent import im.vector.app.core.network.WifiDetector import im.vector.app.core.pushers.PushersManager import im.vector.app.features.badge.BadgeProxy import im.vector.app.features.notifications.NotifiableEventResolver -import im.vector.app.features.notifications.NotifiableMessageEvent import im.vector.app.features.notifications.NotificationDrawerManager import im.vector.app.features.notifications.NotificationUtils -import im.vector.app.features.notifications.SimpleNotifiableEvent +import im.vector.app.features.settings.VectorDataStore import im.vector.app.features.settings.VectorPreferences import im.vector.app.push.fcm.FcmHelper import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import org.matrix.android.sdk.api.extensions.tryOrNull -import org.matrix.android.sdk.api.pushrules.Action +import org.matrix.android.sdk.api.logger.LoggerTag import org.matrix.android.sdk.api.session.Session -import org.matrix.android.sdk.api.session.events.model.Event import timber.log.Timber +import javax.inject.Inject + +private val loggerTag = LoggerTag("Push", LoggerTag.SYNC) /** * Class extending FirebaseMessagingService. */ +@AndroidEntryPoint class VectorFirebaseMessagingService : FirebaseMessagingService() { - private lateinit var notificationDrawerManager: NotificationDrawerManager - private lateinit var notifiableEventResolver: NotifiableEventResolver - private lateinit var pusherManager: PushersManager - private lateinit var activeSessionHolder: ActiveSessionHolder - private lateinit var vectorPreferences: VectorPreferences - private lateinit var wifiDetector: WifiDetector + @Inject lateinit var notificationDrawerManager: NotificationDrawerManager + @Inject lateinit var notifiableEventResolver: NotifiableEventResolver + @Inject lateinit var pusherManager: PushersManager + @Inject lateinit var activeSessionHolder: ActiveSessionHolder + @Inject lateinit var vectorPreferences: VectorPreferences + @Inject lateinit var vectorDataStore: VectorDataStore + @Inject lateinit var wifiDetector: WifiDetector private val coroutineScope = CoroutineScope(SupervisorJob()) @@ -69,18 +72,6 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { Handler(Looper.getMainLooper()) } - override fun onCreate() { - super.onCreate() - with(vectorComponent()) { - notificationDrawerManager = notificationDrawerManager() - notifiableEventResolver = notifiableEventResolver() - pusherManager = pusherManager() - activeSessionHolder = activeSessionHolder() - vectorPreferences = vectorPreferences() - wifiDetector = wifiDetector() - } - } - /** * Called when message is received. * @@ -88,9 +79,13 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { */ override fun onMessageReceived(message: RemoteMessage) { if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) { - Timber.d("## onMessageReceived() %s", message.data.toString()) + Timber.tag(loggerTag.value).d("## onMessageReceived() %s", message.data.toString()) + } + Timber.tag(loggerTag.value).d("## onMessageReceived() from FCM with priority %s", message.priority) + + runBlocking { + vectorDataStore.incrementPushCounter() } - Timber.d("## onMessageReceived() from FCM with priority %s", message.priority) // Diagnostic Push if (message.data["event_id"] == PushersManager.TEST_EVENT_ID) { @@ -100,14 +95,14 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { } if (!vectorPreferences.areNotificationEnabledForDevice()) { - Timber.i("Notification are disabled for this device") + Timber.tag(loggerTag.value).i("Notification are disabled for this device") return } mUIHandler.post { if (ProcessLifecycleOwner.get().lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) { // we are in foreground, let the sync do the things? - Timber.d("PUSH received in a foreground state, ignore") + Timber.tag(loggerTag.value).d("PUSH received in a foreground state, ignore") } else { onMessageReceivedInternal(message.data) } @@ -121,10 +116,10 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { * you retrieve the token. */ override fun onNewToken(refreshedToken: String) { - Timber.i("onNewToken: FCM Token has been updated") + Timber.tag(loggerTag.value).i("onNewToken: FCM Token has been updated") FcmHelper.storeFcmToken(this, refreshedToken) if (vectorPreferences.areNotificationEnabledForDevice() && activeSessionHolder.hasActiveSession()) { - pusherManager.registerPusherWithFcmKey(refreshedToken) + pusherManager.enqueueRegisterPusherWithFcmKey(refreshedToken) } } @@ -138,7 +133,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { * It is recommended that the app do a full sync with the app server after receiving this call. */ override fun onDeletedMessages() { - Timber.v("## onDeletedMessages()") + Timber.tag(loggerTag.value).v("## onDeletedMessages()") } /** @@ -150,9 +145,9 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { private fun onMessageReceivedInternal(data: Map) { try { if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) { - Timber.d("## onMessageReceivedInternal() : $data") + Timber.tag(loggerTag.value).d("## onMessageReceivedInternal() : $data") } else { - Timber.d("## onMessageReceivedInternal() : $data") + Timber.tag(loggerTag.value).d("## onMessageReceivedInternal()") } // update the badge counter @@ -162,24 +157,24 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { val session = activeSessionHolder.getSafeActiveSession() if (session == null) { - Timber.w("## Can't sync from push, no current session") + Timber.tag(loggerTag.value).w("## Can't sync from push, no current session") } else { val eventId = data["event_id"] val roomId = data["room_id"] if (isEventAlreadyKnown(eventId, roomId)) { - Timber.d("Ignoring push, event already known") + Timber.tag(loggerTag.value).d("Ignoring push, event already known") } else { // Try to get the Event content faster - Timber.d("Requesting event in fast lane") + Timber.tag(loggerTag.value).d("Requesting event in fast lane") getEventFastLane(session, roomId, eventId) - Timber.d("Requesting background sync") + Timber.tag(loggerTag.value).d("Requesting background sync") session.requireBackgroundSync() } } } catch (e: Exception) { - Timber.e(e, "## onMessageReceivedInternal() failed") + Timber.tag(loggerTag.value).e(e, "## onMessageReceivedInternal() failed") } } @@ -193,20 +188,19 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { } if (wifiDetector.isConnectedToWifi().not()) { - Timber.d("No WiFi network, do not get Event") + Timber.tag(loggerTag.value).d("No WiFi network, do not get Event") return } coroutineScope.launch { - Timber.d("Fast lane: start request") + Timber.tag(loggerTag.value).d("Fast lane: start request") val event = tryOrNull { session.getEvent(roomId, eventId) } ?: return@launch - val resolvedEvent = notifiableEventResolver.resolveInMemoryEvent(session, event) + val resolvedEvent = notifiableEventResolver.resolveInMemoryEvent(session, event, canBeReplaced = true) resolvedEvent - ?.also { Timber.d("Fast lane: notify drawer") } + ?.also { Timber.tag(loggerTag.value).d("Fast lane: notify drawer") } ?.let { - it.isPushGatewayEvent = true notificationDrawerManager.onNotifiableEventReceived(it) notificationDrawerManager.refreshNotificationDrawer() } @@ -222,92 +216,9 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { val room = session.getRoom(roomId) ?: return false return room.getTimeLineEvent(eventId) != null } catch (e: Exception) { - Timber.e(e, "## isEventAlreadyKnown() : failed to check if the event was already defined") + Timber.tag(loggerTag.value).e(e, "## isEventAlreadyKnown() : failed to check if the event was already defined") } } return false } - - private fun handleNotificationWithoutSyncingMode(data: Map, session: Session?) { - if (session == null) { - Timber.e("## handleNotificationWithoutSyncingMode cannot find session") - return - } - - // The Matrix event ID of the event being notified about. - // This is required if the notification is about a particular Matrix event. - // It may be omitted for notifications that only contain updated badge counts. - // This ID can and should be used to detect duplicate notification requests. - val eventId = data["event_id"] ?: return // Just ignore - - val eventType = data["type"] - if (eventType == null) { - // Just add a generic unknown event - val simpleNotifiableEvent = SimpleNotifiableEvent( - session.myUserId, - eventId, - null, - true, // It's an issue in this case, all event will bing even if expected to be silent. - title = getString(R.string.notification_unknown_new_event), - description = "", - type = null, - timestamp = System.currentTimeMillis(), - soundName = Action.ACTION_OBJECT_VALUE_VALUE_DEFAULT, - isPushGatewayEvent = true - ) - notificationDrawerManager.onNotifiableEventReceived(simpleNotifiableEvent) - notificationDrawerManager.refreshNotificationDrawer() - } else { - val event = parseEvent(data) ?: return - - val notifiableEvent = notifiableEventResolver.resolveEvent(event, session) - - if (notifiableEvent == null) { - Timber.e("Unsupported notifiable event $eventId") - if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) { - Timber.e("--> $event") - } - } else { - if (notifiableEvent is NotifiableMessageEvent) { - if (notifiableEvent.senderName.isNullOrEmpty()) { - notifiableEvent.senderName = data["sender_display_name"] ?: data["sender"] ?: "" - } - if (notifiableEvent.roomName.isNullOrEmpty()) { - notifiableEvent.roomName = findRoomNameBestEffort(data, session) ?: "" - } - } - - notifiableEvent.isPushGatewayEvent = true - notifiableEvent.matrixID = session.myUserId - notificationDrawerManager.onNotifiableEventReceived(notifiableEvent) - notificationDrawerManager.refreshNotificationDrawer() - } - } - } - - private fun findRoomNameBestEffort(data: Map, session: Session?): String? { - var roomName: String? = data["room_name"] - val roomId = data["room_id"] - if (null == roomName && null != roomId) { - // Try to get the room name from our store - roomName = session?.getRoom(roomId)?.roomSummary()?.displayName - } - return roomName - } - - /** - * Try to create an event from the FCM data - * - * @param data the FCM data - * @return the event or null if required data are missing - */ - private fun parseEvent(data: Map?): Event? { - return Event( - eventId = data?.get("event_id") ?: return null, - senderId = data["sender"], - roomId = data["room_id"] ?: return null, - type = data["type"] ?: return null, - originServerTs = System.currentTimeMillis() - ) - } } diff --git a/vector/src/gplay/java/im/vector/app/push/fcm/FcmHelper.kt b/vector/src/gplay/java/im/vector/app/push/fcm/FcmHelper.kt index f3bdcafb1c..fe091ffda3 100755 --- a/vector/src/gplay/java/im/vector/app/push/fcm/FcmHelper.kt +++ b/vector/src/gplay/java/im/vector/app/push/fcm/FcmHelper.kt @@ -75,7 +75,7 @@ object FcmHelper { .addOnSuccessListener { token -> storeFcmToken(activity, token) if (registerPusher) { - pushersManager.registerPusherWithFcmKey(token) + pushersManager.enqueueRegisterPusherWithFcmKey(token) } } .addOnFailureListener { e -> diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index 6c9453a564..376e0e869a 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -83,13 +83,19 @@ android:name="android.max_aspect" android:value="9.9" /> + + + @@ -103,10 +109,13 @@ + + @@ -120,10 +129,13 @@ android:scheme="element" /> + + @@ -143,7 +155,6 @@ android:name=".features.media.VectorAttachmentViewerActivity" android:theme="@style/Theme.Vector.Black.Transparent" tools:ignore="Instantiatable" /> - - @@ -180,7 +190,13 @@ - + + + + @@ -196,8 +212,37 @@ + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - + android:supportsPictureInPicture="true" + android:taskAffinity=".features.call.VectorCallActivity" /> @@ -365,7 +386,9 @@ A media button receiver receives and helps translate hardware media playback buttons, such as those found on wired and wireless headsets, into the appropriate callbacks in your app. --> - + @@ -375,9 +398,15 @@ + android:name="androidx.startup.InitializationProvider" + android:authorities="${applicationId}.androidx-startup" + android:exported="false" + tools:node="merge"> + + - \ No newline at end of file + diff --git a/vector/src/main/java/im/vector/app/AppStateHandler.kt b/vector/src/main/java/im/vector/app/AppStateHandler.kt index e6bc1b08a2..30078963f4 100644 --- a/vector/src/main/java/im/vector/app/AppStateHandler.kt +++ b/vector/src/main/java/im/vector/app/AppStateHandler.kt @@ -76,8 +76,8 @@ class AppStateHandler @Inject constructor( fun setCurrentSpace(spaceId: String?, session: Session? = null) { val uSession = session ?: activeSessionHolder.getSafeActiveSession() ?: return - if (selectedSpaceDataSource.currentValue?.orNull() is RoomGroupingMethod.BySpace - && spaceId == selectedSpaceDataSource.currentValue?.orNull()?.space()?.roomId) return + if (selectedSpaceDataSource.currentValue?.orNull() is RoomGroupingMethod.BySpace && + spaceId == selectedSpaceDataSource.currentValue?.orNull()?.space()?.roomId) return val spaceSum = spaceId?.let { uSession.getRoomSummary(spaceId) } selectedSpaceDataSource.post(Option.just(RoomGroupingMethod.BySpace(spaceSum))) if (spaceId != null) { @@ -91,8 +91,8 @@ class AppStateHandler @Inject constructor( fun setCurrentGroup(groupId: String?, session: Session? = null) { val uSession = session ?: activeSessionHolder.getSafeActiveSession() ?: return - if (selectedSpaceDataSource.currentValue?.orNull() is RoomGroupingMethod.ByLegacyGroup - && groupId == selectedSpaceDataSource.currentValue?.orNull()?.group()?.groupId) return + if (selectedSpaceDataSource.currentValue?.orNull() is RoomGroupingMethod.ByLegacyGroup && + groupId == selectedSpaceDataSource.currentValue?.orNull()?.group()?.groupId) return val activeGroup = groupId?.let { uSession.getGroupSummary(groupId) } selectedSpaceDataSource.post(Option.just(RoomGroupingMethod.ByLegacyGroup(activeGroup))) if (groupId != null) { diff --git a/vector/src/main/java/im/vector/app/VectorApplication.kt b/vector/src/main/java/im/vector/app/VectorApplication.kt index be8447d409..d9027231da 100644 --- a/vector/src/main/java/im/vector/app/VectorApplication.kt +++ b/vector/src/main/java/im/vector/app/VectorApplication.kt @@ -34,15 +34,15 @@ import androidx.lifecycle.ProcessLifecycleOwner import androidx.multidex.MultiDex import com.airbnb.epoxy.EpoxyAsyncUtil import com.airbnb.epoxy.EpoxyController +import com.airbnb.mvrx.Mavericks import com.facebook.stetho.Stetho import com.gabrielittner.threetenbp.LazyThreeTen import com.vanniktech.emoji.EmojiManager import com.vanniktech.emoji.google.GoogleEmojiProvider +import dagger.hilt.android.HiltAndroidApp import im.vector.app.core.di.ActiveSessionHolder -import im.vector.app.core.di.DaggerVectorComponent -import im.vector.app.core.di.HasVectorInjector -import im.vector.app.core.di.VectorComponent import im.vector.app.core.extensions.configureAndStart +import im.vector.app.core.extensions.startSyncing import im.vector.app.core.rx.RxConfig import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.configuration.VectorConfiguration @@ -53,6 +53,7 @@ import im.vector.app.features.notifications.NotificationDrawerManager import im.vector.app.features.notifications.NotificationUtils import im.vector.app.features.pin.PinLocker import im.vector.app.features.popup.PopupAlertManager +import im.vector.app.features.rageshake.VectorFileLogger import im.vector.app.features.rageshake.VectorUncaughtExceptionHandler import im.vector.app.features.room.VectorRoomDisplayNameFallbackProvider import im.vector.app.features.settings.VectorLocale @@ -73,9 +74,9 @@ import java.util.concurrent.Executors import javax.inject.Inject import androidx.work.Configuration as WorkConfiguration +@HiltAndroidApp class VectorApplication : Application(), - HasVectorInjector, MatrixConfiguration.Provider, WorkConfiguration.Provider { @@ -97,16 +98,15 @@ class VectorApplication : @Inject lateinit var pinLocker: PinLocker @Inject lateinit var callManager: WebRtcCallManager @Inject lateinit var invitesAcceptor: InvitesAcceptor - - lateinit var vectorComponent: VectorComponent + @Inject lateinit var vectorFileLogger: VectorFileLogger // font thread handler private var fontThreadHandler: Handler? = null private val powerKeyReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent) { - if (intent.action == Intent.ACTION_SCREEN_OFF - && vectorPreferences.useFlagPinCode()) { + if (intent.action == Intent.ACTION_SCREEN_OFF && + vectorPreferences.useFlagPinCode()) { pinLocker.screenIsOff() } } @@ -116,8 +116,6 @@ class VectorApplication : enableStrictModeIfNeeded() super.onCreate() appContext = this - vectorComponent = DaggerVectorComponent.factory().create(this) - vectorComponent.inject(this) invitesAcceptor.initialize() vectorUncaughtExceptionHandler.activate(this) rxConfig.setupRxPlugin() @@ -130,14 +128,14 @@ class VectorApplication : if (BuildConfig.DEBUG) { Timber.plant(Timber.DebugTree()) } - Timber.plant(vectorComponent.vectorFileLogger()) + Timber.plant(vectorFileLogger) if (BuildConfig.DEBUG) { Stetho.initializeWithDefaults(this) } logInfo() LazyThreeTen.init(this) - + Mavericks.initialize(debugMode = false) EpoxyController.defaultDiffingHandler = EpoxyAsyncUtil.getAsyncBackgroundHandler() EpoxyController.defaultModelBuildingHandler = EpoxyAsyncUtil.getAsyncBackgroundHandler() registerActivityLifecycleCallbacks(VectorActivityLifecycleCallbacks(popupAlertManager)) @@ -162,11 +160,15 @@ class VectorApplication : // Do not display the name change popup doNotShowDisclaimerDialog(this) } + if (authenticationService.hasAuthenticatedSessions() && !activeSessionHolder.hasActiveSession()) { val lastAuthenticatedSession = authenticationService.getLastAuthenticatedSession()!! activeSessionHolder.setActiveSession(lastAuthenticatedSession) - lastAuthenticatedSession.configureAndStart(applicationContext) + lastAuthenticatedSession.configureAndStart(applicationContext, startSyncing = false) } + + ProcessLifecycleOwner.get().lifecycle.addObserver(startSyncOnFirstStart) + ProcessLifecycleOwner.get().lifecycle.addObserver(object : LifecycleObserver { @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) fun entersForeground() { @@ -199,6 +201,15 @@ class VectorApplication : EmojiManager.install(GoogleEmojiProvider()) } + private val startSyncOnFirstStart = object : LifecycleObserver { + @OnLifecycleEvent(Lifecycle.Event.ON_START) + fun onStart() { + Timber.i("App process started") + authenticationService.getLastAuthenticatedSession()?.startSyncing(appContext) + ProcessLifecycleOwner.get().lifecycle.removeObserver(this) + } + } + private fun enableStrictModeIfNeeded() { if (BuildConfig.ENABLE_STRICT_MODE_LOGS) { StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.Builder() @@ -221,10 +232,6 @@ class VectorApplication : .build() } - override fun injector(): VectorComponent { - return vectorComponent - } - private fun logInfo() { val appVersion = versionProvider.getVersion(longFormat = true, useBuildNumber = true) val sdkVersion = Matrix.getSdkVersion() diff --git a/vector/src/main/java/im/vector/app/core/date/DefaultDateFormatterProvider.kt b/vector/src/main/java/im/vector/app/core/date/DefaultDateFormatterProvider.kt index c8736f3ab1..6371035a17 100644 --- a/vector/src/main/java/im/vector/app/core/date/DefaultDateFormatterProvider.kt +++ b/vector/src/main/java/im/vector/app/core/date/DefaultDateFormatterProvider.kt @@ -23,8 +23,8 @@ import org.threeten.bp.format.DateTimeFormatter import javax.inject.Inject class DefaultDateFormatterProvider @Inject constructor(private val context: Context, - private val localeProvider: LocaleProvider) - : DateFormatterProvider { + private val localeProvider: LocaleProvider) : + DateFormatterProvider { override val dateWithMonthFormatter: DateTimeFormatter by lazy { val pattern = DateFormat.getBestDateTimePattern(localeProvider.current(), "d MMMMM") diff --git a/vector/src/main/java/im/vector/app/core/di/ActivityEntryPoint.kt b/vector/src/main/java/im/vector/app/core/di/ActivityEntryPoint.kt new file mode 100644 index 0000000000..c5f7317ebe --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/di/ActivityEntryPoint.kt @@ -0,0 +1,30 @@ +/* + * 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.di + +import androidx.fragment.app.FragmentFactory +import androidx.lifecycle.ViewModelProvider +import dagger.hilt.EntryPoint +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ActivityComponent + +@InstallIn(ActivityComponent::class) +@EntryPoint +interface ActivityEntryPoint { + fun fragmentFactory(): FragmentFactory + fun viewModelFactory(): ViewModelProvider.Factory +} 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 99c94a7804..bf72dcb076 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 @@ -21,6 +21,8 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentFactory import dagger.Binds import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ActivityComponent import dagger.multibindings.IntoMap import im.vector.app.features.attachments.preview.AttachmentsPreviewFragment import im.vector.app.features.contactsbook.ContactsBookFragment @@ -76,7 +78,6 @@ import im.vector.app.features.login2.LoginFragmentSigninPassword2 import im.vector.app.features.login2.LoginFragmentSigninUsername2 import im.vector.app.features.login2.LoginFragmentSignupPassword2 import im.vector.app.features.login2.LoginFragmentSignupUsername2 -import im.vector.app.features.login2.created.AccountCreatedFragment import im.vector.app.features.login2.LoginFragmentToAny2 import im.vector.app.features.login2.LoginGenericTextInputFormFragment2 import im.vector.app.features.login2.LoginResetPasswordFragment2 @@ -88,6 +89,7 @@ import im.vector.app.features.login2.LoginSplashSignUpSignInSelectionFragment2 import im.vector.app.features.login2.LoginSsoOnlyFragment2 import im.vector.app.features.login2.LoginWaitForEmailFragment2 import im.vector.app.features.login2.LoginWebFragment2 +import im.vector.app.features.login2.created.AccountCreatedFragment import im.vector.app.features.login2.terms.LoginTermsFragment2 import im.vector.app.features.matrixto.MatrixToRoomSpaceFragment import im.vector.app.features.matrixto.MatrixToUserFragment @@ -114,12 +116,9 @@ import im.vector.app.features.roomprofile.settings.joinrule.RoomJoinRuleFragment import im.vector.app.features.roomprofile.uploads.RoomUploadsFragment import im.vector.app.features.roomprofile.uploads.files.RoomUploadsFilesFragment import im.vector.app.features.roomprofile.uploads.media.RoomUploadsMediaFragment -import im.vector.app.features.settings.notifications.VectorSettingsAdvancedNotificationPreferenceFragment import im.vector.app.features.settings.VectorSettingsGeneralFragment import im.vector.app.features.settings.VectorSettingsHelpAboutFragment import im.vector.app.features.settings.VectorSettingsLabsFragment -import im.vector.app.features.settings.notifications.VectorSettingsNotificationPreferenceFragment -import im.vector.app.features.settings.notifications.VectorSettingsNotificationsTroubleshootFragment import im.vector.app.features.settings.VectorSettingsPinFragment import im.vector.app.features.settings.VectorSettingsPreferencesFragment import im.vector.app.features.settings.VectorSettingsSecurityPrivacyFragment @@ -134,6 +133,9 @@ import im.vector.app.features.settings.devtools.OutgoingKeyRequestListFragment import im.vector.app.features.settings.homeserver.HomeserverSettingsFragment import im.vector.app.features.settings.ignored.VectorSettingsIgnoredUsersFragment import im.vector.app.features.settings.locale.LocalePickerFragment +import im.vector.app.features.settings.notifications.VectorSettingsAdvancedNotificationPreferenceFragment +import im.vector.app.features.settings.notifications.VectorSettingsNotificationPreferenceFragment +import im.vector.app.features.settings.notifications.VectorSettingsNotificationsTroubleshootFragment import im.vector.app.features.settings.push.PushGatewaysFragment import im.vector.app.features.settings.push.PushRulesFragment import im.vector.app.features.settings.threepids.ThreePidsSettingsFragment @@ -157,6 +159,7 @@ import im.vector.app.features.usercode.ShowUserCodeFragment import im.vector.app.features.userdirectory.UserListFragment import im.vector.app.features.widgets.WidgetFragment +@InstallIn(ActivityComponent::class) @Module interface FragmentModule { /** diff --git a/vector/src/main/java/im/vector/app/core/di/HiltMavericksViewModelFactory.kt b/vector/src/main/java/im/vector/app/core/di/HiltMavericksViewModelFactory.kt new file mode 100644 index 0000000000..13702053e8 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/di/HiltMavericksViewModelFactory.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.core.di + +import com.airbnb.mvrx.MavericksState +import com.airbnb.mvrx.MavericksViewModel +import com.airbnb.mvrx.MavericksViewModelFactory +import com.airbnb.mvrx.ViewModelContext +import dagger.hilt.DefineComponent +import dagger.hilt.EntryPoint +import dagger.hilt.EntryPoints +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +/** + * To connect Mavericks ViewModel creation with Hilt's dependency injection, add the following Factory and companion object to your MavericksViewModel. + * + * Example: + * + * class MyViewModel @AssistedInject constructor(...): MavericksViewModel(...) { + * + * @AssistedFactory + * interface Factory : AssistedViewModelFactory { + * ... + * } + * + * companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() + * } + */ + +inline fun , S : MavericksState> hiltMavericksViewModelFactory() = HiltMavericksViewModelFactory(VM::class.java) + +class HiltMavericksViewModelFactory, S : MavericksState>( + private val viewModelClass: Class> +) : MavericksViewModelFactory { + + override fun create(viewModelContext: ViewModelContext, state: S): VM { + // We want to create the ViewModelComponent. In order to do that, we need to get its parent: ActivityComponent. + val componentBuilder = EntryPoints.get(viewModelContext.app(), CreateMavericksViewModelComponent::class.java).mavericksViewModelComponentBuilder() + val viewModelComponent = componentBuilder.build() + val viewModelFactoryMap = EntryPoints.get(viewModelComponent, HiltMavericksEntryPoint::class.java).viewModelFactories + val viewModelFactory = viewModelFactoryMap[viewModelClass] + + @Suppress("UNCHECKED_CAST") + val castedViewModelFactory = viewModelFactory as? MavericksAssistedViewModelFactory + return castedViewModelFactory?.create(state) as VM + } + + override fun initialState(viewModelContext: ViewModelContext): S? { + return super.initialState(viewModelContext) + } +} + +/** + * Hilt's ViewModelComponent's parent is ActivityRetainedComponent but there is no easy way to access it. SingletonComponent should be sufficient + * because the ViewModel that gets created is the only object with a reference to the created component so the lifecycle of it will + * still be correct. + */ +@MavericksViewModelScoped +@DefineComponent(parent = SingletonComponent::class) +interface MavericksViewModelComponent + +@DefineComponent.Builder +interface MavericksViewModelComponentBuilder { + fun build(): MavericksViewModelComponent +} + +@EntryPoint +@InstallIn(SingletonComponent::class) +interface CreateMavericksViewModelComponent { + fun mavericksViewModelComponentBuilder(): MavericksViewModelComponentBuilder +} + +@EntryPoint +@InstallIn(MavericksViewModelComponent::class) +interface HiltMavericksEntryPoint { + val viewModelFactories: Map>, MavericksAssistedViewModelFactory<*, *>> +} diff --git a/vector/src/main/java/im/vector/app/core/di/HomeModule.kt b/vector/src/main/java/im/vector/app/core/di/HomeModule.kt new file mode 100644 index 0000000000..1af67cdc83 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/di/HomeModule.kt @@ -0,0 +1,36 @@ +/* + * 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.di + +import android.os.Handler +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ActivityComponent +import im.vector.app.features.home.room.detail.timeline.TimelineEventControllerHandler +import im.vector.app.features.home.room.detail.timeline.helper.TimelineAsyncHelper + +@Module +@InstallIn(ActivityComponent::class) +object HomeModule { + @Provides + @JvmStatic + @TimelineEventControllerHandler + fun providesTimelineBackgroundHandler(): Handler { + return TimelineAsyncHelper.getBackgroundHandler() + } +} diff --git a/vector/src/main/java/im/vector/app/core/di/MavericksAssistedViewModelFactory.kt b/vector/src/main/java/im/vector/app/core/di/MavericksAssistedViewModelFactory.kt new file mode 100644 index 0000000000..7cd6245d64 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/di/MavericksAssistedViewModelFactory.kt @@ -0,0 +1,40 @@ +/* + * 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.di + +import com.airbnb.mvrx.MavericksState +import com.airbnb.mvrx.MavericksViewModel + +/** + * This factory allows Mavericks to supply the initial or restored [MavericksState] to Hilt. + * + * Add this interface inside of your [MavericksViewModel] class then create the following Hilt module: + * + * @Module + * @InstallIn(MavericksViewModelComponent::class) + * interface ViewModelsModule { + * @Binds + * @IntoMap + * @ViewModelKey(MyViewModel::class) + * fun myViewModelFactory(factory: MyViewModel.Factory): AssistedViewModelFactory<*, *> + * } + * + * If you already have a ViewModelsModule then all you have to do is add the multibinding entry for your new [MavericksViewModel]. + */ +interface MavericksAssistedViewModelFactory, S : MavericksState> { + fun create(initialState: S): VM +} diff --git a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt new file mode 100644 index 0000000000..f1ff55f765 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt @@ -0,0 +1,549 @@ +/* + * Copyright 2019 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.di + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.multibindings.IntoMap +import im.vector.app.features.auth.ReAuthViewModel +import im.vector.app.features.call.VectorCallViewModel +import im.vector.app.features.call.conference.JitsiCallViewModel +import im.vector.app.features.call.transfer.CallTransferViewModel +import im.vector.app.features.contactsbook.ContactsBookViewModel +import im.vector.app.features.createdirect.CreateDirectRoomViewModel +import im.vector.app.features.crypto.keysbackup.settings.KeysBackupSettingsViewModel +import im.vector.app.features.crypto.quads.SharedSecureStorageViewModel +import im.vector.app.features.crypto.recover.BootstrapSharedViewModel +import im.vector.app.features.crypto.verification.VerificationBottomSheetViewModel +import im.vector.app.features.crypto.verification.choose.VerificationChooseMethodViewModel +import im.vector.app.features.crypto.verification.emoji.VerificationEmojiCodeViewModel +import im.vector.app.features.devtools.RoomDevToolViewModel +import im.vector.app.features.discovery.DiscoverySettingsViewModel +import im.vector.app.features.discovery.change.SetIdentityServerViewModel +import im.vector.app.features.home.HomeActivityViewModel +import im.vector.app.features.home.HomeDetailViewModel +import im.vector.app.features.home.PromoteRestrictedViewModel +import im.vector.app.features.home.UnknownDeviceDetectorSharedViewModel +import im.vector.app.features.home.UnreadMessagesSharedViewModel +import im.vector.app.features.home.room.breadcrumbs.BreadcrumbsViewModel +import im.vector.app.features.home.room.detail.composer.TextComposerViewModel +import im.vector.app.features.home.room.detail.search.SearchViewModel +import im.vector.app.features.home.room.detail.timeline.action.MessageActionsViewModel +import im.vector.app.features.home.room.detail.timeline.edithistory.ViewEditHistoryViewModel +import im.vector.app.features.home.room.detail.timeline.reactions.ViewReactionsViewModel +import im.vector.app.features.home.room.detail.upgrade.MigrateRoomViewModel +import im.vector.app.features.home.room.list.RoomListViewModel +import im.vector.app.features.homeserver.HomeServerCapabilitiesViewModel +import im.vector.app.features.invite.InviteUsersToRoomViewModel +import im.vector.app.features.login.LoginViewModel +import im.vector.app.features.login2.LoginViewModel2 +import im.vector.app.features.login2.created.AccountCreatedViewModel +import im.vector.app.features.matrixto.MatrixToBottomSheetViewModel +import im.vector.app.features.rageshake.BugReportViewModel +import im.vector.app.features.reactions.EmojiSearchResultViewModel +import im.vector.app.features.room.RequireActiveMembershipViewModel +import im.vector.app.features.roomdirectory.RoomDirectoryViewModel +import im.vector.app.features.roomdirectory.createroom.CreateRoomViewModel +import im.vector.app.features.roomdirectory.picker.RoomDirectoryPickerViewModel +import im.vector.app.features.roomdirectory.roompreview.RoomPreviewViewModel +import im.vector.app.features.roommemberprofile.RoomMemberProfileViewModel +import im.vector.app.features.roommemberprofile.devices.DeviceListBottomSheetViewModel +import im.vector.app.features.roomprofile.RoomProfileViewModel +import im.vector.app.features.roomprofile.alias.RoomAliasViewModel +import im.vector.app.features.roomprofile.alias.detail.RoomAliasBottomSheetViewModel +import im.vector.app.features.roomprofile.banned.RoomBannedMemberListViewModel +import im.vector.app.features.roomprofile.members.RoomMemberListViewModel +import im.vector.app.features.roomprofile.notifications.RoomNotificationSettingsViewModel +import im.vector.app.features.roomprofile.permissions.RoomPermissionsViewModel +import im.vector.app.features.roomprofile.settings.RoomSettingsViewModel +import im.vector.app.features.roomprofile.settings.joinrule.advanced.RoomJoinRuleChooseRestrictedViewModel +import im.vector.app.features.roomprofile.uploads.RoomUploadsViewModel +import im.vector.app.features.settings.account.deactivation.DeactivateAccountViewModel +import im.vector.app.features.settings.crosssigning.CrossSigningSettingsViewModel +import im.vector.app.features.settings.devices.DeviceVerificationInfoBottomSheetViewModel +import im.vector.app.features.settings.devices.DevicesViewModel +import im.vector.app.features.settings.devtools.AccountDataViewModel +import im.vector.app.features.settings.devtools.GossipingEventsPaperTrailViewModel +import im.vector.app.features.settings.devtools.KeyRequestListViewModel +import im.vector.app.features.settings.devtools.KeyRequestViewModel +import im.vector.app.features.settings.homeserver.HomeserverSettingsViewModel +import im.vector.app.features.settings.ignored.IgnoredUsersViewModel +import im.vector.app.features.settings.locale.LocalePickerViewModel +import im.vector.app.features.settings.push.PushGatewaysViewModel +import im.vector.app.features.settings.threepids.ThreePidsSettingsViewModel +import im.vector.app.features.share.IncomingShareViewModel +import im.vector.app.features.signout.soft.SoftLogoutViewModel +import im.vector.app.features.spaces.SpaceListViewModel +import im.vector.app.features.spaces.SpaceMenuViewModel +import im.vector.app.features.spaces.create.CreateSpaceViewModel +import im.vector.app.features.spaces.explore.SpaceDirectoryViewModel +import im.vector.app.features.spaces.invite.SpaceInviteBottomSheetViewModel +import im.vector.app.features.spaces.leave.SpaceLeaveAdvancedViewModel +import im.vector.app.features.spaces.manage.SpaceAddRoomsViewModel +import im.vector.app.features.spaces.manage.SpaceManageRoomsViewModel +import im.vector.app.features.spaces.manage.SpaceManageSharedViewModel +import im.vector.app.features.spaces.people.SpacePeopleViewModel +import im.vector.app.features.spaces.preview.SpacePreviewViewModel +import im.vector.app.features.spaces.share.ShareSpaceViewModel +import im.vector.app.features.terms.ReviewTermsViewModel +import im.vector.app.features.usercode.UserCodeSharedViewModel +import im.vector.app.features.userdirectory.UserListViewModel +import im.vector.app.features.widgets.WidgetViewModel +import im.vector.app.features.widgets.permissions.RoomWidgetPermissionViewModel +import im.vector.app.features.workers.signout.ServerBackupStatusViewModel +import im.vector.app.features.workers.signout.SignoutCheckViewModel + +@InstallIn(MavericksViewModelComponent::class) +@Module +interface MavericksViewModelModule { + + @Binds + @IntoMap + @MavericksViewModelKey(RoomListViewModel::class) + fun roomListViewModelFactory(factory: RoomListViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(SpaceManageRoomsViewModel::class) + fun spaceManageRoomsViewModelFactory(factory: SpaceManageRoomsViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(SpaceManageSharedViewModel::class) + fun spaceManageSharedViewModelFactory(factory: SpaceManageSharedViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(SpaceListViewModel::class) + fun spaceListViewModelFactory(factory: SpaceListViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(ReAuthViewModel::class) + fun reAuthViewModelFactory(factory: ReAuthViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(VectorCallViewModel::class) + fun vectorCallViewModelFactory(factory: VectorCallViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(JitsiCallViewModel::class) + fun jitsiCallViewModelFactory(factory: JitsiCallViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(RoomDirectoryViewModel::class) + fun roomDirectoryViewModelFactory(factory: RoomDirectoryViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(ViewReactionsViewModel::class) + fun viewReactionsViewModelFactory(factory: ViewReactionsViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(RoomWidgetPermissionViewModel::class) + fun roomWidgetPermissionViewModelFactory(factory: RoomWidgetPermissionViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(WidgetViewModel::class) + fun widgetViewModelFactory(factory: WidgetViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(ServerBackupStatusViewModel::class) + fun serverBackupStatusViewModelFactory(factory: ServerBackupStatusViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(SignoutCheckViewModel::class) + fun signoutCheckViewModelFactory(factory: SignoutCheckViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(RoomDirectoryPickerViewModel::class) + fun roomDirectoryPickerViewModelFactory(factory: RoomDirectoryPickerViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(RoomDevToolViewModel::class) + fun roomDevToolViewModelFactory(factory: RoomDevToolViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(MigrateRoomViewModel::class) + fun migrateRoomViewModelFactory(factory: MigrateRoomViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(IgnoredUsersViewModel::class) + fun ignoredUsersViewModelFactory(factory: IgnoredUsersViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(CallTransferViewModel::class) + fun callTransferViewModelFactory(factory: CallTransferViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(ContactsBookViewModel::class) + fun contactsBookViewModelFactory(factory: ContactsBookViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(CreateDirectRoomViewModel::class) + fun createDirectRoomViewModelFactory(factory: CreateDirectRoomViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(RoomNotificationSettingsViewModel::class) + fun roomNotificationSettingsViewModelFactory(factory: RoomNotificationSettingsViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(KeysBackupSettingsViewModel::class) + fun keysBackupSettingsViewModelFactory(factory: KeysBackupSettingsViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(SharedSecureStorageViewModel::class) + fun sharedSecureStorageViewModelFactory(factory: SharedSecureStorageViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(PromoteRestrictedViewModel::class) + fun promoteRestrictedViewModelFactory(factory: PromoteRestrictedViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(UserListViewModel::class) + fun userListViewModelFactory(factory: UserListViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(UserCodeSharedViewModel::class) + fun userCodeSharedViewModelFactory(factory: UserCodeSharedViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(ReviewTermsViewModel::class) + fun reviewTermsViewModelFactory(factory: ReviewTermsViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(ShareSpaceViewModel::class) + fun shareSpaceViewModelFactory(factory: ShareSpaceViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(SpacePreviewViewModel::class) + fun spacePreviewViewModelFactory(factory: SpacePreviewViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(SpacePeopleViewModel::class) + fun spacePeopleViewModelFactory(factory: SpacePeopleViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(SpaceAddRoomsViewModel::class) + fun spaceAddRoomsViewModelFactory(factory: SpaceAddRoomsViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(SpaceLeaveAdvancedViewModel::class) + fun spaceLeaveAdvancedViewModelFactory(factory: SpaceLeaveAdvancedViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(SpaceInviteBottomSheetViewModel::class) + fun spaceInviteBottomSheetViewModelFactory(factory: SpaceInviteBottomSheetViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(SpaceDirectoryViewModel::class) + fun spaceDirectoryViewModelFactory(factory: SpaceDirectoryViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(CreateSpaceViewModel::class) + fun createSpaceViewModelFactory(factory: CreateSpaceViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(SpaceMenuViewModel::class) + fun spaceMenuViewModelFactory(factory: SpaceMenuViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(SoftLogoutViewModel::class) + fun softLogoutViewModelFactory(factory: SoftLogoutViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(IncomingShareViewModel::class) + fun incomingShareViewModelFactory(factory: IncomingShareViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(ThreePidsSettingsViewModel::class) + fun threePidsSettingsViewModelFactory(factory: ThreePidsSettingsViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(PushGatewaysViewModel::class) + fun pushGatewaysViewModelFactory(factory: PushGatewaysViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(HomeserverSettingsViewModel::class) + fun homeserverSettingsViewModelFactory(factory: HomeserverSettingsViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(LocalePickerViewModel::class) + fun localePickerViewModelFactory(factory: LocalePickerViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(GossipingEventsPaperTrailViewModel::class) + fun gossipingEventsPaperTrailViewModelFactory(factory: GossipingEventsPaperTrailViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(AccountDataViewModel::class) + fun accountDataViewModelFactory(factory: AccountDataViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(DevicesViewModel::class) + fun devicesViewModelFactory(factory: DevicesViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(KeyRequestListViewModel::class) + fun keyRequestListViewModelFactory(factory: KeyRequestListViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(KeyRequestViewModel::class) + fun keyRequestViewModelFactory(factory: KeyRequestViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(CrossSigningSettingsViewModel::class) + fun crossSigningSettingsViewModelFactory(factory: CrossSigningSettingsViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(DeactivateAccountViewModel::class) + fun deactivateAccountViewModelFactory(factory: DeactivateAccountViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(RoomUploadsViewModel::class) + fun roomUploadsViewModelFactory(factory: RoomUploadsViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(RoomJoinRuleChooseRestrictedViewModel::class) + fun roomJoinRuleChooseRestrictedViewModelFactory(factory: RoomJoinRuleChooseRestrictedViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(RoomSettingsViewModel::class) + fun roomSettingsViewModelFactory(factory: RoomSettingsViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(RoomPermissionsViewModel::class) + fun roomPermissionsViewModelFactory(factory: RoomPermissionsViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(RoomMemberListViewModel::class) + fun roomMemberListViewModelFactory(factory: RoomMemberListViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(RoomBannedMemberListViewModel::class) + fun roomBannedMemberListViewModelFactory(factory: RoomBannedMemberListViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(RoomAliasViewModel::class) + fun roomAliasViewModelFactory(factory: RoomAliasViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(RoomAliasBottomSheetViewModel::class) + fun roomAliasBottomSheetViewModelFactory(factory: RoomAliasBottomSheetViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(RoomProfileViewModel::class) + fun roomProfileViewModelFactory(factory: RoomProfileViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(RoomMemberProfileViewModel::class) + fun roomMemberProfileViewModelFactory(factory: RoomMemberProfileViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(RoomPreviewViewModel::class) + fun roomPreviewViewModelFactory(factory: RoomPreviewViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(CreateRoomViewModel::class) + fun createRoomViewModelFactory(factory: CreateRoomViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(RequireActiveMembershipViewModel::class) + fun requireActiveMembershipViewModelFactory(factory: RequireActiveMembershipViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(EmojiSearchResultViewModel::class) + fun emojiSearchResultViewModelFactory(factory: EmojiSearchResultViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(BugReportViewModel::class) + fun bugReportViewModelFactory(factory: BugReportViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(MatrixToBottomSheetViewModel::class) + fun matrixToBottomSheetViewModelFactory(factory: MatrixToBottomSheetViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(AccountCreatedViewModel::class) + fun accountCreatedViewModelFactory(factory: AccountCreatedViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(LoginViewModel2::class) + fun loginViewModel2Factory(factory: LoginViewModel2.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(LoginViewModel::class) + fun loginViewModelFactory(factory: LoginViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(HomeServerCapabilitiesViewModel::class) + fun homeServerCapabilitiesViewModelFactory(factory: HomeServerCapabilitiesViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(InviteUsersToRoomViewModel::class) + fun inviteUsersToRoomViewModelFactory(factory: InviteUsersToRoomViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(ViewEditHistoryViewModel::class) + fun viewEditHistoryViewModelFactory(factory: ViewEditHistoryViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(MessageActionsViewModel::class) + fun messageActionsViewModelFactory(factory: MessageActionsViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(VerificationChooseMethodViewModel::class) + fun verificationChooseMethodViewModelFactory(factory: VerificationChooseMethodViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(VerificationEmojiCodeViewModel::class) + fun verificationEmojiCodeViewModelFactory(factory: VerificationEmojiCodeViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(SearchViewModel::class) + fun searchViewModelFactory(factory: SearchViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(UnreadMessagesSharedViewModel::class) + fun unreadMessagesSharedViewModelFactory(factory: UnreadMessagesSharedViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(UnknownDeviceDetectorSharedViewModel::class) + fun unknownDeviceDetectorSharedViewModelFactory(factory: UnknownDeviceDetectorSharedViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(DiscoverySettingsViewModel::class) + fun discoverySettingsViewModelFactory(factory: DiscoverySettingsViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(TextComposerViewModel::class) + fun textComposerViewModelFactory(factory: TextComposerViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(SetIdentityServerViewModel::class) + fun setIdentityServerViewModelFactory(factory: SetIdentityServerViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(BreadcrumbsViewModel::class) + fun breadcrumbsViewModelFactory(factory: BreadcrumbsViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(HomeDetailViewModel::class) + fun homeDetailViewModelFactory(factory: HomeDetailViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(DeviceVerificationInfoBottomSheetViewModel::class) + fun deviceVerificationInfoBottomSheetViewModelFactory(factory: DeviceVerificationInfoBottomSheetViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(DeviceListBottomSheetViewModel::class) + fun deviceListBottomSheetViewModelFactory(factory: DeviceListBottomSheetViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(HomeActivityViewModel::class) + fun homeActivityViewModelFactory(factory: HomeActivityViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(BootstrapSharedViewModel::class) + fun bootstrapSharedViewModelFactory(factory: BootstrapSharedViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(VerificationBottomSheetViewModel::class) + fun verificationBottomSheetViewModelFactory(factory: VerificationBottomSheetViewModel.Factory): MavericksAssistedViewModelFactory<*, *> +} diff --git a/vector/src/main/java/im/vector/app/core/di/HasVectorInjector.kt b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelScoped.kt similarity index 74% rename from vector/src/main/java/im/vector/app/core/di/HasVectorInjector.kt rename to vector/src/main/java/im/vector/app/core/di/MavericksViewModelScoped.kt index 79254defcc..58b9246fe5 100644 --- a/vector/src/main/java/im/vector/app/core/di/HasVectorInjector.kt +++ b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelScoped.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019 New Vector Ltd + * 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. @@ -16,7 +16,10 @@ package im.vector.app.core.di -interface HasVectorInjector { +import javax.inject.Scope - fun injector(): VectorComponent -} +/** + * Scope annotation for bindings that should exist for the life of an MavericksViewModel. + */ +@Scope +annotation class MavericksViewModelScoped diff --git a/vector/src/main/java/im/vector/app/core/di/ScreenComponent.kt b/vector/src/main/java/im/vector/app/core/di/ScreenComponent.kt deleted file mode 100644 index 40ba57103b..0000000000 --- a/vector/src/main/java/im/vector/app/core/di/ScreenComponent.kt +++ /dev/null @@ -1,225 +0,0 @@ -/* - * Copyright 2019 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.di - -import androidx.appcompat.app.AppCompatActivity -import androidx.fragment.app.FragmentFactory -import androidx.lifecycle.ViewModelProvider -import dagger.BindsInstance -import dagger.Component -import im.vector.app.core.dialogs.UnrecognizedCertificateDialog -import im.vector.app.core.error.ErrorFormatter -import im.vector.app.core.preference.UserAvatarPreference -import im.vector.app.features.MainActivity -import im.vector.app.features.auth.ReAuthActivity -import im.vector.app.features.call.CallControlsBottomSheet -import im.vector.app.features.call.VectorCallActivity -import im.vector.app.features.call.conference.VectorJitsiActivity -import im.vector.app.features.call.transfer.CallTransferActivity -import im.vector.app.features.createdirect.CreateDirectRoomActivity -import im.vector.app.features.crypto.keysbackup.settings.KeysBackupManageActivity -import im.vector.app.features.crypto.keysbackup.setup.KeysBackupSetupActivity -import im.vector.app.features.crypto.quads.SharedSecureStorageActivity -import im.vector.app.features.crypto.recover.BootstrapBottomSheet -import im.vector.app.features.crypto.verification.VerificationBottomSheet -import im.vector.app.features.debug.DebugMenuActivity -import im.vector.app.features.devtools.RoomDevToolActivity -import im.vector.app.features.home.HomeActivity -import im.vector.app.features.home.HomeModule -import im.vector.app.features.home.room.detail.JoinReplacementRoomBottomSheet -import im.vector.app.features.home.room.detail.RoomDetailActivity -import im.vector.app.features.home.room.detail.readreceipts.DisplayReadReceiptsBottomSheet -import im.vector.app.features.home.room.detail.search.SearchActivity -import im.vector.app.features.home.room.detail.timeline.action.MessageActionsBottomSheet -import im.vector.app.features.home.room.detail.timeline.edithistory.ViewEditHistoryBottomSheet -import im.vector.app.features.home.room.detail.timeline.reactions.ViewReactionsBottomSheet -import im.vector.app.features.home.room.detail.upgrade.MigrateRoomBottomSheet -import im.vector.app.features.home.room.detail.widget.RoomWidgetsBottomSheet -import im.vector.app.features.home.room.filtered.FilteredRoomsActivity -import im.vector.app.features.home.room.list.RoomListModule -import im.vector.app.features.home.room.list.actions.RoomListQuickActionsBottomSheet -import im.vector.app.features.invite.AutoAcceptInvites -import im.vector.app.features.invite.InviteUsersToRoomActivity -import im.vector.app.features.invite.VectorInviteView -import im.vector.app.features.link.LinkHandlerActivity -import im.vector.app.features.login.LoginActivity -import im.vector.app.features.login2.LoginActivity2 -import im.vector.app.features.matrixto.MatrixToBottomSheet -import im.vector.app.features.media.BigImageViewerActivity -import im.vector.app.features.media.VectorAttachmentViewerActivity -import im.vector.app.features.navigation.Navigator -import im.vector.app.features.permalink.PermalinkHandlerActivity -import im.vector.app.features.pin.PinLocker -import im.vector.app.features.qrcode.QrCodeScannerActivity -import im.vector.app.features.rageshake.BugReportActivity -import im.vector.app.features.rageshake.BugReporter -import im.vector.app.features.rageshake.RageShake -import im.vector.app.features.reactions.EmojiReactionPickerActivity -import im.vector.app.features.reactions.widget.ReactionButton -import im.vector.app.features.roomdirectory.RoomDirectoryActivity -import im.vector.app.features.roomdirectory.createroom.CreateRoomActivity -import im.vector.app.features.roommemberprofile.RoomMemberProfileActivity -import im.vector.app.features.roommemberprofile.devices.DeviceListBottomSheet -import im.vector.app.features.roomprofile.RoomProfileActivity -import im.vector.app.features.roomprofile.alias.detail.RoomAliasBottomSheet -import im.vector.app.features.roomprofile.settings.historyvisibility.RoomHistoryVisibilityBottomSheet -import im.vector.app.features.roomprofile.settings.joinrule.RoomJoinRuleActivity -import im.vector.app.features.roomprofile.settings.joinrule.RoomJoinRuleBottomSheet -import im.vector.app.features.settings.VectorSettingsActivity -import im.vector.app.features.settings.devices.DeviceVerificationInfoBottomSheet -import im.vector.app.features.share.IncomingShareActivity -import im.vector.app.features.signout.soft.SoftLogoutActivity -import im.vector.app.features.spaces.InviteRoomSpaceChooserBottomSheet -import im.vector.app.features.spaces.LeaveSpaceBottomSheet -import im.vector.app.features.spaces.SpaceCreationActivity -import im.vector.app.features.spaces.SpaceExploreActivity -import im.vector.app.features.spaces.SpaceSettingsMenuBottomSheet -import im.vector.app.features.spaces.invite.SpaceInviteBottomSheet -import im.vector.app.features.spaces.leave.SpaceLeaveAdvancedActivity -import im.vector.app.features.spaces.manage.SpaceManageActivity -import im.vector.app.features.spaces.share.ShareSpaceBottomSheet -import im.vector.app.features.terms.ReviewTermsActivity -import im.vector.app.features.ui.UiStateRepository -import im.vector.app.features.usercode.UserCodeActivity -import im.vector.app.features.widgets.WidgetActivity -import im.vector.app.features.widgets.permissions.RoomWidgetPermissionBottomSheet -import im.vector.app.features.workers.signout.SignOutBottomSheetDialogFragment - -@Component( - dependencies = [ - VectorComponent::class - ], - modules = [ - ViewModelModule::class, - FragmentModule::class, - HomeModule::class, - RoomListModule::class, - ScreenModule::class - ] -) -@ScreenScope -interface ScreenComponent { - - /* ========================================================================================== - * Shortcut to VectorComponent elements - * ========================================================================================== */ - - fun activeSessionHolder(): ActiveSessionHolder - fun fragmentFactory(): FragmentFactory - fun viewModelFactory(): ViewModelProvider.Factory - fun bugReporter(): BugReporter - fun rageShake(): RageShake - fun navigator(): Navigator - fun pinLocker(): PinLocker - fun errorFormatter(): ErrorFormatter - fun uiStateRepository(): UiStateRepository - fun unrecognizedCertificateDialog(): UnrecognizedCertificateDialog - fun autoAcceptInvites(): AutoAcceptInvites - - /* ========================================================================================== - * Activities - * ========================================================================================== */ - - fun inject(activity: HomeActivity) - fun inject(activity: RoomDetailActivity) - fun inject(activity: RoomProfileActivity) - fun inject(activity: RoomMemberProfileActivity) - fun inject(activity: VectorSettingsActivity) - fun inject(activity: KeysBackupManageActivity) - fun inject(activity: EmojiReactionPickerActivity) - fun inject(activity: LoginActivity) - fun inject(activity: LoginActivity2) - fun inject(activity: LinkHandlerActivity) - fun inject(activity: MainActivity) - fun inject(activity: RoomDirectoryActivity) - fun inject(activity: KeysBackupSetupActivity) - fun inject(activity: BugReportActivity) - fun inject(activity: FilteredRoomsActivity) - fun inject(activity: CreateRoomActivity) - fun inject(activity: CreateDirectRoomActivity) - fun inject(activity: IncomingShareActivity) - fun inject(activity: SoftLogoutActivity) - fun inject(activity: PermalinkHandlerActivity) - fun inject(activity: QrCodeScannerActivity) - fun inject(activity: DebugMenuActivity) - fun inject(activity: SharedSecureStorageActivity) - fun inject(activity: BigImageViewerActivity) - fun inject(activity: InviteUsersToRoomActivity) - fun inject(activity: ReviewTermsActivity) - fun inject(activity: WidgetActivity) - fun inject(activity: VectorCallActivity) - fun inject(activity: VectorAttachmentViewerActivity) - fun inject(activity: VectorJitsiActivity) - fun inject(activity: SearchActivity) - fun inject(activity: UserCodeActivity) - fun inject(activity: CallTransferActivity) - fun inject(activity: ReAuthActivity) - fun inject(activity: RoomDevToolActivity) - fun inject(activity: SpaceCreationActivity) - fun inject(activity: SpaceExploreActivity) - fun inject(activity: SpaceManageActivity) - fun inject(activity: RoomJoinRuleActivity) - fun inject(activity: SpaceLeaveAdvancedActivity) - - /* ========================================================================================== - * BottomSheets - * ========================================================================================== */ - - fun inject(bottomSheet: MessageActionsBottomSheet) - fun inject(bottomSheet: ViewReactionsBottomSheet) - fun inject(bottomSheet: ViewEditHistoryBottomSheet) - fun inject(bottomSheet: DisplayReadReceiptsBottomSheet) - fun inject(bottomSheet: RoomListQuickActionsBottomSheet) - fun inject(bottomSheet: RoomAliasBottomSheet) - fun inject(bottomSheet: RoomHistoryVisibilityBottomSheet) - fun inject(bottomSheet: RoomJoinRuleBottomSheet) - fun inject(bottomSheet: VerificationBottomSheet) - fun inject(bottomSheet: DeviceVerificationInfoBottomSheet) - fun inject(bottomSheet: DeviceListBottomSheet) - fun inject(bottomSheet: BootstrapBottomSheet) - fun inject(bottomSheet: RoomWidgetPermissionBottomSheet) - fun inject(bottomSheet: RoomWidgetsBottomSheet) - fun inject(bottomSheet: CallControlsBottomSheet) - fun inject(bottomSheet: SignOutBottomSheetDialogFragment) - fun inject(bottomSheet: MatrixToBottomSheet) - fun inject(bottomSheet: ShareSpaceBottomSheet) - fun inject(bottomSheet: SpaceSettingsMenuBottomSheet) - fun inject(bottomSheet: InviteRoomSpaceChooserBottomSheet) - fun inject(bottomSheet: SpaceInviteBottomSheet) - fun inject(bottomSheet: JoinReplacementRoomBottomSheet) - fun inject(bottomSheet: MigrateRoomBottomSheet) - fun inject(bottomSheet: LeaveSpaceBottomSheet) - - /* ========================================================================================== - * Others - * ========================================================================================== */ - - fun inject(view: VectorInviteView) - fun inject(preference: UserAvatarPreference) - fun inject(button: ReactionButton) - - /* ========================================================================================== - * Factory - * ========================================================================================== */ - - @Component.Factory - interface Factory { - fun create(vectorComponent: VectorComponent, - @BindsInstance context: AppCompatActivity - ): ScreenComponent - } -} diff --git a/vector/src/main/java/im/vector/app/core/di/ScreenModule.kt b/vector/src/main/java/im/vector/app/core/di/ScreenModule.kt index 5f50f186d0..2dab05378c 100644 --- a/vector/src/main/java/im/vector/app/core/di/ScreenModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/ScreenModule.kt @@ -1,11 +1,11 @@ /* - * Copyright 2019 New Vector Ltd + * 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 + * 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, @@ -20,9 +20,13 @@ import androidx.appcompat.app.AppCompatActivity import androidx.recyclerview.widget.RecyclerView import dagger.Module import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ActivityComponent +import dagger.hilt.android.scopes.ActivityScoped import im.vector.app.core.glide.GlideApp @Module +@InstallIn(ActivityComponent::class) object ScreenModule { @Provides @@ -31,6 +35,6 @@ object ScreenModule { @Provides @JvmStatic - @ScreenScope + @ActivityScoped fun providesSharedViewPool() = RecyclerView.RecycledViewPool() } diff --git a/vector/src/main/java/im/vector/app/core/di/SingletonEntryPoint.kt b/vector/src/main/java/im/vector/app/core/di/SingletonEntryPoint.kt new file mode 100644 index 0000000000..52316751e6 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/di/SingletonEntryPoint.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.core.di + +import dagger.hilt.EntryPoint +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import im.vector.app.core.dialogs.UnrecognizedCertificateDialog +import im.vector.app.core.error.ErrorFormatter +import im.vector.app.features.call.webrtc.WebRtcCallManager +import im.vector.app.features.home.AvatarRenderer +import im.vector.app.features.navigation.Navigator +import im.vector.app.features.pin.PinLocker +import im.vector.app.features.rageshake.BugReporter +import im.vector.app.features.session.SessionListener +import im.vector.app.features.settings.VectorPreferences +import im.vector.app.features.ui.UiStateRepository +import kotlinx.coroutines.CoroutineScope + +@InstallIn(SingletonComponent::class) +@EntryPoint +interface SingletonEntryPoint { + + fun sessionListener(): SessionListener + + fun avatarRenderer(): AvatarRenderer + + fun activeSessionHolder(): ActiveSessionHolder + + fun unrecognizedCertificateDialog(): UnrecognizedCertificateDialog + + fun navigator(): Navigator + + fun errorFormatter(): ErrorFormatter + + fun bugReporter(): BugReporter + + fun vectorPreferences(): VectorPreferences + + fun uiStateRepository(): UiStateRepository + + fun pinLocker(): PinLocker + + fun webRtcCallManager(): WebRtcCallManager + + fun appCoroutineScope(): CoroutineScope +} diff --git a/vector/src/main/java/im/vector/app/core/di/VectorModule.kt b/vector/src/main/java/im/vector/app/core/di/SingletonModule.kt similarity index 50% rename from vector/src/main/java/im/vector/app/core/di/VectorModule.kt rename to vector/src/main/java/im/vector/app/core/di/SingletonModule.kt index 006a2f5aa0..e89a060022 100644 --- a/vector/src/main/java/im/vector/app/core/di/VectorModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/SingletonModule.kt @@ -16,6 +16,7 @@ package im.vector.app.core.di +import android.app.Application import android.content.Context import android.content.Context.MODE_PRIVATE import android.content.SharedPreferences @@ -23,6 +24,9 @@ import android.content.res.Resources import dagger.Binds import dagger.Module import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import im.vector.app.core.dispatchers.CoroutineDispatchers import im.vector.app.core.error.DefaultErrorFormatter import im.vector.app.core.error.ErrorFormatter import im.vector.app.features.invite.AutoAcceptInvites @@ -33,68 +37,20 @@ import im.vector.app.features.pin.PinCodeStore import im.vector.app.features.pin.SharedPrefPinCodeStore import im.vector.app.features.ui.SharedPreferencesUiStateRepository import im.vector.app.features.ui.UiStateRepository +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob import org.matrix.android.sdk.api.Matrix import org.matrix.android.sdk.api.auth.AuthenticationService import org.matrix.android.sdk.api.auth.HomeServerHistoryService import org.matrix.android.sdk.api.legacy.LegacySessionImporter import org.matrix.android.sdk.api.raw.RawService import org.matrix.android.sdk.api.session.Session +import javax.inject.Singleton +@InstallIn(SingletonComponent::class) @Module -abstract class VectorModule { - - @Module - companion object { - - @Provides - @JvmStatic - fun providesResources(context: Context): Resources { - return context.resources - } - - @Provides - @JvmStatic - fun providesSharedPreferences(context: Context): SharedPreferences { - return context.getSharedPreferences("im.vector.riot", MODE_PRIVATE) - } - - @Provides - @JvmStatic - fun providesMatrix(context: Context): Matrix { - return Matrix.getInstance(context) - } - - @Provides - @JvmStatic - fun providesCurrentSession(activeSessionHolder: ActiveSessionHolder): Session { - // TODO: handle session injection better - return activeSessionHolder.getActiveSession() - } - - @Provides - @JvmStatic - fun providesLegacySessionImporter(matrix: Matrix): LegacySessionImporter { - return matrix.legacySessionImporter() - } - - @Provides - @JvmStatic - fun providesAuthenticationService(matrix: Matrix): AuthenticationService { - return matrix.authenticationService() - } - - @Provides - @JvmStatic - fun providesRawService(matrix: Matrix): RawService { - return matrix.rawService() - } - - @Provides - @JvmStatic - fun providesHomeServerHistoryService(matrix: Matrix): HomeServerHistoryService { - return matrix.homeServerHistoryService() - } - } +abstract class VectorBindModule { @Binds abstract fun bindNavigator(navigator: DefaultNavigator): Navigator @@ -111,3 +67,76 @@ abstract class VectorModule { @Binds abstract fun bindAutoAcceptInvites(autoAcceptInvites: CompileTimeAutoAcceptInvites): AutoAcceptInvites } + +@InstallIn(SingletonComponent::class) +@Module +object VectorStaticModule { + + @Provides + @JvmStatic + fun providesContext(application: Application): Context { + return application.applicationContext + } + + @Provides + @JvmStatic + fun providesResources(context: Context): Resources { + return context.resources + } + + @Provides + @JvmStatic + fun providesSharedPreferences(context: Context): SharedPreferences { + return context.getSharedPreferences("im.vector.riot", MODE_PRIVATE) + } + + @Provides + @JvmStatic + fun providesMatrix(context: Context): Matrix { + return Matrix.getInstance(context) + } + + @Provides + @JvmStatic + fun providesCurrentSession(activeSessionHolder: ActiveSessionHolder): Session { + // TODO: handle session injection better + return activeSessionHolder.getActiveSession() + } + + @Provides + @JvmStatic + fun providesLegacySessionImporter(matrix: Matrix): LegacySessionImporter { + return matrix.legacySessionImporter() + } + + @Provides + @JvmStatic + fun providesAuthenticationService(matrix: Matrix): AuthenticationService { + return matrix.authenticationService() + } + + @Provides + @JvmStatic + fun providesRawService(matrix: Matrix): RawService { + return matrix.rawService() + } + + @Provides + @JvmStatic + fun providesHomeServerHistoryService(matrix: Matrix): HomeServerHistoryService { + return matrix.homeServerHistoryService() + } + + @Provides + @JvmStatic + @Singleton + fun providesApplicationCoroutineScope(): CoroutineScope { + return CoroutineScope(SupervisorJob() + Dispatchers.Main) + } + + @Provides + @JvmStatic + fun providesCoroutineDispatchers(): CoroutineDispatchers { + return CoroutineDispatchers(io = Dispatchers.IO) + } +} 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 deleted file mode 100644 index 68b212c830..0000000000 --- a/vector/src/main/java/im/vector/app/core/di/VectorComponent.kt +++ /dev/null @@ -1,174 +0,0 @@ -/* - * Copyright 2019 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.di - -import android.content.Context -import android.content.res.Resources -import dagger.BindsInstance -import dagger.Component -import im.vector.app.ActiveSessionDataSource -import im.vector.app.AppStateHandler -import im.vector.app.EmojiCompatFontProvider -import im.vector.app.EmojiCompatWrapper -import im.vector.app.VectorApplication -import im.vector.app.core.dialogs.UnrecognizedCertificateDialog -import im.vector.app.core.error.ErrorFormatter -import im.vector.app.core.network.WifiDetector -import im.vector.app.core.pushers.PushersManager -import im.vector.app.core.utils.AssetReader -import im.vector.app.core.utils.DimensionConverter -import im.vector.app.features.call.conference.JitsiActiveConferenceHolder -import im.vector.app.features.call.webrtc.WebRtcCallManager -import im.vector.app.features.configuration.VectorConfiguration -import im.vector.app.features.crypto.keysrequest.KeyRequestHandler -import im.vector.app.features.crypto.verification.IncomingVerificationRequestHandler -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.html.EventHtmlRenderer -import im.vector.app.features.html.VectorHtmlCompressor -import im.vector.app.features.invite.AutoAcceptInvites -import im.vector.app.features.login.ReAuthHelper -import im.vector.app.features.navigation.Navigator -import im.vector.app.features.notifications.NotifiableEventResolver -import im.vector.app.features.notifications.NotificationBroadcastReceiver -import im.vector.app.features.notifications.NotificationDrawerManager -import im.vector.app.features.notifications.NotificationUtils -import im.vector.app.features.notifications.PushRuleTriggerListener -import im.vector.app.features.pin.PinCodeStore -import im.vector.app.features.pin.PinLocker -import im.vector.app.features.popup.PopupAlertManager -import im.vector.app.features.rageshake.BugReporter -import im.vector.app.features.rageshake.VectorFileLogger -import im.vector.app.features.rageshake.VectorUncaughtExceptionHandler -import im.vector.app.features.reactions.data.EmojiDataSource -import im.vector.app.features.session.SessionListener -import im.vector.app.features.settings.VectorPreferences -import im.vector.app.features.ui.UiStateRepository -import org.matrix.android.sdk.api.Matrix -import org.matrix.android.sdk.api.auth.AuthenticationService -import org.matrix.android.sdk.api.auth.HomeServerHistoryService -import org.matrix.android.sdk.api.raw.RawService -import org.matrix.android.sdk.api.session.Session -import javax.inject.Singleton - -@Component(modules = [VectorModule::class]) -@Singleton -interface VectorComponent { - - fun inject(notificationBroadcastReceiver: NotificationBroadcastReceiver) - - fun inject(vectorApplication: VectorApplication) - - fun matrix(): Matrix - - fun matrixItemColorProvider(): MatrixItemColorProvider - - fun sessionListener(): SessionListener - - fun currentSession(): Session - - fun notificationUtils(): NotificationUtils - - fun notificationDrawerManager(): NotificationDrawerManager - - fun appContext(): Context - - fun resources(): Resources - - fun assetReader(): AssetReader - - fun dimensionConverter(): DimensionConverter - - fun vectorConfiguration(): VectorConfiguration - - fun avatarRenderer(): AvatarRenderer - - fun activeSessionHolder(): ActiveSessionHolder - - fun unrecognizedCertificateDialog(): UnrecognizedCertificateDialog - - fun emojiCompatFontProvider(): EmojiCompatFontProvider - - fun emojiCompatWrapper(): EmojiCompatWrapper - - fun eventHtmlRenderer(): EventHtmlRenderer - - fun vectorHtmlCompressor(): VectorHtmlCompressor - - fun navigator(): Navigator - - fun errorFormatter(): ErrorFormatter - - fun appStateHandler(): AppStateHandler - - fun currentSpaceSuggestedRoomListDataSource(): CurrentSpaceSuggestedRoomListDataSource - - fun roomDetailPendingActionStore(): RoomDetailPendingActionStore - - fun activeSessionObservableStore(): ActiveSessionDataSource - - fun incomingVerificationRequestHandler(): IncomingVerificationRequestHandler - - fun incomingKeyRequestHandler(): KeyRequestHandler - - fun authenticationService(): AuthenticationService - - fun rawService(): RawService - - fun homeServerHistoryService(): HomeServerHistoryService - - fun bugReporter(): BugReporter - - fun vectorUncaughtExceptionHandler(): VectorUncaughtExceptionHandler - - fun pushRuleTriggerListener(): PushRuleTriggerListener - - fun pusherManager(): PushersManager - - fun notifiableEventResolver(): NotifiableEventResolver - - fun vectorPreferences(): VectorPreferences - - fun wifiDetector(): WifiDetector - - fun vectorFileLogger(): VectorFileLogger - - fun uiStateRepository(): UiStateRepository - - fun pinCodeStore(): PinCodeStore - - fun emojiDataSource(): EmojiDataSource - - fun alertManager(): PopupAlertManager - - fun reAuthHelper(): ReAuthHelper - - fun pinLocker(): PinLocker - - fun autoAcceptInvites(): AutoAcceptInvites - - fun webRtcCallManager(): WebRtcCallManager - - fun jitsiActiveConferenceHolder(): JitsiActiveConferenceHolder - - @Component.Factory - interface Factory { - fun create(@BindsInstance context: Context): VectorComponent - } -} diff --git a/vector/src/main/java/im/vector/app/core/di/ViewModelKey.kt b/vector/src/main/java/im/vector/app/core/di/ViewModelKey.kt index 5f0ee30821..2782edd558 100644 --- a/vector/src/main/java/im/vector/app/core/di/ViewModelKey.kt +++ b/vector/src/main/java/im/vector/app/core/di/ViewModelKey.kt @@ -17,6 +17,7 @@ package im.vector.app.core.di import androidx.lifecycle.ViewModel +import com.airbnb.mvrx.MavericksViewModel import dagger.MapKey import kotlin.reflect.KClass @@ -24,3 +25,8 @@ import kotlin.reflect.KClass @Retention(AnnotationRetention.RUNTIME) @MapKey annotation class ViewModelKey(val value: KClass) + +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.FUNCTION) +@MapKey +annotation class MavericksViewModelKey(val value: KClass>) diff --git a/vector/src/main/java/im/vector/app/core/di/ViewModelModule.kt b/vector/src/main/java/im/vector/app/core/di/ViewModelModule.kt index 4e07c1e2ca..4f8329c026 100644 --- a/vector/src/main/java/im/vector/app/core/di/ViewModelModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/ViewModelModule.kt @@ -20,6 +20,8 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import dagger.Binds import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ActivityComponent import dagger.multibindings.IntoMap import im.vector.app.core.platform.ConfigurationViewModel import im.vector.app.features.call.SharedKnownCallsViewModel @@ -42,6 +44,7 @@ import im.vector.app.features.spaces.SpacePreviewSharedActionViewModel import im.vector.app.features.spaces.people.SpacePeopleSharedActionViewModel import im.vector.app.features.userdirectory.UserListSharedActionViewModel +@InstallIn(ActivityComponent::class) @Module interface ViewModelModule { diff --git a/vector/src/main/java/im/vector/app/core/di/ScreenScope.kt b/vector/src/main/java/im/vector/app/core/dispatchers/CoroutineDispatchers.kt similarity index 64% rename from vector/src/main/java/im/vector/app/core/di/ScreenScope.kt rename to vector/src/main/java/im/vector/app/core/dispatchers/CoroutineDispatchers.kt index c39d6a947e..c489290a55 100644 --- a/vector/src/main/java/im/vector/app/core/di/ScreenScope.kt +++ b/vector/src/main/java/im/vector/app/core/dispatchers/CoroutineDispatchers.kt @@ -1,11 +1,11 @@ /* - * Copyright 2019 New Vector Ltd + * 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 + * 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, @@ -14,11 +14,9 @@ * limitations under the License. */ -package im.vector.app.core.di +package im.vector.app.core.dispatchers -import javax.inject.Scope +import kotlinx.coroutines.CoroutineDispatcher +import javax.inject.Inject -@Scope -@MustBeDocumented -@Retention(AnnotationRetention.RUNTIME) -annotation class ScreenScope +data class CoroutineDispatchers @Inject constructor(val io: CoroutineDispatcher) diff --git a/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetRadioActionItem.kt b/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetRadioActionItem.kt index 91361aa89d..8899532d04 100644 --- a/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetRadioActionItem.kt +++ b/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetRadioActionItem.kt @@ -27,6 +27,7 @@ import im.vector.app.core.epoxy.ClickListener import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.epoxy.onClick +import im.vector.app.core.extensions.setAttributeTintedImageResource import im.vector.app.core.extensions.setTextOrHide /** @@ -62,7 +63,7 @@ abstract class BottomSheetRadioActionItem : VectorEpoxyModel(R.id.matrixItemTitle) val subtitleView by bind(R.id.matrixItemSubtitle) + val presenceImageView by bind(R.id.matrixItemPresenceImageView) val avatarImageView by bind(R.id.matrixItemAvatar) val avatarDecorationImageView by bind(R.id.matrixItemAvatarDecoration) val editableView by bind(R.id.matrixItemEditable) diff --git a/vector/src/main/java/im/vector/app/core/epoxy/profiles/ProfileMatrixItemWithPowerLevelWithPresence.kt b/vector/src/main/java/im/vector/app/core/epoxy/profiles/ProfileMatrixItemWithPowerLevelWithPresence.kt new file mode 100644 index 0000000000..92216cbb38 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/epoxy/profiles/ProfileMatrixItemWithPowerLevelWithPresence.kt @@ -0,0 +1,34 @@ +/* + * Copyright 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 + +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.app.R +import org.matrix.android.sdk.api.session.presence.model.UserPresence + +@EpoxyModelClass(layout = R.layout.item_profile_matrix_item) +abstract class ProfileMatrixItemWithPowerLevelWithPresence : ProfileMatrixItemWithPowerLevel() { + + @EpoxyAttribute var userPresence: UserPresence? = null + + override fun bind(holder: Holder) { + super.bind(holder) + holder.presenceImageView.render(userPresence = userPresence) + } +} 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 index 37d16ab6b1..721d84e050 100644 --- 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 @@ -27,6 +27,7 @@ import im.vector.app.core.epoxy.ClickListener import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.epoxy.onClick +import im.vector.app.core.extensions.setAttributeTintedImageResource @EpoxyModelClass(layout = R.layout.item_radio) abstract class RadioButtonItem : VectorEpoxyModel() { @@ -54,7 +55,7 @@ abstract class RadioButtonItem : VectorEpoxyModel() { } if (selected) { - holder.radioImage.setImageDrawable(ContextCompat.getDrawable(holder.view.context, R.drawable.ic_radio_on)) + holder.radioImage.setAttributeTintedImageResource(R.drawable.ic_radio_on, R.attr.colorPrimary) holder.radioImage.contentDescription = holder.view.context.getString(R.string.a11y_checked) } else { holder.radioImage.setImageDrawable(ContextCompat.getDrawable(holder.view.context, R.drawable.ic_radio_off)) diff --git a/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt b/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt index f0ba79e31c..6494f31336 100644 --- a/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt +++ b/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt @@ -58,49 +58,49 @@ class DefaultErrorFormatter @Inject constructor( } is Failure.ServerError -> { when { - throwable.error.code == MatrixError.M_CONSENT_NOT_GIVEN -> { + throwable.error.code == MatrixError.M_CONSENT_NOT_GIVEN -> { // Special case for terms and conditions stringProvider.getString(R.string.error_terms_not_accepted) } - throwable.isInvalidPassword() -> { + throwable.isInvalidPassword() -> { stringProvider.getString(R.string.auth_invalid_login_param) } - throwable.error.code == MatrixError.M_USER_IN_USE -> { + throwable.error.code == MatrixError.M_USER_IN_USE -> { stringProvider.getString(R.string.login_signup_error_user_in_use) } - throwable.error.code == MatrixError.M_BAD_JSON -> { + throwable.error.code == MatrixError.M_BAD_JSON -> { stringProvider.getString(R.string.login_error_bad_json) } - throwable.error.code == MatrixError.M_NOT_JSON -> { + throwable.error.code == MatrixError.M_NOT_JSON -> { stringProvider.getString(R.string.login_error_not_json) } - throwable.error.code == MatrixError.M_THREEPID_DENIED -> { + throwable.error.code == MatrixError.M_THREEPID_DENIED -> { stringProvider.getString(R.string.login_error_threepid_denied) } - throwable.error.code == MatrixError.M_LIMIT_EXCEEDED -> { + throwable.error.code == MatrixError.M_LIMIT_EXCEEDED -> { limitExceededError(throwable.error) } - throwable.error.code == MatrixError.M_TOO_LARGE -> { + throwable.error.code == MatrixError.M_TOO_LARGE -> { stringProvider.getString(R.string.error_file_too_big_simple) } - throwable.error.code == MatrixError.M_THREEPID_NOT_FOUND -> { + throwable.error.code == MatrixError.M_THREEPID_NOT_FOUND -> { stringProvider.getString(R.string.login_reset_password_error_not_found) } - throwable.error.code == MatrixError.M_USER_DEACTIVATED -> { + throwable.error.code == MatrixError.M_USER_DEACTIVATED -> { stringProvider.getString(R.string.auth_invalid_login_deactivated_account) } - throwable.error.code == MatrixError.M_THREEPID_IN_USE - && throwable.error.message == "Email is already in use" -> { + throwable.error.code == MatrixError.M_THREEPID_IN_USE && + throwable.error.message == "Email is already in use" -> { stringProvider.getString(R.string.account_email_already_used_error) } - throwable.error.code == MatrixError.M_THREEPID_IN_USE - && throwable.error.message == "MSISDN is already in use" -> { + throwable.error.code == MatrixError.M_THREEPID_IN_USE && + throwable.error.message == "MSISDN is already in use" -> { stringProvider.getString(R.string.account_phone_number_already_used_error) } - throwable.error.code == MatrixError.M_THREEPID_AUTH_FAILED -> { + throwable.error.code == MatrixError.M_THREEPID_AUTH_FAILED -> { stringProvider.getString(R.string.error_threepid_auth_failed) } - else -> { + else -> { throwable.error.message.takeIf { it.isNotEmpty() } ?: throwable.error.code.takeIf { it.isNotEmpty() } } diff --git a/vector/src/main/java/im/vector/app/core/extensions/BasicExtensions.kt b/vector/src/main/java/im/vector/app/core/extensions/BasicExtensions.kt index 07a684abef..ee3d79d846 100644 --- a/vector/src/main/java/im/vector/app/core/extensions/BasicExtensions.kt +++ b/vector/src/main/java/im/vector/app/core/extensions/BasicExtensions.kt @@ -57,7 +57,7 @@ fun CharSequence.isMsisdn(): Boolean { * - "fi.le.txt".insertBeforeLast("_foo") will return "fi.le_foo.txt" * - null.insertBeforeLast("_foo") will return "_foo" */ -fun String?.insertBeforeLast(insert: String, delimiter: String = ".") : String { +fun String?.insertBeforeLast(insert: String, delimiter: String = "."): String { if (this == null) return insert val idx = lastIndexOf(delimiter) return if (idx == -1) { diff --git a/vector/src/main/java/im/vector/app/core/extensions/Context.kt b/vector/src/main/java/im/vector/app/core/extensions/Context.kt index c1c435edf2..59847da7c9 100644 --- a/vector/src/main/java/im/vector/app/core/extensions/Context.kt +++ b/vector/src/main/java/im/vector/app/core/extensions/Context.kt @@ -17,14 +17,9 @@ package im.vector.app.core.extensions import android.content.Context -import im.vector.app.core.di.HasVectorInjector -import im.vector.app.core.di.VectorComponent +import dagger.hilt.EntryPoints +import im.vector.app.core.di.SingletonEntryPoint -fun Context.vectorComponent(): VectorComponent { - val appContext = applicationContext - if (appContext is HasVectorInjector) { - return appContext.injector() - } else { - throw IllegalStateException("Your application context doesn't implement HasVectorInjector") - } +fun Context.singletonEntryPoint(): SingletonEntryPoint { + return EntryPoints.get(applicationContext, SingletonEntryPoint::class.java) } diff --git a/vector/src/main/java/im/vector/app/core/extensions/EditText.kt b/vector/src/main/java/im/vector/app/core/extensions/EditText.kt index 05b70def3d..0eb9dcdaf9 100644 --- a/vector/src/main/java/im/vector/app/core/extensions/EditText.kt +++ b/vector/src/main/java/im/vector/app/core/extensions/EditText.kt @@ -18,6 +18,7 @@ package im.vector.app.core.extensions import android.text.Editable import android.text.InputType +import android.text.Spanned import android.view.MotionEvent import android.view.View import android.view.inputmethod.EditorInfo @@ -57,3 +58,38 @@ fun EditText.setupAsSearch(@DrawableRes searchIconRes: Int = R.drawable.ic_searc return@OnTouchListener false }) } + +fun EditText.setTextIfDifferent(newText: CharSequence?): Boolean { + if (!isTextDifferent(newText, text)) { + // Previous text is the same. No op + return false + } + setText(newText) + // Since the text changed we move the cursor to the end of the new text. + // This allows us to fill in text programmatically with a different value, + // but if the user is typing and the view is rebound we won't lose their cursor position. + setSelection(newText?.length ?: 0) + return true +} + +private fun isTextDifferent(str1: CharSequence?, str2: CharSequence?): Boolean { + if (str1 === str2) { + return false + } + if (str1 == null || str2 == null) { + return true + } + val length = str1.length + if (length != str2.length) { + return true + } + if (str1 is Spanned) { + return str1 != str2 + } + for (i in 0 until length) { + if (str1[i] != str2[i]) { + return true + } + } + return false +} diff --git a/vector/src/main/java/im/vector/app/core/extensions/Parcelable.kt b/vector/src/main/java/im/vector/app/core/extensions/Parcelable.kt index 6ca0144601..65f59f7b73 100644 --- a/vector/src/main/java/im/vector/app/core/extensions/Parcelable.kt +++ b/vector/src/main/java/im/vector/app/core/extensions/Parcelable.kt @@ -18,8 +18,8 @@ package im.vector.app.core.extensions import android.os.Bundle import android.os.Parcelable -import com.airbnb.mvrx.MvRx +import com.airbnb.mvrx.Mavericks fun Parcelable?.toMvRxBundle(): Bundle? { - return this?.let { Bundle().apply { putParcelable(MvRx.KEY_ARG, it) } } + return this?.let { Bundle().apply { putParcelable(Mavericks.KEY_ARG, it) } } } diff --git a/vector/src/main/java/im/vector/app/core/extensions/Session.kt b/vector/src/main/java/im/vector/app/core/extensions/Session.kt index 699247ab6d..90b08ef92b 100644 --- a/vector/src/main/java/im/vector/app/core/extensions/Session.kt +++ b/vector/src/main/java/im/vector/app/core/extensions/Session.kt @@ -26,13 +26,15 @@ import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupState import org.matrix.android.sdk.api.session.sync.FilterService import timber.log.Timber -fun Session.configureAndStart(context: Context) { +fun Session.configureAndStart(context: Context, startSyncing: Boolean = true) { Timber.i("Configure and start session for $myUserId") open() setFilter(FilterService.FilterPreset.ElementFilter) - startSyncing(context) + if (startSyncing) { + startSyncing(context) + } refreshPushers() - context.vectorComponent().webRtcCallManager().checkForProtocolsSupportIfNeeded() + context.singletonEntryPoint().webRtcCallManager().checkForProtocolsSupportIfNeeded() } fun Session.startSyncing(context: Context) { @@ -62,15 +64,15 @@ fun Session.startSyncing(context: Context) { * Tell is the session has unsaved e2e keys in the backup */ fun Session.hasUnsavedKeys(): Boolean { - return cryptoService().inboundGroupSessionsCount(false) > 0 - && cryptoService().keysBackupService().state != KeysBackupState.ReadyToBackUp + return cryptoService().inboundGroupSessionsCount(false) > 0 && + cryptoService().keysBackupService().state != KeysBackupState.ReadyToBackUp } fun Session.cannotLogoutSafely(): Boolean { // has some encrypted chat - return hasUnsavedKeys() + return hasUnsavedKeys() || // has local cross signing keys - || (cryptoService().crossSigningService().allPrivateKeysKnown() + (cryptoService().crossSigningService().allPrivateKeysKnown() && // That are not backed up - && !sharedSecretStorageService.isRecoverySetup()) + !sharedSecretStorageService.isRecoverySetup()) } 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 1c424f7071..adb655f169 100644 --- a/vector/src/main/java/im/vector/app/core/extensions/TextView.kt +++ b/vector/src/main/java/im/vector/app/core/extensions/TextView.kt @@ -39,13 +39,15 @@ import im.vector.app.features.themes.ThemeUtils /** * Set a text in the TextView, or set visibility to GONE if the text is null */ -fun TextView.setTextOrHide(newText: CharSequence?, hideWhenBlank: Boolean = true) { - if (newText == null - || (newText.isBlank() && hideWhenBlank)) { +fun TextView.setTextOrHide(newText: CharSequence?, hideWhenBlank: Boolean = true, vararg relatedViews: View = emptyArray()) { + if (newText == null || + (newText.isBlank() && hideWhenBlank)) { isVisible = false + relatedViews.forEach { it.isVisible = false } } else { this.text = newText isVisible = true + relatedViews.forEach { it.isVisible = true } } } diff --git a/vector/src/main/java/im/vector/app/core/extensions/TimelineEvent.kt b/vector/src/main/java/im/vector/app/core/extensions/TimelineEvent.kt index 48e3a488ed..43ff186e99 100644 --- a/vector/src/main/java/im/vector/app/core/extensions/TimelineEvent.kt +++ b/vector/src/main/java/im/vector/app/core/extensions/TimelineEvent.kt @@ -22,7 +22,7 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent fun TimelineEvent.canReact(): Boolean { // Only event of type EventType.MESSAGE or EventType.STICKER are supported for the moment - return root.getClearType() in listOf(EventType.MESSAGE, EventType.STICKER) - && root.sendState == SendState.SYNCED - && !root.isRedacted() + return root.getClearType() in listOf(EventType.MESSAGE, EventType.STICKER) && + root.sendState == SendState.SYNCED && + !root.isRedacted() } diff --git a/vector/src/main/java/im/vector/app/core/extensions/UriExtensions.kt b/vector/src/main/java/im/vector/app/core/extensions/UriExtensions.kt new file mode 100644 index 0000000000..ec7a03bdbb --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/extensions/UriExtensions.kt @@ -0,0 +1,25 @@ +/* + * 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.extensions + +import android.net.Uri + +const val IGNORED_SCHEMA = "ignored" + +fun Uri.isIgnored() = scheme == IGNORED_SCHEMA + +fun createIgnoredUri(path: String): Uri = Uri.parse("$IGNORED_SCHEMA://$path") diff --git a/vector/src/main/java/im/vector/app/core/extensions/ViewExtensions.kt b/vector/src/main/java/im/vector/app/core/extensions/ViewExtensions.kt index b28f33816b..54fcac42d1 100644 --- a/vector/src/main/java/im/vector/app/core/extensions/ViewExtensions.kt +++ b/vector/src/main/java/im/vector/app/core/extensions/ViewExtensions.kt @@ -22,9 +22,14 @@ import android.view.View import android.view.ViewGroup import android.widget.EditText import android.widget.ImageView +import androidx.annotation.AttrRes +import androidx.annotation.DrawableRes import androidx.appcompat.widget.SearchView +import androidx.core.content.ContextCompat +import androidx.core.graphics.drawable.DrawableCompat import androidx.core.view.isVisible import im.vector.app.R +import im.vector.app.features.themes.ThemeUtils /** * Remove left margin of a SearchView @@ -58,3 +63,20 @@ fun ImageView.setDrawableOrHide(drawableRes: Drawable?) { setImageDrawable(drawableRes) isVisible = drawableRes != null } + +fun View.setAttributeTintedBackground(@DrawableRes drawableRes: Int, @AttrRes tint: Int) { + val drawable = ContextCompat.getDrawable(context, drawableRes)!! + DrawableCompat.setTint(drawable, ThemeUtils.getColor(context, tint)) + background = drawable +} + +fun ImageView.setAttributeTintedImageResource(@DrawableRes drawableRes: Int, @AttrRes tint: Int) { + val drawable = ContextCompat.getDrawable(context, drawableRes)!! + DrawableCompat.setTint(drawable, ThemeUtils.getColor(context, tint)) + setImageDrawable(drawable) +} + +fun View.setAttributeBackground(@AttrRes attributeId: Int) { + val attribute = ThemeUtils.getAttribute(context, attributeId)!! + setBackgroundResource(attribute.resourceId) +} diff --git a/vector/src/main/java/im/vector/app/core/glide/AvatarPlaceholder.kt b/vector/src/main/java/im/vector/app/core/glide/AvatarPlaceholder.kt index 32968c4f80..e61f81de55 100644 --- a/vector/src/main/java/im/vector/app/core/glide/AvatarPlaceholder.kt +++ b/vector/src/main/java/im/vector/app/core/glide/AvatarPlaceholder.kt @@ -26,7 +26,7 @@ import com.bumptech.glide.load.model.ModelLoader import com.bumptech.glide.load.model.ModelLoaderFactory import com.bumptech.glide.load.model.MultiModelLoaderFactory import com.bumptech.glide.signature.ObjectKey -import im.vector.app.core.extensions.vectorComponent +import im.vector.app.core.extensions.singletonEntryPoint import org.matrix.android.sdk.api.util.MatrixItem data class AvatarPlaceholder(val matrixItem: MatrixItem) @@ -42,8 +42,8 @@ class AvatarPlaceholderModelLoaderFactory(private val context: Context) : ModelL } } -class AvatarPlaceholderModelLoader(private val context: Context) - : ModelLoader { +class AvatarPlaceholderModelLoader(private val context: Context) : + ModelLoader { override fun buildLoadData(model: AvatarPlaceholder, width: Int, height: Int, options: Options): ModelLoader.LoadData? { return ModelLoader.LoadData(ObjectKey(model), AvatarPlaceholderDataFetcher(context, model)) @@ -54,10 +54,10 @@ class AvatarPlaceholderModelLoader(private val context: Context) } } -class AvatarPlaceholderDataFetcher(context: Context, private val data: AvatarPlaceholder) - : DataFetcher { +class AvatarPlaceholderDataFetcher(context: Context, private val data: AvatarPlaceholder) : + DataFetcher { - private val avatarRenderer = context.vectorComponent().avatarRenderer() + private val avatarRenderer = context.singletonEntryPoint().avatarRenderer() override fun loadData(priority: Priority, callback: DataFetcher.DataCallback) { val avatarPlaceholder = avatarRenderer.getPlaceholderDrawable(data.matrixItem) diff --git a/vector/src/main/java/im/vector/app/core/glide/MyAppGlideModule.kt b/vector/src/main/java/im/vector/app/core/glide/MyAppGlideModule.kt index 74c9d4f0f6..59bffd95fd 100644 --- a/vector/src/main/java/im/vector/app/core/glide/MyAppGlideModule.kt +++ b/vector/src/main/java/im/vector/app/core/glide/MyAppGlideModule.kt @@ -19,7 +19,6 @@ package im.vector.app.core.glide import android.content.Context import android.graphics.drawable.Drawable import android.util.Log - import com.bumptech.glide.Glide import com.bumptech.glide.GlideBuilder import com.bumptech.glide.Registry diff --git a/vector/src/main/java/im/vector/app/core/glide/VectorGlideModelLoader.kt b/vector/src/main/java/im/vector/app/core/glide/VectorGlideModelLoader.kt index 5fae815dfb..6b42e3fff8 100644 --- a/vector/src/main/java/im/vector/app/core/glide/VectorGlideModelLoader.kt +++ b/vector/src/main/java/im/vector/app/core/glide/VectorGlideModelLoader.kt @@ -25,7 +25,7 @@ import com.bumptech.glide.load.model.ModelLoader import com.bumptech.glide.load.model.ModelLoaderFactory import com.bumptech.glide.load.model.MultiModelLoaderFactory import com.bumptech.glide.signature.ObjectKey -import im.vector.app.core.extensions.vectorComponent +import im.vector.app.core.extensions.singletonEntryPoint import im.vector.app.core.files.LocalFilesHelper import im.vector.app.features.media.ImageContentRenderer import im.vector.app.features.session.coroutineScope @@ -48,8 +48,8 @@ class VectorGlideModelLoaderFactory(private val context: Context) : ModelLoaderF } } -class VectorGlideModelLoader(private val context: Context) - : ModelLoader { +class VectorGlideModelLoader(private val context: Context) : + ModelLoader { override fun handles(model: ImageContentRenderer.Data): Boolean { // Always handle return true @@ -63,11 +63,11 @@ class VectorGlideModelLoader(private val context: Context) class VectorGlideDataFetcher(context: Context, private val data: ImageContentRenderer.Data, private val width: Int, - private val height: Int) - : DataFetcher { + private val height: Int) : + DataFetcher { private val localFilesHelper = LocalFilesHelper(context) - private val activeSessionHolder = context.vectorComponent().activeSessionHolder() + private val activeSessionHolder = context.singletonEntryPoint().activeSessionHolder() private val client = activeSessionHolder.getSafeActiveSession()?.getOkHttpClient() ?: OkHttpClient() diff --git a/vector/src/main/java/im/vector/app/core/platform/ButtonStateView.kt b/vector/src/main/java/im/vector/app/core/platform/ButtonStateView.kt index ee2933f542..e7e91dbfd9 100755 --- a/vector/src/main/java/im/vector/app/core/platform/ButtonStateView.kt +++ b/vector/src/main/java/im/vector/app/core/platform/ButtonStateView.kt @@ -28,8 +28,8 @@ import im.vector.app.core.epoxy.ClickListener import im.vector.app.core.epoxy.onClick import im.vector.app.databinding.ViewButtonStateBinding -class ButtonStateView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) - : FrameLayout(context, attrs, defStyle) { +class ButtonStateView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) : + FrameLayout(context, attrs, defStyle) { sealed class State { object Button : State() diff --git a/vector/src/main/java/im/vector/app/core/platform/LifecycleAwareLazy.kt b/vector/src/main/java/im/vector/app/core/platform/LifecycleAwareLazy.kt new file mode 100644 index 0000000000..283106232e --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/platform/LifecycleAwareLazy.kt @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.core.platform + +import androidx.annotation.MainThread +import androidx.fragment.app.Fragment +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleObserver +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.OnLifecycleEvent + +fun LifecycleOwner.lifecycleAwareLazy(initializer: () -> T): Lazy = LifecycleAwareLazy(this, initializer) + +private object UninitializedValue + +class LifecycleAwareLazy( + private val owner: LifecycleOwner, + initializer: () -> T +) : Lazy, LifecycleObserver { + + private var initializer: (() -> T)? = initializer + + private var _value: Any? = UninitializedValue + + @Suppress("UNCHECKED_CAST") + override val value: T + @MainThread + get() { + if (_value === UninitializedValue) { + _value = initializer!!() + attachToLifecycle() + } + return _value as T + } + + @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) + fun resetValue() { + _value = UninitializedValue + detachFromLifecycle() + } + + private fun attachToLifecycle() { + if (getLifecycleOwner().lifecycle.currentState == Lifecycle.State.DESTROYED) { + throw IllegalStateException("Initialization failed because lifecycle has been destroyed!") + } + getLifecycleOwner().lifecycle.addObserver(this) + } + + private fun detachFromLifecycle() { + getLifecycleOwner().lifecycle.removeObserver(this) + } + + private fun getLifecycleOwner() = when (owner) { + is Fragment -> owner.viewLifecycleOwner + else -> owner + } + + override fun isInitialized(): Boolean = _value !== UninitializedValue + + override fun toString(): String = if (isInitialized()) value.toString() else "Lazy value not initialized yet." +} diff --git a/vector/src/main/java/im/vector/app/core/platform/MaxHeightScrollView.kt b/vector/src/main/java/im/vector/app/core/platform/MaxHeightScrollView.kt index 139ccc8c05..8d4c5d8950 100644 --- a/vector/src/main/java/im/vector/app/core/platform/MaxHeightScrollView.kt +++ b/vector/src/main/java/im/vector/app/core/platform/MaxHeightScrollView.kt @@ -24,8 +24,8 @@ import im.vector.app.R private const val DEFAULT_MAX_HEIGHT = 200 -class MaxHeightScrollView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) - : NestedScrollView(context, attrs, defStyle) { +class MaxHeightScrollView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) : + NestedScrollView(context, attrs, defStyle) { var maxHeight: Int = 0 set(value) { diff --git a/vector/src/main/java/im/vector/app/core/platform/SimpleFragmentActivity.kt b/vector/src/main/java/im/vector/app/core/platform/SimpleFragmentActivity.kt index 066bd165a9..a70b2d66e6 100644 --- a/vector/src/main/java/im/vector/app/core/platform/SimpleFragmentActivity.kt +++ b/vector/src/main/java/im/vector/app/core/platform/SimpleFragmentActivity.kt @@ -15,15 +15,11 @@ */ package im.vector.app.core.platform -import androidx.annotation.CallSuper import androidx.core.view.isGone import androidx.core.view.isVisible -import im.vector.app.core.di.ScreenComponent import im.vector.app.core.extensions.hideKeyboard import im.vector.app.databinding.ActivityBinding -import org.matrix.android.sdk.api.session.Session - /** * Simple activity with a toolbar, a waiting overlay, and a fragment container and a session. */ @@ -33,13 +29,6 @@ abstract class SimpleFragmentActivity : VectorBaseActivity() { final override fun getCoordinatorLayout() = views.coordinatorLayout - lateinit var session: Session - - @CallSuper - override fun injectWith(injector: ScreenComponent) { - session = injector.activeSessionHolder().getActiveSession() - } - override fun initUiAndData() { configureToolbar(views.toolbar) waitingView = views.waitingView.waitingView diff --git a/vector/src/main/java/im/vector/app/core/platform/StateView.kt b/vector/src/main/java/im/vector/app/core/platform/StateView.kt index a935c56c51..b3d42dc38f 100755 --- a/vector/src/main/java/im/vector/app/core/platform/StateView.kt +++ b/vector/src/main/java/im/vector/app/core/platform/StateView.kt @@ -26,8 +26,8 @@ import im.vector.app.R import im.vector.app.core.extensions.updateConstraintSet import im.vector.app.databinding.ViewStateBinding -class StateView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) - : FrameLayout(context, attrs, defStyle) { +class StateView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) : + FrameLayout(context, attrs, defStyle) { sealed class State { object Content : State() 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 dc19520865..4d06dbe6a2 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 @@ -40,18 +40,16 @@ import androidx.fragment.app.FragmentFactory import androidx.fragment.app.FragmentManager import androidx.lifecycle.ViewModelProvider import androidx.viewbinding.ViewBinding +import com.airbnb.mvrx.MavericksView import com.bumptech.glide.util.Util import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.snackbar.Snackbar import com.jakewharton.rxbinding3.view.clicks +import dagger.hilt.android.EntryPointAccessors import im.vector.app.BuildConfig import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder -import im.vector.app.core.di.DaggerScreenComponent -import im.vector.app.core.di.HasScreenInjector -import im.vector.app.core.di.HasVectorInjector -import im.vector.app.core.di.ScreenComponent -import im.vector.app.core.di.VectorComponent +import im.vector.app.core.di.ActivityEntryPoint import im.vector.app.core.dialogs.DialogLocker import im.vector.app.core.dialogs.UnrecognizedCertificateDialog import im.vector.app.core.extensions.exhaustive @@ -60,7 +58,7 @@ import im.vector.app.core.extensions.observeNotNull import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.extensions.restart import im.vector.app.core.extensions.setTextOrHide -import im.vector.app.core.extensions.vectorComponent +import im.vector.app.core.extensions.singletonEntryPoint import im.vector.app.core.utils.toast import im.vector.app.features.MainActivity import im.vector.app.features.MainActivityArgs @@ -86,9 +84,9 @@ import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.failure.GlobalError import timber.log.Timber import java.util.concurrent.TimeUnit -import kotlin.system.measureTimeMillis +import javax.inject.Inject -abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector { +abstract class VectorBaseActivity : AppCompatActivity(), MavericksView { /* ========================================================================================== * View * ========================================================================================== */ @@ -135,8 +133,8 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasSc private lateinit var sessionListener: SessionListener protected lateinit var bugReporter: BugReporter private lateinit var pinLocker: PinLocker + @Inject lateinit var rageShake: RageShake - lateinit var navigator: Navigator private set private lateinit var fragmentFactory: FragmentFactory @@ -155,8 +153,6 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasSc private val uiDisposables = CompositeDisposable() private val restorables = ArrayList() - private lateinit var screenComponent: ScreenComponent - override fun attachBaseContext(base: Context) { val vectorConfiguration = VectorConfiguration(this) super.attachBaseContext(vectorConfiguration.getLocalisedContext(base)) @@ -186,25 +182,19 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasSc @CallSuper override fun onCreate(savedInstanceState: Bundle?) { Timber.i("onCreate Activity ${javaClass.simpleName}") - val vectorComponent = getVectorComponent() - screenComponent = DaggerScreenComponent.factory().create(vectorComponent, this) - val timeForInjection = measureTimeMillis { - injectWith(screenComponent) - } - Timber.v("Injecting dependencies into ${javaClass.simpleName} took $timeForInjection ms") + val singletonEntryPoint = singletonEntryPoint() + val activityEntryPoint = EntryPointAccessors.fromActivity(this, ActivityEntryPoint::class.java) ThemeUtils.setActivityTheme(this, getOtherThemes()) - fragmentFactory = screenComponent.fragmentFactory() + fragmentFactory = activityEntryPoint.fragmentFactory() supportFragmentManager.fragmentFactory = fragmentFactory + viewModelFactory = activityEntryPoint.viewModelFactory() super.onCreate(savedInstanceState) - viewModelFactory = screenComponent.viewModelFactory() configurationViewModel = viewModelProvider.get(ConfigurationViewModel::class.java) - bugReporter = screenComponent.bugReporter() - pinLocker = screenComponent.pinLocker() - // Shake detector - rageShake = screenComponent.rageShake() - navigator = screenComponent.navigator() - activeSessionHolder = screenComponent.activeSessionHolder() - vectorPreferences = vectorComponent.vectorPreferences() + bugReporter = singletonEntryPoint.bugReporter() + pinLocker = singletonEntryPoint.pinLocker() + navigator = singletonEntryPoint.navigator() + activeSessionHolder = singletonEntryPoint.activeSessionHolder() + vectorPreferences = singletonEntryPoint.vectorPreferences() configurationViewModel.activityRestarter.observe(this) { if (!it.hasBeenHandled) { // Recreate the Activity because configuration has changed @@ -216,7 +206,7 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasSc navigator.openPinCode(this, pinStartForActivityResult, PinMode.AUTH) } } - sessionListener = vectorComponent.sessionListener() + sessionListener = singletonEntryPoint.sessionListener() sessionListener.globalErrorLiveData.observeEvent(this) { handleGlobalError(it) } @@ -272,7 +262,7 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasSc } private fun handleCertificateError(certificateError: GlobalError.CertificateError) { - vectorComponent() + singletonEntryPoint() .unrecognizedCertificateDialog() .show(this, certificateError.fingerprint, @@ -402,12 +392,6 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasSc bugReporter.inMultiWindowMode = isInMultiWindowMode } - override fun injector(): ScreenComponent { - return screenComponent - } - - protected open fun injectWith(injector: ScreenComponent) = Unit - protected fun createFragment(fragmentClass: Class, args: Bundle?): Fragment { return fragmentFactory.instantiate(classLoader, fragmentClass.name).apply { arguments = args @@ -418,10 +402,6 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasSc * PRIVATE METHODS * ========================================================================================== */ - internal fun getVectorComponent(): VectorComponent { - return (application as HasVectorInjector).injector() - } - /** * Force to render the activity in fullscreen */ @@ -576,6 +556,8 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasSc open fun initUiAndData() = Unit + override fun invalidate() = Unit + @StringRes open fun getTitleRes() = -1 diff --git a/vector/src/main/java/im/vector/app/core/platform/VectorBaseBottomSheetDialogFragment.kt b/vector/src/main/java/im/vector/app/core/platform/VectorBaseBottomSheetDialogFragment.kt index b9b5bc8ca5..711b2b144b 100644 --- a/vector/src/main/java/im/vector/app/core/platform/VectorBaseBottomSheetDialogFragment.kt +++ b/vector/src/main/java/im/vector/app/core/platform/VectorBaseBottomSheetDialogFragment.kt @@ -27,15 +27,14 @@ import android.widget.FrameLayout import androidx.annotation.CallSuper import androidx.lifecycle.ViewModelProvider import androidx.viewbinding.ViewBinding -import com.airbnb.mvrx.MvRx -import com.airbnb.mvrx.MvRxView -import com.airbnb.mvrx.MvRxViewId +import com.airbnb.mvrx.Mavericks +import com.airbnb.mvrx.MavericksView import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.jakewharton.rxbinding3.view.clicks -import im.vector.app.core.di.DaggerScreenComponent -import im.vector.app.core.di.ScreenComponent +import dagger.hilt.android.EntryPointAccessors +import im.vector.app.core.di.ActivityEntryPoint import im.vector.app.core.utils.DimensionConverter import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.CompositeDisposable @@ -44,13 +43,9 @@ import timber.log.Timber import java.util.concurrent.TimeUnit /** - * Add MvRx capabilities to bottomsheetdialog (like BaseMvRxFragment) + * Add Mavericks capabilities, handle DI and bindings. */ -abstract class VectorBaseBottomSheetDialogFragment : BottomSheetDialogFragment(), MvRxView { - - private val mvrxViewIdProperty = MvRxViewId() - final override val mvrxViewId: String by mvrxViewIdProperty - private lateinit var screenComponent: ScreenComponent +abstract class VectorBaseBottomSheetDialogFragment : BottomSheetDialogFragment(), MavericksView { /* ========================================================================================== * View @@ -125,17 +120,9 @@ abstract class VectorBaseBottomSheetDialogFragment : BottomShe } override fun onAttach(context: Context) { - screenComponent = DaggerScreenComponent.factory().create(vectorBaseActivity.getVectorComponent(), vectorBaseActivity) - viewModelFactory = screenComponent.viewModelFactory() + val activityEntryPoint = EntryPointAccessors.fromActivity(vectorBaseActivity, ActivityEntryPoint::class.java) + viewModelFactory = activityEntryPoint.viewModelFactory() super.onAttach(context) - injectWith(screenComponent) - } - - protected open fun injectWith(injector: ScreenComponent) = Unit - - override fun onCreate(savedInstanceState: Bundle?) { - mvrxViewIdProperty.restoreFrom(savedInstanceState) - super.onCreate(savedInstanceState) } override fun onResume() { @@ -154,11 +141,6 @@ abstract class VectorBaseBottomSheetDialogFragment : BottomShe } } - override fun onSaveInstanceState(outState: Bundle) { - super.onSaveInstanceState(outState) - mvrxViewIdProperty.saveTo(outState) - } - override fun onStart() { super.onStart() // This ensures that invalidate() is called for static screens that don't @@ -179,7 +161,7 @@ abstract class VectorBaseBottomSheetDialogFragment : BottomShe } protected fun setArguments(args: Parcelable? = null) { - arguments = args?.let { Bundle().apply { putParcelable(MvRx.KEY_ARG, it) } } + arguments = args?.let { Bundle().apply { putParcelable(Mavericks.KEY_ARG, it) } } } /* ========================================================================================== diff --git a/vector/src/main/java/im/vector/app/core/platform/VectorBaseFragment.kt b/vector/src/main/java/im/vector/app/core/platform/VectorBaseFragment.kt index 2c9452c4fd..d3c66ec61d 100644 --- a/vector/src/main/java/im/vector/app/core/platform/VectorBaseFragment.kt +++ b/vector/src/main/java/im/vector/app/core/platform/VectorBaseFragment.kt @@ -27,19 +27,20 @@ import android.view.ViewGroup import androidx.annotation.CallSuper import androidx.annotation.MainThread import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider import androidx.viewbinding.ViewBinding -import com.airbnb.mvrx.BaseMvRxFragment +import com.airbnb.mvrx.MavericksView import com.bumptech.glide.util.Util.assertMainThread import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.jakewharton.rxbinding3.view.clicks +import dagger.hilt.android.EntryPointAccessors import im.vector.app.R -import im.vector.app.core.di.DaggerScreenComponent -import im.vector.app.core.di.HasScreenInjector -import im.vector.app.core.di.ScreenComponent +import im.vector.app.core.di.ActivityEntryPoint import im.vector.app.core.dialogs.UnrecognizedCertificateDialog import im.vector.app.core.error.ErrorFormatter +import im.vector.app.core.extensions.singletonEntryPoint import im.vector.app.core.extensions.toMvRxBundle import im.vector.app.features.navigation.Navigator import im.vector.lib.ui.styles.dialogs.MaterialProgressDialog @@ -49,7 +50,7 @@ import io.reactivex.disposables.Disposable import timber.log.Timber import java.util.concurrent.TimeUnit -abstract class VectorBaseFragment : BaseMvRxFragment(), HasScreenInjector { +abstract class VectorBaseFragment : Fragment(), MavericksView { protected val vectorBaseActivity: VectorBaseActivity<*> by lazy { activity as VectorBaseActivity<*> @@ -59,8 +60,6 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), HasScr * Navigator and other common objects * ========================================================================================== */ - private lateinit var screenComponent: ScreenComponent - protected lateinit var navigator: Navigator protected lateinit var errorFormatter: ErrorFormatter protected lateinit var unrecognizedCertificateDialog: UnrecognizedCertificateDialog @@ -94,12 +93,13 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), HasScr * ========================================================================================== */ override fun onAttach(context: Context) { - screenComponent = DaggerScreenComponent.factory().create(vectorBaseActivity.getVectorComponent(), vectorBaseActivity) - navigator = screenComponent.navigator() - errorFormatter = screenComponent.errorFormatter() - unrecognizedCertificateDialog = screenComponent.unrecognizedCertificateDialog() - viewModelFactory = screenComponent.viewModelFactory() - childFragmentManager.fragmentFactory = screenComponent.fragmentFactory() + val singletonEntryPoint = context.singletonEntryPoint() + val activityEntryPoint = EntryPointAccessors.fromActivity(vectorBaseActivity, ActivityEntryPoint::class.java) + navigator = singletonEntryPoint.navigator() + errorFormatter = singletonEntryPoint.errorFormatter() + unrecognizedCertificateDialog = singletonEntryPoint.unrecognizedCertificateDialog() + viewModelFactory = activityEntryPoint.viewModelFactory() + childFragmentManager.fragmentFactory = activityEntryPoint.fragmentFactory() super.onAttach(context) } @@ -160,10 +160,6 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), HasScr super.onDestroy() } - override fun injector(): ScreenComponent { - return screenComponent - } - /* ========================================================================================== * Restorable * ========================================================================================== */ diff --git a/vector/src/main/java/im/vector/app/core/platform/VectorEventViewModel.kt b/vector/src/main/java/im/vector/app/core/platform/VectorEventViewModel.kt index 2d499db531..9ab46557a5 100644 --- a/vector/src/main/java/im/vector/app/core/platform/VectorEventViewModel.kt +++ b/vector/src/main/java/im/vector/app/core/platform/VectorEventViewModel.kt @@ -25,5 +25,5 @@ interface VectorSharedAction /** * Parent class to handle navigation events, action events, or other any events */ -open class VectorSharedActionViewModel(private val store: MutableDataSource = PublishDataSource()) - : ViewModel(), MutableDataSource by store +open class VectorSharedActionViewModel(private val store: MutableDataSource = PublishDataSource()) : + ViewModel(), MutableDataSource by store diff --git a/vector/src/main/java/im/vector/app/core/platform/VectorViewModel.kt b/vector/src/main/java/im/vector/app/core/platform/VectorViewModel.kt index d6f43beaf7..6e7c24d4e9 100644 --- a/vector/src/main/java/im/vector/app/core/platform/VectorViewModel.kt +++ b/vector/src/main/java/im/vector/app/core/platform/VectorViewModel.kt @@ -20,17 +20,17 @@ import com.airbnb.mvrx.Async import com.airbnb.mvrx.BaseMvRxViewModel import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Loading -import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.Success import im.vector.app.core.utils.DataSource import im.vector.app.core.utils.PublishDataSource import io.reactivex.Observable import io.reactivex.Single -abstract class VectorViewModel(initialState: S) - : BaseMvRxViewModel(initialState, false) { +abstract class VectorViewModel(initialState: S) : + BaseMvRxViewModel(initialState) { - interface Factory { + interface Factory { fun create(state: S): BaseMvRxViewModel } diff --git a/vector/src/main/java/im/vector/app/core/preference/UserAvatarPreference.kt b/vector/src/main/java/im/vector/app/core/preference/UserAvatarPreference.kt index 3bb50c6284..3095f98ea5 100755 --- a/vector/src/main/java/im/vector/app/core/preference/UserAvatarPreference.kt +++ b/vector/src/main/java/im/vector/app/core/preference/UserAvatarPreference.kt @@ -23,7 +23,7 @@ import android.widget.ProgressBar import androidx.preference.Preference import androidx.preference.PreferenceViewHolder import im.vector.app.R -import im.vector.app.core.extensions.vectorComponent +import im.vector.app.core.extensions.singletonEntryPoint import im.vector.app.features.home.AvatarRenderer import org.matrix.android.sdk.api.session.user.model.User import org.matrix.android.sdk.api.util.MatrixItem @@ -33,7 +33,7 @@ class UserAvatarPreference : Preference { private var mAvatarView: ImageView? = null private var mLoadingProgressBar: ProgressBar? = null - private var avatarRenderer: AvatarRenderer = context.vectorComponent().avatarRenderer() + private var avatarRenderer: AvatarRenderer = context.singletonEntryPoint().avatarRenderer() private var userItem: MatrixItem.UserItem? = null diff --git a/vector/src/main/java/im/vector/app/core/pushers/PushersManager.kt b/vector/src/main/java/im/vector/app/core/pushers/PushersManager.kt index 5896122393..27d6d05708 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/PushersManager.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/PushersManager.kt @@ -21,6 +21,7 @@ import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.resources.AppNameProvider import im.vector.app.core.resources.LocaleProvider import im.vector.app.core.resources.StringProvider +import org.matrix.android.sdk.api.session.pushers.PushersService import java.util.UUID import javax.inject.Inject import kotlin.math.abs @@ -44,23 +45,45 @@ class PushersManager @Inject constructor( ) } - fun registerPusherWithFcmKey(pushKey: String): UUID { + fun enqueueRegisterPusherWithFcmKey(pushKey: String): UUID { val currentSession = activeSessionHolder.getActiveSession() - val profileTag = DEFAULT_PUSHER_FILE_TAG + "_" + abs(currentSession.myUserId.hashCode()) + return currentSession.enqueueAddHttpPusher(createHttpPusher(pushKey)) + } - return currentSession.addHttpPusher( - pushKey, - stringProvider.getString(R.string.pusher_app_id), - profileTag, - localeProvider.current().language, - appNameProvider.getAppName(), - currentSession.sessionParams.deviceId ?: "MOBILE", - stringProvider.getString(R.string.pusher_http_url), - append = false, - withEventIdOnly = true + suspend fun registerPusherWithFcmKey(pushKey: String) { + val currentSession = activeSessionHolder.getActiveSession() + currentSession.addHttpPusher(createHttpPusher(pushKey)) + } + + private fun createHttpPusher(pushKey: String) = PushersService.HttpPusher( + pushKey, + stringProvider.getString(R.string.pusher_app_id), + profileTag = DEFAULT_PUSHER_FILE_TAG + "_" + abs(activeSessionHolder.getActiveSession().myUserId.hashCode()), + localeProvider.current().language, + appNameProvider.getAppName(), + activeSessionHolder.getActiveSession().sessionParams.deviceId ?: "MOBILE", + stringProvider.getString(R.string.pusher_http_url), + append = false, + withEventIdOnly = true + ) + + suspend fun registerEmailForPush(email: String) { + val currentSession = activeSessionHolder.getActiveSession() + val appName = appNameProvider.getAppName() + currentSession.addEmailPusher( + email = email, + lang = localeProvider.current().language, + emailBranding = appName, + appDisplayName = appName, + deviceDisplayName = currentSession.sessionParams.deviceId ?: "MOBILE" ) } + suspend fun unregisterEmailPusher(email: String) { + val currentSession = activeSessionHolder.getSafeActiveSession() ?: return + currentSession.removeEmailPusher(email) + } + suspend fun unregisterPusher(pushKey: String) { val currentSession = activeSessionHolder.getSafeActiveSession() ?: return currentSession.removeHttpPusher(pushKey, stringProvider.getString(R.string.pusher_app_id)) diff --git a/vector/src/main/java/im/vector/app/core/services/CallRingPlayer.kt b/vector/src/main/java/im/vector/app/core/services/CallRingPlayer.kt index faa921b99e..524ff37914 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 @@ -62,6 +62,10 @@ class CallRingPlayerIncoming( val ringtoneUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE) ringtone = RingtoneManager.getRingtone(applicationContext, ringtoneUri) Timber.v("Play ringtone for incoming call") + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + ringtone?.isLooping = true + } ringtone?.play() } diff --git a/vector/src/main/java/im/vector/app/core/services/CallService.kt b/vector/src/main/java/im/vector/app/core/services/CallService.kt index d8cf8cf6b8..d194434641 100644 --- a/vector/src/main/java/im/vector/app/core/services/CallService.kt +++ b/vector/src/main/java/im/vector/app/core/services/CallService.kt @@ -25,14 +25,16 @@ import android.view.KeyEvent import androidx.core.app.NotificationManagerCompat import androidx.core.content.ContextCompat import androidx.media.session.MediaButtonReceiver -import com.airbnb.mvrx.MvRx -import im.vector.app.core.extensions.vectorComponent +import com.airbnb.mvrx.Mavericks +import dagger.hilt.android.AndroidEntryPoint +import im.vector.app.core.extensions.singletonEntryPoint import im.vector.app.features.call.CallArgs import im.vector.app.features.call.VectorCallActivity import im.vector.app.features.call.telecom.CallConnection import im.vector.app.features.call.webrtc.WebRtcCall import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.call.webrtc.getOpponentAsMatrixItem +import im.vector.app.features.displayname.getBestName import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.notifications.NotificationUtils import im.vector.app.features.popup.IncomingCallAlert @@ -41,23 +43,25 @@ import org.matrix.android.sdk.api.logger.LoggerTag import org.matrix.android.sdk.api.session.room.model.call.EndCallReason import org.matrix.android.sdk.api.util.MatrixItem import timber.log.Timber +import javax.inject.Inject private val loggerTag = LoggerTag("CallService", LoggerTag.VOIP) /** * Foreground service to manage calls */ +@AndroidEntryPoint class CallService : VectorService() { private val connections = mutableMapOf() - private val knownCalls = mutableSetOf() + private val knownCalls = mutableMapOf() private val connectedCallIds = mutableSetOf() - private lateinit var notificationManager: NotificationManagerCompat - private lateinit var notificationUtils: NotificationUtils - private lateinit var callManager: WebRtcCallManager - private lateinit var avatarRenderer: AvatarRenderer - private lateinit var alertManager: PopupAlertManager + lateinit var notificationManager: NotificationManagerCompat + @Inject lateinit var notificationUtils: NotificationUtils + @Inject lateinit var callManager: WebRtcCallManager + @Inject lateinit var avatarRenderer: AvatarRenderer + @Inject lateinit var alertManager: PopupAlertManager private var callRingPlayerIncoming: CallRingPlayerIncoming? = null private var callRingPlayerOutgoing: CallRingPlayerOutgoing? = null @@ -79,10 +83,6 @@ class CallService : VectorService() { override fun onCreate() { super.onCreate() notificationManager = NotificationManagerCompat.from(this) - notificationUtils = vectorComponent().notificationUtils() - callManager = vectorComponent().webRtcCallManager() - avatarRenderer = vectorComponent().avatarRenderer() - alertManager = vectorComponent().alertManager() callRingPlayerIncoming = CallRingPlayerIncoming(applicationContext, notificationUtils) callRingPlayerOutgoing = CallRingPlayerOutgoing(applicationContext) } @@ -165,7 +165,7 @@ class CallService : VectorService() { val incomingCallAlert = IncomingCallAlert(callId, shouldBeDisplayedIn = { activity -> if (activity is VectorCallActivity) { - activity.intent.getParcelableExtra(MvRx.KEY_ARG)?.callId != call.callId + activity.intent.getParcelableExtra(Mavericks.KEY_ARG)?.callId != call.callId } else true } ).apply { @@ -190,7 +190,7 @@ class CallService : VectorService() { } else { notificationManager.notify(callId.hashCode(), notification) } - knownCalls.add(callInformation) + knownCalls[callId] = callInformation } private fun handleCallTerminated(intent: Intent) { @@ -198,20 +198,22 @@ class CallService : VectorService() { val endCallReason = intent.getSerializableExtra(EXTRA_END_CALL_REASON) as EndCallReason val rejected = intent.getBooleanExtra(EXTRA_END_CALL_REJECTED, false) alertManager.cancelAlert(callId) - val terminatedCall = knownCalls.firstOrNull { it.callId == callId } + val terminatedCall = knownCalls.remove(callId) if (terminatedCall == null) { - Timber.tag(loggerTag.value).v("Call terminated for unknown call $callId$") + Timber.tag(loggerTag.value).v("Call terminated for unknown call $callId") handleUnexpectedState(callId) return } - knownCalls.remove(terminatedCall) + val notification = notificationUtils.buildCallEndedNotification(false) + val notificationId = callId.hashCode() + startForeground(notificationId, notification) if (knownCalls.isEmpty()) { + Timber.tag(loggerTag.value).v("No more call, stop the service") + stopForeground(true) mediaSession?.isActive = false myStopSelf() } val wasConnected = connectedCallIds.remove(callId) - val notification = notificationUtils.buildCallEndedNotification(terminatedCall.isVideoCall) - notificationManager.notify(callId.hashCode(), notification) if (!wasConnected && !terminatedCall.isOutgoing && !rejected && endCallReason != EndCallReason.ANSWERED_ELSEWHERE) { val missedCallNotification = notificationUtils.buildCallMissedNotification(terminatedCall) notificationManager.notify(MISSED_CALL_TAG, terminatedCall.nativeRoomId.hashCode(), missedCallNotification) @@ -243,7 +245,7 @@ class CallService : VectorService() { } else { notificationManager.notify(callId.hashCode(), notification) } - knownCalls.add(callInformation) + knownCalls[callId] = callInformation } /** @@ -267,18 +269,19 @@ class CallService : VectorService() { } else { notificationManager.notify(callId.hashCode(), notification) } - knownCalls.add(callInformation) + knownCalls[callId] = callInformation } private fun handleUnexpectedState(callId: String?) { Timber.tag(loggerTag.value).v("Fallback to clear everything") callRingPlayerIncoming?.stop() callRingPlayerOutgoing?.stop() - if (callId != null) { - notificationManager.cancel(callId.hashCode()) - } val notification = notificationUtils.buildCallEndedNotification(false) - startForeground(DEFAULT_NOTIFICATION_ID, notification) + if (callId != null) { + startForeground(callId.hashCode(), notification) + } else { + startForeground(DEFAULT_NOTIFICATION_ID, notification) + } if (knownCalls.isEmpty()) { mediaSession?.isActive = false myStopSelf() @@ -294,7 +297,7 @@ class CallService : VectorService() { callId = this.callId, nativeRoomId = this.nativeRoomId, opponentUserId = this.mxCall.opponentUserId, - opponentMatrixItem = vectorComponent().activeSessionHolder().getSafeActiveSession()?.let { + opponentMatrixItem = singletonEntryPoint().activeSessionHolder().getSafeActiveSession()?.let { this.getOpponentAsMatrixItem(it) }, isVideoCall = this.mxCall.isVideoCall, @@ -371,7 +374,7 @@ class CallService : VectorService() { putExtra(EXTRA_END_CALL_REASON, endCallReason) putExtra(EXTRA_END_CALL_REJECTED, rejected) } - ContextCompat.startForegroundService(context, intent) + context.startService(intent) } } diff --git a/vector/src/main/java/im/vector/app/core/services/VectorSyncService.kt b/vector/src/main/java/im/vector/app/core/services/VectorSyncService.kt index 2a00e94976..1fd86cac4d 100644 --- a/vector/src/main/java/im/vector/app/core/services/VectorSyncService.kt +++ b/vector/src/main/java/im/vector/app/core/services/VectorSyncService.kt @@ -30,13 +30,15 @@ import androidx.work.WorkManager import androidx.work.WorkRequest import androidx.work.Worker import androidx.work.WorkerParameters +import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R -import im.vector.app.core.extensions.vectorComponent import im.vector.app.features.notifications.NotificationUtils import im.vector.app.features.settings.BackgroundSyncMode import org.matrix.android.sdk.internal.session.sync.job.SyncService import timber.log.Timber +import javax.inject.Inject +@AndroidEntryPoint class VectorSyncService : SyncService() { companion object { @@ -71,12 +73,7 @@ class VectorSyncService : SyncService() { } } - private lateinit var notificationUtils: NotificationUtils - - override fun onCreate() { - super.onCreate() - notificationUtils = vectorComponent().notificationUtils() - } + @Inject lateinit var notificationUtils: NotificationUtils override fun getDefaultSyncDelaySeconds() = BackgroundSyncMode.DEFAULT_SYNC_DELAY_SECONDS diff --git a/vector/src/main/java/im/vector/app/core/ui/bottomsheet/BottomSheetGeneric.kt b/vector/src/main/java/im/vector/app/core/ui/bottomsheet/BottomSheetGeneric.kt index e773993b21..bd7a07c640 100644 --- a/vector/src/main/java/im/vector/app/core/ui/bottomsheet/BottomSheetGeneric.kt +++ b/vector/src/main/java/im/vector/app/core/ui/bottomsheet/BottomSheetGeneric.kt @@ -26,7 +26,6 @@ import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.configureWith import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment import im.vector.app.databinding.BottomSheetGenericListBinding - import javax.inject.Inject /** diff --git a/vector/src/main/java/im/vector/app/core/ui/bottomsheet/BottomSheetGenericController.kt b/vector/src/main/java/im/vector/app/core/ui/bottomsheet/BottomSheetGenericController.kt index f0715e6bba..6e92549809 100644 --- a/vector/src/main/java/im/vector/app/core/ui/bottomsheet/BottomSheetGenericController.kt +++ b/vector/src/main/java/im/vector/app/core/ui/bottomsheet/BottomSheetGenericController.kt @@ -20,8 +20,8 @@ import com.airbnb.epoxy.TypedEpoxyController /** * Epoxy controller for generic bottom sheet actions */ -abstract class BottomSheetGenericController - : TypedEpoxyController() { +abstract class BottomSheetGenericController : + TypedEpoxyController() { var listener: Listener? = null diff --git a/vector/src/main/java/im/vector/app/core/ui/bottomsheet/BottomSheetGenericState.kt b/vector/src/main/java/im/vector/app/core/ui/bottomsheet/BottomSheetGenericState.kt index 38c81a7ef6..0f57600b13 100644 --- a/vector/src/main/java/im/vector/app/core/ui/bottomsheet/BottomSheetGenericState.kt +++ b/vector/src/main/java/im/vector/app/core/ui/bottomsheet/BottomSheetGenericState.kt @@ -16,6 +16,6 @@ package im.vector.app.core.ui.bottomsheet -import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MavericksState -abstract class BottomSheetGenericState : MvRxState +abstract class BottomSheetGenericState : MavericksState diff --git a/vector/src/main/java/im/vector/app/core/ui/bottomsheet/BottomSheetGenericViewModel.kt b/vector/src/main/java/im/vector/app/core/ui/bottomsheet/BottomSheetGenericViewModel.kt index 6cc2c4c981..bde87a5588 100644 --- a/vector/src/main/java/im/vector/app/core/ui/bottomsheet/BottomSheetGenericViewModel.kt +++ b/vector/src/main/java/im/vector/app/core/ui/bottomsheet/BottomSheetGenericViewModel.kt @@ -16,12 +16,12 @@ package im.vector.app.core.ui.bottomsheet -import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MavericksState import im.vector.app.core.platform.EmptyAction import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel -abstract class BottomSheetGenericViewModel(initialState: State) : +abstract class BottomSheetGenericViewModel(initialState: State) : VectorViewModel(initialState) { override fun handle(action: EmptyAction) { 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 index 5aee73ee69..13db5ddcb3 100644 --- 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 @@ -36,7 +36,7 @@ class CurrentCallsViewPresenter { this.currentCall = currentCall this.currentCall?.addListener(tickListener) this.calls = calls - val hasActiveCall = currentCall != null + val hasActiveCall = calls.isNotEmpty() currentCallsView?.isVisible = hasActiveCall currentCallsView?.render(calls, currentCall?.formattedDuration() ?: "") } diff --git a/vector/src/main/java/im/vector/app/core/ui/views/FailedMessagesWarningView.kt b/vector/src/main/java/im/vector/app/core/ui/views/FailedMessagesWarningView.kt index f9518552a3..755230b5bf 100644 --- a/vector/src/main/java/im/vector/app/core/ui/views/FailedMessagesWarningView.kt +++ b/vector/src/main/java/im/vector/app/core/ui/views/FailedMessagesWarningView.kt @@ -19,7 +19,6 @@ package im.vector.app.core.ui.views import android.content.Context import android.util.AttributeSet import androidx.constraintlayout.widget.ConstraintLayout -import androidx.core.view.isVisible import im.vector.app.R import im.vector.app.databinding.ViewFailedMessagesWarningBinding @@ -49,8 +48,4 @@ class FailedMessagesWarningView @JvmOverloads constructor( views.failedMessagesDeleteAllButton.setOnClickListener { callback?.onDeleteAllClicked() } views.failedMessagesRetryButton.setOnClickListener { callback?.onRetryClicked() } } - - fun render(hasFailedMessages: Boolean) { - isVisible = hasFailedMessages - } } diff --git a/vector/src/main/java/im/vector/app/core/ui/views/KeysBackupBanner.kt b/vector/src/main/java/im/vector/app/core/ui/views/KeysBackupBanner.kt index 153cac2ac7..94c1ab6576 100755 --- a/vector/src/main/java/im/vector/app/core/ui/views/KeysBackupBanner.kt +++ b/vector/src/main/java/im/vector/app/core/ui/views/KeysBackupBanner.kt @@ -25,7 +25,6 @@ import androidx.core.view.isVisible import im.vector.app.R import im.vector.app.core.di.DefaultSharedPreferences import im.vector.app.databinding.ViewKeysBackupBannerBinding - import timber.log.Timber /** @@ -137,8 +136,8 @@ class KeysBackupBanner @JvmOverloads constructor( } private fun renderSetup(nbOfKeys: Int) { - if (nbOfKeys == 0 - || DefaultSharedPreferences.getInstance(context).getBoolean(BANNER_SETUP_DO_NOT_SHOW_AGAIN, false)) { + if (nbOfKeys == 0 || + DefaultSharedPreferences.getInstance(context).getBoolean(BANNER_SETUP_DO_NOT_SHOW_AGAIN, false)) { // Do not display the setup banner if there is no keys to backup, or if the user has already closed it isVisible = false } else { diff --git a/vector/src/main/java/im/vector/app/core/ui/views/NotificationAreaView.kt b/vector/src/main/java/im/vector/app/core/ui/views/NotificationAreaView.kt index 463d94b288..94809d2981 100644 --- a/vector/src/main/java/im/vector/app/core/ui/views/NotificationAreaView.kt +++ b/vector/src/main/java/im/vector/app/core/ui/views/NotificationAreaView.kt @@ -30,7 +30,6 @@ import im.vector.app.core.extensions.exhaustive import im.vector.app.core.utils.DimensionConverter import im.vector.app.databinding.ViewNotificationAreaBinding import im.vector.app.features.themes.ThemeUtils - import me.gujun.android.span.span import me.saket.bettermovementmethod.BetterLinkMovementMethod import org.matrix.android.sdk.api.failure.MatrixError @@ -50,7 +49,7 @@ class NotificationAreaView @JvmOverloads constructor( var delegate: Delegate? = null private var state: State = State.Initial - private lateinit var views : ViewNotificationAreaBinding + private lateinit var views: ViewNotificationAreaBinding init { setupView() diff --git a/vector/src/main/java/im/vector/app/core/ui/views/PasswordStrengthBar.kt b/vector/src/main/java/im/vector/app/core/ui/views/PasswordStrengthBar.kt index 2f6c4b45cf..a984707bf7 100644 --- a/vector/src/main/java/im/vector/app/core/ui/views/PasswordStrengthBar.kt +++ b/vector/src/main/java/im/vector/app/core/ui/views/PasswordStrengthBar.kt @@ -35,8 +35,8 @@ import im.vector.app.databinding.ViewPasswordStrengthBarBinding class PasswordStrengthBar @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, - defStyleAttr: Int = 0) - : LinearLayout(context, attrs, defStyleAttr) { + defStyleAttr: Int = 0) : + LinearLayout(context, attrs, defStyleAttr) { private val views: ViewPasswordStrengthBarBinding diff --git a/vector/src/main/java/im/vector/app/core/ui/views/PresenceStateImageView.kt b/vector/src/main/java/im/vector/app/core/ui/views/PresenceStateImageView.kt new file mode 100644 index 0000000000..301f8afdc9 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/ui/views/PresenceStateImageView.kt @@ -0,0 +1,54 @@ +/* + * 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 android.content.Context +import android.util.AttributeSet +import androidx.appcompat.widget.AppCompatImageView +import androidx.core.view.isVisible +import im.vector.app.R +import org.matrix.android.sdk.api.session.presence.model.PresenceEnum +import org.matrix.android.sdk.api.session.presence.model.UserPresence + +/** + * Custom ImageView to dynamically render Presence state in multiple screens + */ +class PresenceStateImageView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : AppCompatImageView(context, attrs, defStyleAttr) { + + fun render(showPresence: Boolean = true, userPresence: UserPresence?) { + isVisible = showPresence && userPresence != null + + when (userPresence?.presence) { + PresenceEnum.ONLINE -> { + setImageResource(R.drawable.ic_presence_online) + contentDescription = context.getString(R.string.a11y_presence_online) + } + PresenceEnum.UNAVAILABLE -> { + setImageResource(R.drawable.ic_presence_offline) + contentDescription = context.getString(R.string.a11y_presence_unavailable) + } + PresenceEnum.OFFLINE -> { + setImageResource(R.drawable.ic_presence_offline) + contentDescription = context.getString(R.string.a11y_presence_offline) + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/core/ui/views/ReadReceiptsView.kt b/vector/src/main/java/im/vector/app/core/ui/views/ReadReceiptsView.kt index 02b351737e..3a79e7e328 100644 --- a/vector/src/main/java/im/vector/app/core/ui/views/ReadReceiptsView.kt +++ b/vector/src/main/java/im/vector/app/core/ui/views/ReadReceiptsView.kt @@ -37,7 +37,7 @@ class ReadReceiptsView @JvmOverloads constructor( defStyleAttr: Int = 0 ) : LinearLayout(context, attrs, defStyleAttr) { - private val views : ViewReadReceiptsBinding + private val views: ViewReadReceiptsBinding init { setupView() diff --git a/vector/src/main/java/im/vector/app/core/utils/Dialogs.kt b/vector/src/main/java/im/vector/app/core/utils/Dialogs.kt index 7806f2603d..ed3ae00567 100644 --- a/vector/src/main/java/im/vector/app/core/utils/Dialogs.kt +++ b/vector/src/main/java/im/vector/app/core/utils/Dialogs.kt @@ -20,6 +20,7 @@ import android.content.Context import android.webkit.WebView import android.webkit.WebViewClient import com.google.android.material.dialog.MaterialAlertDialogBuilder +import im.vector.app.R /** * Open a web view above the current activity. @@ -38,3 +39,17 @@ fun Context.displayInWebView(url: String) { .setPositiveButton(android.R.string.ok, null) .show() } + +fun Context.showIdentityServerConsentDialog(configuredIdentityServer: String?, policyLinkCallback: () -> Unit, consentCallBack: (() -> Unit)) { + MaterialAlertDialogBuilder(this) + .setTitle(R.string.identity_server_consent_dialog_title) + .setMessage(getString(R.string.identity_server_consent_dialog_content, configuredIdentityServer ?: "")) + .setPositiveButton(R.string.yes) { _, _ -> + consentCallBack.invoke() + } + .setNeutralButton(R.string.identity_server_consent_dialog_neutral_policy) { _, _ -> + policyLinkCallback.invoke() + } + .setNegativeButton(R.string.no, null) + .show() +} diff --git a/vector/src/main/java/im/vector/app/core/utils/PermissionsTools.kt b/vector/src/main/java/im/vector/app/core/utils/PermissionsTools.kt index 9f1497a40a..ba396ed252 100644 --- a/vector/src/main/java/im/vector/app/core/utils/PermissionsTools.kt +++ b/vector/src/main/java/im/vector/app/core/utils/PermissionsTools.kt @@ -52,8 +52,7 @@ private var permissionDialogDisplayed = false * So when the user does not grant the permission and check the box do not ask again, this boolean will be false. * Only useful if the first boolean is false */ -fun ComponentActivity.registerForPermissionsResult(lambda: (allGranted: Boolean, deniedPermanently: Boolean) -> Unit) - : ActivityResultLauncher> { +fun ComponentActivity.registerForPermissionsResult(lambda: (allGranted: Boolean, deniedPermanently: Boolean) -> Unit): ActivityResultLauncher> { return registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { result -> onPermissionResult(result, lambda) } diff --git a/vector/src/main/java/im/vector/app/core/utils/SnapHelperUtils.kt b/vector/src/main/java/im/vector/app/core/utils/SnapHelperUtils.kt index e039e0bde5..070c953a3f 100644 --- a/vector/src/main/java/im/vector/app/core/utils/SnapHelperUtils.kt +++ b/vector/src/main/java/im/vector/app/core/utils/SnapHelperUtils.kt @@ -59,8 +59,8 @@ class SnapOnScrollListener( } override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { - if (behavior == Behavior.NOTIFY_ON_SCROLL_STATE_IDLE - && newState == RecyclerView.SCROLL_STATE_IDLE) { + if (behavior == Behavior.NOTIFY_ON_SCROLL_STATE_IDLE && + newState == RecyclerView.SCROLL_STATE_IDLE) { maybeNotifySnapPositionChange(recyclerView) } } diff --git a/vector/src/main/java/im/vector/app/core/utils/SystemUtils.kt b/vector/src/main/java/im/vector/app/core/utils/SystemUtils.kt index b12c1b7369..966b38828e 100644 --- a/vector/src/main/java/im/vector/app/core/utils/SystemUtils.kt +++ b/vector/src/main/java/im/vector/app/core/utils/SystemUtils.kt @@ -48,8 +48,8 @@ import im.vector.app.features.notifications.NotificationUtils */ fun isIgnoringBatteryOptimizations(context: Context): Boolean { // no issue before Android M, battery optimisations did not exist - return Build.VERSION.SDK_INT < Build.VERSION_CODES.M - || context.getSystemService()?.isIgnoringBatteryOptimizations(context.packageName) == true + return Build.VERSION.SDK_INT < Build.VERSION_CODES.M || + context.getSystemService()?.isIgnoringBatteryOptimizations(context.packageName) == true } fun isAirplaneModeOn(context: Context): Boolean { diff --git a/vector/src/main/java/im/vector/app/features/MainActivity.kt b/vector/src/main/java/im/vector/app/features/MainActivity.kt index 583aae260b..8e0995b426 100644 --- a/vector/src/main/java/im/vector/app/features/MainActivity.kt +++ b/vector/src/main/java/im/vector/app/features/MainActivity.kt @@ -24,9 +24,9 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import com.bumptech.glide.Glide import com.google.android.material.dialog.MaterialAlertDialogBuilder +import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder -import im.vector.app.core.di.ScreenComponent import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.extensions.startSyncing import im.vector.app.core.platform.VectorBaseActivity @@ -45,10 +45,10 @@ import im.vector.app.features.signout.soft.SoftLogoutActivity import im.vector.app.features.signout.soft.SoftLogoutActivity2 import im.vector.app.features.themes.ActivityOtherThemes import im.vector.app.features.ui.UiStateRepository -import kotlinx.parcelize.Parcelize import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import kotlinx.parcelize.Parcelize import org.matrix.android.sdk.api.failure.GlobalError import timber.log.Timber import javax.inject.Inject @@ -67,6 +67,7 @@ data class MainActivityArgs( * This Activity, when started with argument, is also doing some cleanup when user signs out, * clears cache, is logged out, or is soft logged out */ +@AndroidEntryPoint class MainActivity : VectorBaseActivity(), UnlockedActivity { companion object { @@ -98,10 +99,6 @@ class MainActivity : VectorBaseActivity(), UnlockedActivity @Inject lateinit var pinLocker: PinLocker @Inject lateinit var popupAlertManager: PopupAlertManager - override fun injectWith(injector: ScreenComponent) { - injector.inject(this) - } - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) args = parseArgs() @@ -220,20 +217,20 @@ class MainActivity : VectorBaseActivity(), UnlockedActivity private fun startNextActivityAndFinish(ignoreClearCredentials: Boolean = false) { val intent = when { - args.clearCredentials - && !ignoreClearCredentials - && (!args.isUserLoggedOut || args.isAccountDeactivated) -> { + args.clearCredentials && + !ignoreClearCredentials && + (!args.isUserLoggedOut || args.isAccountDeactivated) -> { // User has explicitly asked to log out or deactivated his account navigator.openLogin(this, null) null } - args.isSoftLogout -> + args.isSoftLogout -> // The homeserver has invalidated the token, with a soft logout getSoftLogoutActivityIntent() - args.isUserLoggedOut -> + args.isUserLoggedOut -> // the homeserver has invalidated the token (password changed, device deleted, other security reasons) SignedOutActivity.newIntent(this) - sessionHolder.hasActiveSession() -> + sessionHolder.hasActiveSession() -> // We have a session. // Check it can be opened if (sessionHolder.getActiveSession().isOpenable) { @@ -242,7 +239,7 @@ class MainActivity : VectorBaseActivity(), UnlockedActivity // The token is still invalid getSoftLogoutActivityIntent() } - else -> { + else -> { // First start, or no active session navigator.openLogin(this, null) null 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 eba5dadeda..35644e1843 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 @@ -53,8 +53,8 @@ private const val ANIMATION_DURATION = 250 */ class AttachmentTypeSelectorView(context: Context, inflater: LayoutInflater, - var callback: Callback?) - : PopupWindow(context) { + var callback: Callback?) : + PopupWindow(context) { interface Callback { fun onTypeSelected(type: Type) diff --git a/vector/src/main/java/im/vector/app/features/attachments/ContentAttachmentData.kt b/vector/src/main/java/im/vector/app/features/attachments/ContentAttachmentData.kt index 0502f2b0ad..9805b6f755 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/ContentAttachmentData.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/ContentAttachmentData.kt @@ -27,9 +27,9 @@ private val listOfPreviewableMimeTypes = listOf( fun ContentAttachmentData.isPreviewable(): Boolean { // Preview supports image and video - return (type == ContentAttachmentData.Type.IMAGE - && listOfPreviewableMimeTypes.contains(getSafeMimeType() ?: "")) - || type == ContentAttachmentData.Type.VIDEO + return (type == ContentAttachmentData.Type.IMAGE && + listOfPreviewableMimeTypes.contains(getSafeMimeType() ?: "")) || + type == ContentAttachmentData.Type.VIDEO } data class GroupedContentAttachmentData( diff --git a/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewActivity.kt b/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewActivity.kt index 6c25f688bd..939dd9f11d 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewActivity.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewActivity.kt @@ -20,6 +20,7 @@ package im.vector.app.features.attachments.preview import android.content.Context import android.content.Intent import com.google.android.material.appbar.MaterialToolbar +import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R import im.vector.app.core.extensions.addFragment import im.vector.app.core.platform.ToolbarConfigurable @@ -28,6 +29,7 @@ import im.vector.app.databinding.ActivitySimpleBinding import im.vector.app.features.themes.ActivityOtherThemes import org.matrix.android.sdk.api.session.content.ContentAttachmentData +@AndroidEntryPoint class AttachmentsPreviewActivity : VectorBaseActivity(), ToolbarConfigurable { companion object { diff --git a/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewViewModel.kt b/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewViewModel.kt index 28d617e613..0a0e700ce9 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewViewModel.kt @@ -20,8 +20,8 @@ package im.vector.app.features.attachments.preview import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel -class AttachmentsPreviewViewModel(initialState: AttachmentsPreviewViewState) - : VectorViewModel(initialState) { +class AttachmentsPreviewViewModel(initialState: AttachmentsPreviewViewState) : + VectorViewModel(initialState) { override fun handle(action: AttachmentsPreviewAction) { when (action) { diff --git a/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewViewState.kt b/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewViewState.kt index cee2a7ddd7..8b8208fc4f 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewViewState.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewViewState.kt @@ -17,14 +17,14 @@ package im.vector.app.features.attachments.preview -import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MavericksState import org.matrix.android.sdk.api.session.content.ContentAttachmentData data class AttachmentsPreviewViewState( val attachments: List, val currentAttachmentIndex: Int = 0, val sendImagesWithOriginalSize: Boolean = false -) : MvRxState { +) : MavericksState { constructor(args: AttachmentsPreviewArgs) : this(attachments = args.attachments) } diff --git a/vector/src/main/java/im/vector/app/features/attachments/preview/Extensions.kt b/vector/src/main/java/im/vector/app/features/attachments/preview/Extensions.kt index 853f9f8997..672cde977d 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/preview/Extensions.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/preview/Extensions.kt @@ -24,7 +24,7 @@ import org.matrix.android.sdk.api.util.MimeTypes.isMimeTypeImage * All images are editable, expect Gif */ fun ContentAttachmentData.isEditable(): Boolean { - return type == ContentAttachmentData.Type.IMAGE - && getSafeMimeType()?.isMimeTypeImage() == true - && getSafeMimeType() != MimeTypes.Gif + return type == ContentAttachmentData.Type.IMAGE && + getSafeMimeType()?.isMimeTypeImage() == true && + getSafeMimeType() != MimeTypes.Gif } diff --git a/vector/src/main/java/im/vector/app/features/auth/ReAuthActivity.kt b/vector/src/main/java/im/vector/app/features/auth/ReAuthActivity.kt index ce23111a95..e3c9795b13 100644 --- a/vector/src/main/java/im/vector/app/features/auth/ReAuthActivity.kt +++ b/vector/src/main/java/im/vector/app/features/auth/ReAuthActivity.kt @@ -26,11 +26,11 @@ import androidx.browser.customtabs.CustomTabsCallback import androidx.browser.customtabs.CustomTabsClient import androidx.browser.customtabs.CustomTabsServiceConnection import androidx.browser.customtabs.CustomTabsSession -import com.airbnb.mvrx.MvRx +import com.airbnb.mvrx.Mavericks import com.airbnb.mvrx.viewModel import com.airbnb.mvrx.withState +import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R -import im.vector.app.core.di.ScreenComponent import im.vector.app.core.extensions.addFragment import im.vector.app.core.platform.SimpleFragmentActivity import im.vector.app.core.utils.openUrlInChromeCustomTab @@ -42,7 +42,8 @@ import org.matrix.android.sdk.api.auth.registration.nextUncompletedStage import timber.log.Timber import javax.inject.Inject -class ReAuthActivity : SimpleFragmentActivity(), ReAuthViewModel.Factory { +@AndroidEntryPoint +class ReAuthActivity : SimpleFragmentActivity() { @Parcelize data class Args( @@ -59,14 +60,6 @@ class ReAuthActivity : SimpleFragmentActivity(), ReAuthViewModel.Factory { private var customTabsSession: CustomTabsSession? = null @Inject lateinit var authenticationService: AuthenticationService - @Inject lateinit var reAuthViewModelFactory: ReAuthViewModel.Factory - - override fun create(initialState: ReAuthState) = reAuthViewModelFactory.create(initialState) - - override fun injectWith(injector: ScreenComponent) { - super.injectWith(injector) - injector.inject(this) - } private val sharedViewModel: ReAuthViewModel by viewModel() @@ -78,7 +71,7 @@ class ReAuthActivity : SimpleFragmentActivity(), ReAuthViewModel.Factory { val title = intent.extras?.getString(EXTRA_REASON_TITLE) ?: getString(R.string.re_authentication_activity_title) supportActionBar?.setTitle(title) ?: run { setTitle(title) } -// val authArgs = intent.getParcelableExtra(MvRx.KEY_ARG) +// val authArgs = intent.getParcelableExtra(Mavericks.KEY_ARG) // For the sso flow we can for now only rely on the fallback flow, that handles all // the UI, due to the sandbox nature of CCT (chrome custom tab) we cannot get much information @@ -221,7 +214,7 @@ class ReAuthActivity : SimpleFragmentActivity(), ReAuthViewModel.Factory { } } return Intent(context, ReAuthActivity::class.java).apply { - putExtra(MvRx.KEY_ARG, Args(authType, reasonTitle, fromError.session, lastErrorCode, resultKeyStoreAlias)) + putExtra(Mavericks.KEY_ARG, Args(authType, reasonTitle, fromError.session, lastErrorCode, resultKeyStoreAlias)) } } } diff --git a/vector/src/main/java/im/vector/app/features/auth/ReAuthState.kt b/vector/src/main/java/im/vector/app/features/auth/ReAuthState.kt index 075ef758b8..733516c691 100644 --- a/vector/src/main/java/im/vector/app/features/auth/ReAuthState.kt +++ b/vector/src/main/java/im/vector/app/features/auth/ReAuthState.kt @@ -16,7 +16,7 @@ package im.vector.app.features.auth -import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MavericksState data class ReAuthState( val title: String? = null, @@ -25,7 +25,7 @@ data class ReAuthState( val ssoFallbackPageWasShown: Boolean = false, val lastErrorCode: String? = null, val resultKeyStoreAlias: String = "" -) : MvRxState { +) : MavericksState { constructor(args: ReAuthActivity.Args) : this( args.title, args.session, diff --git a/vector/src/main/java/im/vector/app/features/auth/ReAuthViewModel.kt b/vector/src/main/java/im/vector/app/features/auth/ReAuthViewModel.kt index edbceae20f..830fd4e79d 100644 --- a/vector/src/main/java/im/vector/app/features/auth/ReAuthViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/auth/ReAuthViewModel.kt @@ -16,13 +16,12 @@ package im.vector.app.features.auth -import com.airbnb.mvrx.ActivityViewModelContext -import com.airbnb.mvrx.FragmentViewModelContext -import com.airbnb.mvrx.MvRxViewModelFactory -import com.airbnb.mvrx.ViewModelContext +import com.airbnb.mvrx.MavericksViewModelFactory import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.platform.VectorViewModel import org.matrix.android.sdk.api.auth.data.LoginFlowTypes import org.matrix.android.sdk.api.session.Session @@ -35,20 +34,11 @@ class ReAuthViewModel @AssistedInject constructor( ) : VectorViewModel(initialState) { @AssistedFactory - interface Factory { - fun create(initialState: ReAuthState): ReAuthViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: ReAuthState): ReAuthViewModel } - companion object : MvRxViewModelFactory { - - override fun create(viewModelContext: ViewModelContext, state: ReAuthState): ReAuthViewModel? { - 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") - } - } + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() override fun handle(action: ReAuthActions) = withState { state -> when (action) { diff --git a/vector/src/main/java/im/vector/app/features/autocomplete/AutocompleteMatrixItem.kt b/vector/src/main/java/im/vector/app/features/autocomplete/AutocompleteMatrixItem.kt index 3d70615abf..dba2661927 100644 --- a/vector/src/main/java/im/vector/app/features/autocomplete/AutocompleteMatrixItem.kt +++ b/vector/src/main/java/im/vector/app/features/autocomplete/AutocompleteMatrixItem.kt @@ -26,6 +26,7 @@ import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.epoxy.onClick import im.vector.app.core.extensions.setTextOrHide +import im.vector.app.features.displayname.getBestName import im.vector.app.features.home.AvatarRenderer import org.matrix.android.sdk.api.util.MatrixItem diff --git a/vector/src/main/java/im/vector/app/features/autocomplete/command/CommandAutocompletePolicy.kt b/vector/src/main/java/im/vector/app/features/autocomplete/command/CommandAutocompletePolicy.kt index 603ec64a7c..08f61be0f8 100644 --- a/vector/src/main/java/im/vector/app/features/autocomplete/command/CommandAutocompletePolicy.kt +++ b/vector/src/main/java/im/vector/app/features/autocomplete/command/CommandAutocompletePolicy.kt @@ -37,8 +37,8 @@ class CommandAutocompletePolicy @Inject constructor() : AutocompletePolicy { // Only if text which starts with '/' and without space override fun shouldShowPopup(text: Spannable?, cursorPos: Int): Boolean { - return enabled && text?.startsWith("/") == true - && !text.contains(" ") + return enabled && text?.startsWith("/") == true && + !text.contains(" ") } override fun shouldDismissPopup(text: Spannable?, cursorPos: Int): Boolean { diff --git a/vector/src/main/java/im/vector/app/features/autocomplete/emoji/AutocompleteEmojiPresenter.kt b/vector/src/main/java/im/vector/app/features/autocomplete/emoji/AutocompleteEmojiPresenter.kt index bf180746de..4f272c7a24 100644 --- a/vector/src/main/java/im/vector/app/features/autocomplete/emoji/AutocompleteEmojiPresenter.kt +++ b/vector/src/main/java/im/vector/app/features/autocomplete/emoji/AutocompleteEmojiPresenter.kt @@ -21,6 +21,11 @@ import androidx.recyclerview.widget.RecyclerView import im.vector.app.features.autocomplete.AutocompleteClickListener import im.vector.app.features.autocomplete.RecyclerViewPresenter import im.vector.app.features.reactions.data.EmojiDataSource +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancelChildren +import kotlinx.coroutines.launch import javax.inject.Inject class AutocompleteEmojiPresenter @Inject constructor(context: Context, @@ -28,11 +33,14 @@ class AutocompleteEmojiPresenter @Inject constructor(context: Context, private val controller: AutocompleteEmojiController) : RecyclerViewPresenter(context), AutocompleteClickListener { + private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main) + init { controller.listener = this } fun clear() { + coroutineScope.coroutineContext.cancelChildren() controller.listener = null } @@ -45,12 +53,14 @@ class AutocompleteEmojiPresenter @Inject constructor(context: Context, } override fun onQuery(query: CharSequence?) { - val data = if (query.isNullOrBlank()) { - // Return common emojis - emojiDataSource.getQuickReactions() - } else { - emojiDataSource.filterWith(query.toString()) + coroutineScope.launch { + val data = if (query.isNullOrBlank()) { + // Return common emojis + emojiDataSource.getQuickReactions() + } else { + emojiDataSource.filterWith(query.toString()) + } + controller.setData(data) } - controller.setData(data) } } diff --git a/vector/src/main/java/im/vector/app/features/autocomplete/member/AutocompleteMemberPresenter.kt b/vector/src/main/java/im/vector/app/features/autocomplete/member/AutocompleteMemberPresenter.kt index aa0c10e0a2..4976cb39b9 100644 --- a/vector/src/main/java/im/vector/app/features/autocomplete/member/AutocompleteMemberPresenter.kt +++ b/vector/src/main/java/im/vector/app/features/autocomplete/member/AutocompleteMemberPresenter.kt @@ -19,8 +19,8 @@ package im.vector.app.features.autocomplete.member import android.content.Context import androidx.recyclerview.widget.RecyclerView import dagger.assisted.Assisted -import dagger.assisted.AssistedInject import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import im.vector.app.features.autocomplete.AutocompleteClickListener import im.vector.app.features.autocomplete.RecyclerViewPresenter import org.matrix.android.sdk.api.query.QueryStringValue @@ -35,7 +35,7 @@ class AutocompleteMemberPresenter @AssistedInject constructor(context: Context, private val controller: AutocompleteMemberController ) : RecyclerViewPresenter(context), AutocompleteClickListener { - private val room = session.getRoom(roomId)!! + private val room by lazy { session.getRoom(roomId)!! } init { controller.listener = this 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 f9e2338077..b4f49db781 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,10 +23,12 @@ import android.view.ViewGroup import androidx.core.content.ContextCompat import androidx.core.view.isVisible import com.airbnb.mvrx.activityViewModel +import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment import im.vector.app.databinding.BottomSheetCallControlsBinding +@AndroidEntryPoint class CallControlsBottomSheet : VectorBaseBottomSheetDialogFragment() { override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): BottomSheetCallControlsBinding { return BottomSheetCallControlsBinding.inflate(inflater, container, false) 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 fb5e48af98..4b0ea412f3 100644 --- a/vector/src/main/java/im/vector/app/features/call/SharedKnownCallsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/call/SharedKnownCallsViewModel.kt @@ -41,7 +41,7 @@ class SharedKnownCallsViewModel @Inject constructor( } } - private val currentCallListener = object : WebRtcCallManager.CurrentCallListener { + private val callManagerListener = object : WebRtcCallManager.Listener { override fun onCurrentCallChange(call: WebRtcCall?) { val knownCalls = callManager.getCalls() liveKnownCalls.postValue(knownCalls) @@ -50,12 +50,17 @@ class SharedKnownCallsViewModel @Inject constructor( it.addListener(callListener) } } + + override fun onCallEnded(callId: String) { + val knownCalls = callManager.getCalls() + liveKnownCalls.postValue(knownCalls) + } } init { val knownCalls = callManager.getCalls() liveKnownCalls.postValue(knownCalls) - callManager.addCurrentCallListener(currentCallListener) + callManager.addListener(callManagerListener) knownCalls.forEach { it.addListener(callListener) } @@ -65,7 +70,7 @@ class SharedKnownCallsViewModel @Inject constructor( callManager.getCalls().forEach { it.removeListener(callListener) } - callManager.removeCurrentCallListener(currentCallListener) + callManager.removeListener(callManagerListener) super.onCleared() } } diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt index f71dcc0635..879a357dfb 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 @@ -36,13 +36,13 @@ import androidx.core.content.getSystemService import androidx.core.view.isInvisible import androidx.core.view.isVisible import com.airbnb.mvrx.Fail -import com.airbnb.mvrx.MvRx +import com.airbnb.mvrx.Mavericks 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 dagger.hilt.android.AndroidEntryPoint 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 @@ -55,6 +55,7 @@ import im.vector.app.features.call.dialpad.DialPadFragment import im.vector.app.features.call.utils.EglUtils import im.vector.app.features.call.webrtc.WebRtcCall import im.vector.app.features.call.webrtc.WebRtcCallManager +import im.vector.app.features.displayname.getBestName 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 @@ -84,21 +85,16 @@ data class CallArgs( private val loggerTag = LoggerTag("VectorCallActivity", LoggerTag.VOIP) +@AndroidEntryPoint class VectorCallActivity : VectorBaseActivity(), CallControlsView.InteractionListener { override fun getBinding() = ActivityCallBinding.inflate(layoutInflater) + @Inject lateinit var callManager: WebRtcCallManager @Inject lateinit var avatarRenderer: AvatarRenderer - override fun injectWith(injector: ScreenComponent) { - injector.inject(this) - } - private val callViewModel: VectorCallViewModel by viewModel() - @Inject lateinit var callManager: WebRtcCallManager - @Inject lateinit var viewModelFactory: VectorCallViewModel.Factory - private val dialPadCallback = object : DialPadFragment.Callback { override fun onDigitAppended(digit: String) { callViewModel.handle(VectorCallViewActions.SendDtmfDigit(digit)) @@ -138,7 +134,7 @@ class VectorCallActivity : VectorBaseActivity(), CallContro renderState(it) } - callViewModel.asyncSubscribe(this, VectorCallViewState::callState) { + callViewModel.onAsync(VectorCallViewState::callState) { if (it is CallState.Ended) { handleCallEnded(it) } @@ -152,7 +148,7 @@ class VectorCallActivity : VectorBaseActivity(), CallContro } .disposeOnDestroy() - callViewModel.selectSubscribe(this, VectorCallViewState::callId, VectorCallViewState::isVideoCall) { _, isVideoCall -> + callViewModel.onEach(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() @@ -167,8 +163,8 @@ class VectorCallActivity : VectorBaseActivity(), CallContro override fun onNewIntent(intent: Intent?) { super.onNewIntent(intent) - intent?.takeIf { it.hasExtra(MvRx.KEY_ARG) } - ?.let { intent.getParcelableExtra(MvRx.KEY_ARG) } + intent?.takeIf { it.hasExtra(Mavericks.KEY_ARG) } + ?.let { intent.getParcelableExtra(Mavericks.KEY_ARG) } ?.let { callViewModel.handle(VectorCallViewActions.SwitchCall(it)) } @@ -630,10 +626,11 @@ class VectorCallActivity : VectorBaseActivity(), CallContro const val INCOMING_ACCEPT = "INCOMING_ACCEPT" fun newIntent(context: Context, call: WebRtcCall, mode: String?): Intent { + val callArgs = CallArgs(call.nativeRoomId, call.callId, call.mxCall.opponentUserId, !call.mxCall.isOutgoing, call.mxCall.isVideoCall) 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(Mavericks.KEY_ARG, callArgs) putExtra(EXTRA_MODE, mode) } } @@ -645,10 +642,11 @@ class VectorCallActivity : VectorBaseActivity(), CallContro isIncomingCall: Boolean, isVideoCall: Boolean, mode: String?): Intent { + val callArgs = CallArgs(signalingRoomId, callId, otherUserId, isIncomingCall, isVideoCall) 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(Mavericks.KEY_ARG, callArgs) 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 1834c05e41..67aa7bede2 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 @@ -25,9 +25,9 @@ sealed class VectorCallViewActions : VectorViewModelAction { object DeclineCall : VectorCallViewActions() object ToggleMute : VectorCallViewActions() object ToggleVideo : VectorCallViewActions() - object ToggleHoldResume: VectorCallViewActions() + object ToggleHoldResume : VectorCallViewActions() data class ChangeAudioDevice(val device: CallAudioManager.Device) : VectorCallViewActions() - object OpenDialPad: VectorCallViewActions() + object OpenDialPad : VectorCallViewActions() data class SendDtmfDigit(val digit: String) : VectorCallViewActions() data class SwitchCall(val callArgs: CallArgs) : VectorCallViewActions() @@ -36,5 +36,5 @@ sealed class VectorCallViewActions : VectorViewModelAction { object ToggleCamera : VectorCallViewActions() object ToggleHDSD : VectorCallViewActions() object InitiateCallTransfer : VectorCallViewActions() - object TransferCall: VectorCallViewActions() + object TransferCall : 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 9f19429c00..5a0a2f127c 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 @@ -27,8 +27,8 @@ sealed class VectorCallViewEvents : VectorViewEvents { val available: Set, val current: CallAudioManager.Device ) : VectorCallViewEvents() - object ShowDialPad: VectorCallViewEvents() - object ShowCallTransferScreen: VectorCallViewEvents() + object ShowDialPad : VectorCallViewEvents() + object ShowCallTransferScreen : VectorCallViewEvents() // data class CallAnswered(val content: CallAnswerContent) : VectorCallViewEvents() // data class CallHangup(val content: CallHangupContent) : VectorCallViewEvents() // object CallAccepted : VectorCallViewEvents() 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 63ba83bdbc..5af2b826af 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 @@ -19,12 +19,13 @@ package im.vector.app.features.call import androidx.lifecycle.viewModelScope import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Loading -import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.Success -import com.airbnb.mvrx.ViewModelContext import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import im.vector.app.features.call.audio.CallAudioManager @@ -134,7 +135,15 @@ class VectorCallViewModel @AssistedInject constructor( } ?: VectorCallViewState.TransfereeState.UnknownTransferee } - private val currentCallListener = object : WebRtcCallManager.CurrentCallListener { + private val callManagerListener = object : WebRtcCallManager.Listener { + + override fun onCallEnded(callId: String) { + withState { state -> + if (state.otherKnownCallInfo?.callId == callId) { + setState { copy(otherKnownCallInfo = null) } + } + } + } override fun onCurrentCallChange(call: WebRtcCall?) { if (call != null) { @@ -159,9 +168,7 @@ class VectorCallViewModel @AssistedInject constructor( } private fun updateOtherKnownCall(currentCall: WebRtcCall) { - val otherCall = callManager.getCalls().firstOrNull { - it.callId != currentCall.callId && it.mxCall.state is CallState.Connected - } + val otherCall = getOtherKnownCall(currentCall) setState { if (otherCall == null) { copy(otherKnownCallInfo = null) @@ -171,6 +178,12 @@ class VectorCallViewModel @AssistedInject constructor( } } + private fun getOtherKnownCall(currentCall: WebRtcCall): WebRtcCall? { + return callManager.getCalls().firstOrNull { + it.callId != currentCall.callId && it.mxCall.state is CallState.Connected + } + } + init { setupCallWithCurrentState() } @@ -184,7 +197,7 @@ class VectorCallViewModel @AssistedInject constructor( } } else { call = webRtcCall - callManager.addCurrentCallListener(currentCallListener) + callManager.addListener(callManagerListener) webRtcCall.addListener(callListener) val currentSoundDevice = callManager.audioManager.selectedDevice if (currentSoundDevice == CallAudioManager.Device.Phone) { @@ -230,7 +243,7 @@ class VectorCallViewModel @AssistedInject constructor( } override fun onCleared() { - callManager.removeCurrentCallListener(currentCallListener) + callManager.removeListener(callManagerListener) call?.removeListener(callListener) call = null proximityManager.stop() @@ -310,10 +323,10 @@ class VectorCallViewModel @AssistedInject constructor( VectorCallViewEvents.ShowCallTransferScreen ) } - VectorCallViewActions.TransferCall -> { + VectorCallViewActions.TransferCall -> { handleCallTransfer() } - is VectorCallViewActions.SwitchCall -> { + is VectorCallViewActions.SwitchCall -> { setState { VectorCallViewState(action.callArgs) } setupCallWithCurrentState() } @@ -329,16 +342,9 @@ class VectorCallViewModel @AssistedInject constructor( } @AssistedFactory - interface Factory { - fun create(initialState: VectorCallViewState): VectorCallViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: VectorCallViewState): VectorCallViewModel } - companion object : MvRxViewModelFactory { - - @JvmStatic - override fun create(viewModelContext: ViewModelContext, state: VectorCallViewState): VectorCallViewModel { - val callActivity: VectorCallActivity = viewModelContext.activity() - return callActivity.viewModelFactory.create(state) - } - } + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() } 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 a351806e1a..2d33cbf9b9 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 @@ -17,7 +17,7 @@ package im.vector.app.features.call import com.airbnb.mvrx.Async -import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.Uninitialized import im.vector.app.features.call.audio.CallAudioManager import org.matrix.android.sdk.api.session.call.CallState @@ -43,7 +43,7 @@ data class VectorCallViewState( val formattedDuration: String = "", val canOpponentBeTransferred: Boolean = false, val transferee: TransfereeState = TransfereeState.NoTransferee -) : MvRxState { +) : MavericksState { sealed class TransfereeState { object NoTransferee : TransfereeState() diff --git a/vector/src/main/java/im/vector/app/features/call/conference/ConferenceEvent.kt b/vector/src/main/java/im/vector/app/features/call/conference/ConferenceEvent.kt index cfa076f31b..0a63ad6907 100644 --- a/vector/src/main/java/im/vector/app/features/call/conference/ConferenceEvent.kt +++ b/vector/src/main/java/im/vector/app/features/call/conference/ConferenceEvent.kt @@ -51,8 +51,8 @@ class ConferenceEventEmitter(private val context: Context) { } class ConferenceEventObserver(private val context: Context, - private val onBroadcastEvent: (ConferenceEvent) -> Unit) - : LifecycleObserver { + private val onBroadcastEvent: (ConferenceEvent) -> Unit) : + LifecycleObserver { // See https://jitsi.github.io/handbook/docs/dev-guide/dev-guide-android-sdk#listening-for-broadcasted-events private val broadcastReceiver = object : BroadcastReceiver() { diff --git a/vector/src/main/java/im/vector/app/features/call/conference/JitsiCallViewActions.kt b/vector/src/main/java/im/vector/app/features/call/conference/JitsiCallViewActions.kt index 830af7de01..7c9b9385f9 100644 --- a/vector/src/main/java/im/vector/app/features/call/conference/JitsiCallViewActions.kt +++ b/vector/src/main/java/im/vector/app/features/call/conference/JitsiCallViewActions.kt @@ -25,5 +25,5 @@ sealed class JitsiCallViewActions : VectorViewModelAction { /** * The ViewModel will either ask the View to finish, or to join another conf. */ - object OnConferenceLeft: JitsiCallViewActions() + object OnConferenceLeft : JitsiCallViewActions() } diff --git a/vector/src/main/java/im/vector/app/features/call/conference/JitsiCallViewEvents.kt b/vector/src/main/java/im/vector/app/features/call/conference/JitsiCallViewEvents.kt index c8d570a73f..25062ffe15 100644 --- a/vector/src/main/java/im/vector/app/features/call/conference/JitsiCallViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/call/conference/JitsiCallViewEvents.kt @@ -34,6 +34,6 @@ sealed class JitsiCallViewEvents : VectorViewEvents { ) : JitsiCallViewEvents() object LeaveConference : JitsiCallViewEvents() - object FailJoiningConference: JitsiCallViewEvents() + object FailJoiningConference : JitsiCallViewEvents() object Finish : JitsiCallViewEvents() } diff --git a/vector/src/main/java/im/vector/app/features/call/conference/JitsiCallViewModel.kt b/vector/src/main/java/im/vector/app/features/call/conference/JitsiCallViewModel.kt index 0fc85cb58c..d04bebfd1b 100644 --- a/vector/src/main/java/im/vector/app/features/call/conference/JitsiCallViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/call/conference/JitsiCallViewModel.kt @@ -16,25 +16,29 @@ package im.vector.app.features.call.conference +import androidx.lifecycle.asFlow import androidx.lifecycle.viewModelScope import com.airbnb.mvrx.Fail -import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized -import com.airbnb.mvrx.ViewModelContext import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel -import io.reactivex.disposables.Disposable +import kotlinx.coroutines.Job import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.widgets.model.Widget import org.matrix.android.sdk.api.session.widgets.model.WidgetType -import org.matrix.android.sdk.rx.asObservable class JitsiCallViewModel @AssistedInject constructor( @Assisted initialState: JitsiCallViewState, @@ -43,11 +47,11 @@ class JitsiCallViewModel @AssistedInject constructor( ) : VectorViewModel(initialState) { @AssistedFactory - interface Factory { - fun create(initialState: JitsiCallViewState): JitsiCallViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: JitsiCallViewState): JitsiCallViewModel } - private var currentWidgetObserver: Disposable? = null + private var currentWidgetObserver: Job? = null private val widgetService = session.widgetService() private var confIsJoined = false @@ -59,11 +63,11 @@ class JitsiCallViewModel @AssistedInject constructor( private fun observeWidget(roomId: String, widgetId: String) { confIsJoined = false - currentWidgetObserver?.dispose() + currentWidgetObserver?.cancel() currentWidgetObserver = widgetService.getRoomWidgetsLive(roomId, QueryStringValue.Equals(widgetId), WidgetType.Jitsi.values()) - .asObservable() + .asFlow() .distinctUntilChanged() - .subscribe { + .onEach { val jitsiWidget = it.firstOrNull() if (jitsiWidget != null) { setState { @@ -81,7 +85,7 @@ class JitsiCallViewModel @AssistedInject constructor( } } } - .disposeOnClear() + .launchIn(viewModelScope) } private fun joinConference(jitsiWidget: Widget) = withState { state -> @@ -104,8 +108,8 @@ class JitsiCallViewModel @AssistedInject constructor( private fun handleSwitchTo(action: JitsiCallViewActions.SwitchTo) = withState { state -> // Check if it is the same conf - if (action.args.roomId != state.roomId - || action.args.widgetId != state.widgetId) { + if (action.args.roomId != state.roomId || + action.args.widgetId != state.widgetId) { if (action.withConfirmation) { // Ask confirmation to switch, but wait a bit for the Activity to quit the PiP mode viewModelScope.launch { @@ -140,24 +144,7 @@ class JitsiCallViewModel @AssistedInject constructor( } } - companion object : MvRxViewModelFactory { - + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() { const val ENABLE_VIDEO_OPTION = "ENABLE_VIDEO_OPTION" - - @JvmStatic - override fun create(viewModelContext: ViewModelContext, state: JitsiCallViewState): JitsiCallViewModel? { - val callActivity: VectorJitsiActivity = viewModelContext.activity() - return callActivity.viewModelFactory.create(state) - } - - override fun initialState(viewModelContext: ViewModelContext): JitsiCallViewState? { - val args: VectorJitsiActivity.Args = viewModelContext.args() - - return JitsiCallViewState( - roomId = args.roomId, - widgetId = args.widgetId, - enableVideo = args.enableVideo - ) - } } } diff --git a/vector/src/main/java/im/vector/app/features/call/conference/JitsiCallViewState.kt b/vector/src/main/java/im/vector/app/features/call/conference/JitsiCallViewState.kt index 1fd04542e0..7ecd44b9b0 100644 --- a/vector/src/main/java/im/vector/app/features/call/conference/JitsiCallViewState.kt +++ b/vector/src/main/java/im/vector/app/features/call/conference/JitsiCallViewState.kt @@ -17,7 +17,7 @@ package im.vector.app.features.call.conference import com.airbnb.mvrx.Async -import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.Uninitialized import org.matrix.android.sdk.api.session.widgets.model.Widget @@ -26,4 +26,11 @@ data class JitsiCallViewState( val widgetId: String = "", val enableVideo: Boolean = false, val widget: Async = Uninitialized -) : MvRxState +) : MavericksState { + + constructor(args: VectorJitsiActivity.Args) : this( + roomId = args.roomId, + widgetId = args.widgetId, + enableVideo = args.enableVideo + ) +} 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 b8b6d83dd1..b691296ba3 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 @@ -22,6 +22,7 @@ import im.vector.app.core.resources.StringProvider import im.vector.app.core.utils.ensureProtocol import im.vector.app.core.utils.toBase32String import im.vector.app.features.call.conference.jwt.JitsiJWTFactory +import im.vector.app.features.displayname.getBestName import im.vector.app.features.raw.wellknown.getElementWellknown import im.vector.app.features.settings.VectorLocale import im.vector.app.features.themes.ThemeProvider 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 index 391471d2f2..fd7fc31e6d 100644 --- 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 @@ -87,10 +87,10 @@ import org.matrix.android.sdk.api.session.room.model.Membership fun render(roomDetailViewState: RoomDetailViewState) { val summary = roomDetailViewState.asyncRoomSummary() - val newState = if (summary?.membership != Membership.JOIN - || roomDetailViewState.isWebRTCCallOptionAvailable() - || !roomDetailViewState.isAllowedToManageWidgets - || roomDetailViewState.jitsiState.widgetId == null) { + val newState = if (summary?.membership != Membership.JOIN || + roomDetailViewState.isWebRTCCallOptionAvailable() || + !roomDetailViewState.isAllowedToManageWidgets || + roomDetailViewState.jitsiState.widgetId == null) { State.Unmount } else if (roomDetailViewState.jitsiState.deleteWidgetInProgress) { State.Progress 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 e7fd541f3d..3fcefc9c8e 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 @@ -28,13 +28,13 @@ import android.widget.Toast import androidx.core.view.isVisible import androidx.lifecycle.Lifecycle import com.airbnb.mvrx.Fail -import com.airbnb.mvrx.MvRx +import com.airbnb.mvrx.Mavericks import com.airbnb.mvrx.Success import com.airbnb.mvrx.viewModel import com.facebook.react.modules.core.PermissionListener import com.google.android.material.dialog.MaterialAlertDialogBuilder +import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R -import im.vector.app.core.di.ScreenComponent import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.databinding.ActivityJitsiBinding @@ -48,8 +48,8 @@ import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.util.JsonDict import timber.log.Timber import java.net.URL -import javax.inject.Inject +@AndroidEntryPoint class VectorJitsiActivity : VectorBaseActivity(), JitsiMeetActivityInterface { @Parcelize @@ -61,16 +61,10 @@ class VectorJitsiActivity : VectorBaseActivity(), JitsiMee override fun getBinding() = ActivityJitsiBinding.inflate(layoutInflater) - @Inject lateinit var viewModelFactory: JitsiCallViewModel.Factory - private var jitsiMeetView: JitsiMeetView? = null private val jitsiViewModel: JitsiCallViewModel by viewModel() - override fun injectWith(injector: ScreenComponent) { - injector.inject(this) - } - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -205,8 +199,8 @@ class VectorJitsiActivity : VectorBaseActivity(), JitsiMee JitsiMeetActivityDelegate.onNewIntent(intent) // Is it a switch to another conf? - intent?.takeIf { it.hasExtra(MvRx.KEY_ARG) } - ?.let { intent.getParcelableExtra(MvRx.KEY_ARG) } + intent?.takeIf { it.hasExtra(Mavericks.KEY_ARG) } + ?.let { intent.getParcelableExtra(Mavericks.KEY_ARG) } ?.let { jitsiViewModel.handle(JitsiCallViewActions.SwitchTo(it, true)) } @@ -242,7 +236,7 @@ class VectorJitsiActivity : VectorBaseActivity(), JitsiMee companion object { fun newIntent(context: Context, roomId: String, widgetId: String, enableVideo: Boolean): Intent { return Intent(context, VectorJitsiActivity::class.java).apply { - putExtra(MvRx.KEY_ARG, Args(roomId, widgetId, enableVideo)) + putExtra(Mavericks.KEY_ARG, Args(roomId, widgetId, enableVideo)) } } } diff --git a/vector/src/main/java/im/vector/app/features/call/dialpad/CallDialPadBottomSheet.kt b/vector/src/main/java/im/vector/app/features/call/dialpad/CallDialPadBottomSheet.kt index 3472d01c72..8bf2ce47bd 100644 --- a/vector/src/main/java/im/vector/app/features/call/dialpad/CallDialPadBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/call/dialpad/CallDialPadBottomSheet.kt @@ -87,7 +87,7 @@ class CallDialPadBottomSheet : VectorBaseBottomSheetDialogFragment { val callId = intent.getStringExtra(EXTRA_CALL_ID) ?: return diff --git a/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferActivity.kt b/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferActivity.kt index c80b21334a..c03b526f8c 100644 --- a/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferActivity.kt +++ b/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferActivity.kt @@ -20,18 +20,14 @@ import android.content.Context import android.content.Intent import android.os.Bundle import android.os.Parcelable -import com.airbnb.mvrx.MvRx +import com.airbnb.mvrx.Mavericks import com.airbnb.mvrx.viewModel import com.google.android.material.tabs.TabLayoutMediator +import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R -import im.vector.app.core.di.ScreenComponent import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.databinding.ActivityCallTransferBinding -import im.vector.app.features.contactsbook.ContactsBookViewModel -import im.vector.app.features.contactsbook.ContactsBookViewState -import im.vector.app.features.userdirectory.UserListViewModel -import im.vector.app.features.userdirectory.UserListViewState import kotlinx.parcelize.Parcelize import javax.inject.Inject @@ -40,14 +36,9 @@ data class CallTransferArgs(val callId: String) : Parcelable private const val USER_LIST_FRAGMENT_TAG = "USER_LIST_FRAGMENT_TAG" -class CallTransferActivity : VectorBaseActivity(), - CallTransferViewModel.Factory, - UserListViewModel.Factory, - ContactsBookViewModel.Factory { +@AndroidEntryPoint +class CallTransferActivity : VectorBaseActivity() { - @Inject lateinit var userListViewModelFactory: UserListViewModel.Factory - @Inject lateinit var callTransferViewModelFactory: CallTransferViewModel.Factory - @Inject lateinit var contactsBookViewModelFactory: ContactsBookViewModel.Factory @Inject lateinit var errorFormatter: ErrorFormatter private lateinit var sectionsPagerAdapter: CallTransferPagerAdapter @@ -58,22 +49,6 @@ class CallTransferActivity : VectorBaseActivity(), override fun getCoordinatorLayout() = views.vectorCoordinatorLayout - override fun injectWith(injector: ScreenComponent) { - injector.inject(this) - } - - override fun create(initialState: UserListViewState): UserListViewModel { - return userListViewModelFactory.create(initialState) - } - - override fun create(initialState: CallTransferViewState): CallTransferViewModel { - return callTransferViewModelFactory.create(initialState) - } - - override fun create(initialState: ContactsBookViewState): ContactsBookViewModel { - return contactsBookViewModelFactory.create(initialState) - } - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) waitingView = views.waitingView.waitingView @@ -121,7 +96,7 @@ class CallTransferActivity : VectorBaseActivity(), fun newIntent(context: Context, callId: String): Intent { return Intent(context, CallTransferActivity::class.java).also { - it.putExtra(MvRx.KEY_ARG, CallTransferArgs(callId)) + it.putExtra(Mavericks.KEY_ARG, CallTransferArgs(callId)) } } } diff --git a/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferViewEvents.kt b/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferViewEvents.kt index b110164d1e..fd4c9d672d 100644 --- a/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferViewEvents.kt @@ -20,6 +20,6 @@ import im.vector.app.core.platform.VectorViewEvents sealed class CallTransferViewEvents : VectorViewEvents { object Dismiss : CallTransferViewEvents() - object Loading: CallTransferViewEvents() + object Loading : CallTransferViewEvents() object FailToTransfer : CallTransferViewEvents() } diff --git a/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferViewModel.kt b/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferViewModel.kt index 0217551260..ffc6ff9bc3 100644 --- a/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferViewModel.kt @@ -16,13 +16,12 @@ package im.vector.app.features.call.transfer -import androidx.lifecycle.viewModelScope -import com.airbnb.mvrx.ActivityViewModelContext -import com.airbnb.mvrx.MvRxViewModelFactory -import com.airbnb.mvrx.ViewModelContext +import com.airbnb.mvrx.MavericksViewModelFactory import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import im.vector.app.features.call.dialpad.DialPadLookup @@ -37,22 +36,15 @@ import org.matrix.android.sdk.api.session.call.MxCall class CallTransferViewModel @AssistedInject constructor(@Assisted initialState: CallTransferViewState, private val dialPadLookup: DialPadLookup, private val directRoomHelper: DirectRoomHelper, - private val callManager: WebRtcCallManager) - : VectorViewModel(initialState) { + private val callManager: WebRtcCallManager) : + VectorViewModel(initialState) { @AssistedFactory - interface Factory { - fun create(initialState: CallTransferViewState): CallTransferViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: CallTransferViewState): CallTransferViewModel } - companion object : MvRxViewModelFactory { - - @JvmStatic - override fun create(viewModelContext: ViewModelContext, state: CallTransferViewState): CallTransferViewModel? { - val activity: CallTransferActivity = (viewModelContext as ActivityViewModelContext).activity() - return activity.callTransferViewModelFactory.create(state) - } - } + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() private val call = callManager.getCallById(initialState.callId) private val callListener = object : WebRtcCall.Listener { diff --git a/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferViewState.kt b/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferViewState.kt index 2b29d9f6f2..babda1dd19 100644 --- a/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferViewState.kt +++ b/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferViewState.kt @@ -16,11 +16,11 @@ package im.vector.app.features.call.transfer -import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MavericksState data class CallTransferViewState( val callId: String -) : MvRxState { +) : MavericksState { constructor(args: CallTransferArgs) : this(callId = args.callId) } 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 2d39fda2e3..e632d00790 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 @@ -636,8 +636,8 @@ class WebRtcCall( // We consider a call to be on hold only if *all* the tracks are on hold // (is this the right thing to do?) for (transceiver in peerConnection?.transceivers ?: emptyList()) { - val trackOnHold = transceiver.currentDirection == RtpTransceiver.RtpTransceiverDirection.INACTIVE - || transceiver.currentDirection == RtpTransceiver.RtpTransceiverDirection.RECV_ONLY + val trackOnHold = transceiver.currentDirection == RtpTransceiver.RtpTransceiverDirection.INACTIVE || + transceiver.currentDirection == RtpTransceiver.RtpTransceiverDirection.RECV_ONLY if (!trackOnHold) callOnHold = false } return callOnHold @@ -810,17 +810,19 @@ class WebRtcCall( } } - fun endCall(reason: EndCallReason = EndCallReason.USER_HANGUP) { + fun endCall(reason: EndCallReason = EndCallReason.USER_HANGUP, sendSignaling: Boolean = true) { sessionScope?.launch(dispatcher) { if (mxCall.state is CallState.Ended) { return@launch } val reject = mxCall.state is CallState.LocalRinging terminate(reason, reject) - if (reject) { - mxCall.reject() - } else { - mxCall.hangUp(reason) + if (sendSignaling) { + if (reject) { + mxCall.reject() + } else { + mxCall.hangUp(reason) + } } } } @@ -889,8 +891,8 @@ class WebRtcCall( val polite = !mxCall.isOutgoing // Here we follow the perfect negotiation logic from // https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API/Perfect_negotiation - val offerCollision = description.type == SdpType.OFFER - && (makingOffer || peerConnection.signalingState() != PeerConnection.SignalingState.STABLE) + val offerCollision = description.type == SdpType.OFFER && + (makingOffer || peerConnection.signalingState() != PeerConnection.SignalingState.STABLE) ignoreOffer = !polite && offerCollision if (ignoreOffer) { diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt index 73a6c07d6a..9e620174f3 100644 --- a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt @@ -84,9 +84,10 @@ class WebRtcCallManager @Inject constructor( private val sessionScope: CoroutineScope? get() = currentSession?.coroutineScope - interface CurrentCallListener { - fun onCurrentCallChange(call: WebRtcCall?) {} - fun onAudioDevicesChange() {} + interface Listener { + fun onCallEnded(callId: String) = Unit + fun onCurrentCallChange(call: WebRtcCall?) = Unit + fun onAudioDevicesChange() = Unit } val supportedPSTNProtocol: String? @@ -106,13 +107,13 @@ class WebRtcCallManager @Inject constructor( protocolsChecker?.removeListener(listener) } - private val currentCallsListeners = CopyOnWriteArrayList() + private val currentCallsListeners = CopyOnWriteArrayList() - fun addCurrentCallListener(listener: CurrentCallListener) { + fun addListener(listener: Listener) { currentCallsListeners.add(listener) } - fun removeCurrentCallListener(listener: CurrentCallListener) { + fun removeListener(listener: Listener) { currentCallsListeners.remove(listener) } @@ -250,10 +251,13 @@ class WebRtcCallManager @Inject constructor( callsByRoomId[webRtcCall.signalingRoomId]?.remove(webRtcCall) callsByRoomId[webRtcCall.nativeRoomId]?.remove(webRtcCall) transferees.remove(callId) - if (getCurrentCall()?.callId == callId) { + if (currentCall.get()?.callId == callId) { val otherCall = getCalls().lastOrNull() currentCall.setAndNotify(otherCall) } + tryOrNull { + currentCallsListeners.forEach { it.onCallEnded(callId) } + } // There is no active calls if (getCurrentCall() == null) { Timber.tag(loggerTag.value).v("Dispose peerConnectionFactory as there is no need to keep one") @@ -424,7 +428,11 @@ class WebRtcCallManager @Inject constructor( override fun onCallManagedByOtherSession(callId: String) { Timber.tag(loggerTag.value).v("onCallManagedByOtherSession: $callId") - onCallEnded(callId, EndCallReason.ANSWERED_ELSEWHERE, false) + val call = callsByCallId[callId] + ?: return Unit.also { + Timber.tag(loggerTag.value).w("onCallManagedByOtherSession for non active call? $callId") + } + call.endCall(EndCallReason.ANSWERED_ELSEWHERE, sendSignaling = false) } override fun onCallAssertedIdentityReceived(callAssertedIdentityContent: CallAssertedIdentityContent) { diff --git a/vector/src/main/java/im/vector/app/features/command/Command.kt b/vector/src/main/java/im/vector/app/features/command/Command.kt index 206c5af17a..ccabd25ff4 100644 --- a/vector/src/main/java/im/vector/app/features/command/Command.kt +++ b/vector/src/main/java/im/vector/app/features/command/Command.kt @@ -28,14 +28,20 @@ enum class Command(val command: String, val parameters: String, @StringRes val d EMOTE("/me", "", R.string.command_description_emote, false), BAN_USER("/ban", " [reason]", R.string.command_description_ban_user, false), UNBAN_USER("/unban", " [reason]", R.string.command_description_unban_user, false), + IGNORE_USER("/ignore", " [reason]", R.string.command_description_ignore_user, false), + UNIGNORE_USER("/unignore", "", R.string.command_description_unignore_user, false), SET_USER_POWER_LEVEL("/op", " []", R.string.command_description_op_user, false), RESET_USER_POWER_LEVEL("/deop", "", R.string.command_description_deop_user, false), + ROOM_NAME("/roomname", "", R.string.command_description_room_name, false), INVITE("/invite", " [reason]", R.string.command_description_invite_user, false), - JOIN_ROOM("/join", " [reason]", R.string.command_description_join_room, false), - PART("/part", " [reason]", R.string.command_description_part_room, false), + JOIN_ROOM("/join", " [reason]", R.string.command_description_join_room, false), + PART("/part", "[]", R.string.command_description_part_room, false), TOPIC("/topic", "", R.string.command_description_topic, false), KICK_USER("/kick", " [reason]", R.string.command_description_kick_user, false), CHANGE_DISPLAY_NAME("/nick", "", R.string.command_description_nick, false), + CHANGE_DISPLAY_NAME_FOR_ROOM("/myroomnick", "", R.string.command_description_nick_for_room, false), + ROOM_AVATAR("/roomavatar", "", R.string.command_description_room_avatar, true /* Since user has to know the mxc url */), + CHANGE_AVATAR_FOR_ROOM("/myroomavatar", "", R.string.command_description_avatar_for_room, true /* Since user has to know the mxc url */), MARKDOWN("/markdown", "", R.string.command_description_markdown, false), RAINBOW("/rainbow", "", R.string.command_description_rainbow, false), RAINBOW_EMOTE("/rainbowme", "", R.string.command_description_rainbow_emote, false), @@ -43,11 +49,13 @@ enum class Command(val command: String, val parameters: String, @StringRes val d SPOILER("/spoiler", "", R.string.command_description_spoiler, false), POLL("/poll", "Question | Option 1 | Option 2 ...", R.string.command_description_poll, false), SHRUG("/shrug", "", R.string.command_description_shrug, false), + LENNY("/lenny", "", R.string.command_description_lenny, false), PLAIN("/plain", "", R.string.command_description_plain, false), + WHOIS("/whois", "", R.string.command_description_whois, false), DISCARD_SESSION("/discardsession", "", R.string.command_description_discard_session, false), CONFETTI("/confetti", "", R.string.command_confetti, false), SNOWFALL("/snowfall", "", R.string.command_snow, false), - CREATE_SPACE("/createspace", " *", R.string.command_description_create_space, true), + CREATE_SPACE("/createspace", " *", R.string.command_description_create_space, true), ADD_TO_SPACE("/addToSpace", "spaceId", R.string.command_description_add_to_space, true), JOIN_SPACE("/joinSpace", "spaceId", R.string.command_description_join_space, true), LEAVE_ROOM("/leave", "", R.string.command_description_leave_room, true), diff --git a/vector/src/main/java/im/vector/app/features/command/CommandParser.kt b/vector/src/main/java/im/vector/app/features/command/CommandParser.kt index adba6e4a18..3a6b005d6f 100644 --- a/vector/src/main/java/im/vector/app/features/command/CommandParser.kt +++ b/vector/src/main/java/im/vector/app/features/command/CommandParser.kt @@ -20,6 +20,7 @@ import im.vector.app.core.extensions.isEmail import im.vector.app.core.extensions.isMsisdn import im.vector.app.features.home.room.detail.ChatEffect import org.matrix.android.sdk.api.MatrixPatterns +import org.matrix.android.sdk.api.MatrixUrls.isMxcUrl import org.matrix.android.sdk.api.session.identity.ThreePid import timber.log.Timber @@ -61,7 +62,7 @@ object CommandParser { } return when (val slashCommand = messageParts.first()) { - Command.PLAIN.command -> { + Command.PLAIN.command -> { val text = textMessage.substring(Command.PLAIN.command.length).trim() if (text.isNotEmpty()) { @@ -70,7 +71,7 @@ object CommandParser { ParsedCommand.ErrorSyntax(Command.PLAIN) } } - Command.CHANGE_DISPLAY_NAME.command -> { + Command.CHANGE_DISPLAY_NAME.command -> { val newDisplayName = textMessage.substring(Command.CHANGE_DISPLAY_NAME.command.length).trim() if (newDisplayName.isNotEmpty()) { @@ -79,7 +80,42 @@ object CommandParser { ParsedCommand.ErrorSyntax(Command.CHANGE_DISPLAY_NAME) } } - Command.TOPIC.command -> { + Command.CHANGE_DISPLAY_NAME_FOR_ROOM.command -> { + val newDisplayName = textMessage.substring(Command.CHANGE_DISPLAY_NAME_FOR_ROOM.command.length).trim() + + if (newDisplayName.isNotEmpty()) { + ParsedCommand.ChangeDisplayNameForRoom(newDisplayName) + } else { + ParsedCommand.ErrorSyntax(Command.CHANGE_DISPLAY_NAME_FOR_ROOM) + } + } + Command.ROOM_AVATAR.command -> { + if (messageParts.size == 2) { + val url = messageParts[1] + + if (url.isMxcUrl()) { + ParsedCommand.ChangeRoomAvatar(url) + } else { + ParsedCommand.ErrorSyntax(Command.ROOM_AVATAR) + } + } else { + ParsedCommand.ErrorSyntax(Command.ROOM_AVATAR) + } + } + Command.CHANGE_AVATAR_FOR_ROOM.command -> { + if (messageParts.size == 2) { + val url = messageParts[1] + + if (url.isMxcUrl()) { + ParsedCommand.ChangeAvatarForRoom(url) + } else { + ParsedCommand.ErrorSyntax(Command.CHANGE_AVATAR_FOR_ROOM) + } + } else { + ParsedCommand.ErrorSyntax(Command.CHANGE_AVATAR_FOR_ROOM) + } + } + Command.TOPIC.command -> { val newTopic = textMessage.substring(Command.TOPIC.command.length).trim() if (newTopic.isNotEmpty()) { @@ -88,22 +124,22 @@ object CommandParser { ParsedCommand.ErrorSyntax(Command.TOPIC) } } - Command.EMOTE.command -> { + Command.EMOTE.command -> { val message = textMessage.subSequence(Command.EMOTE.command.length, textMessage.length).trim() ParsedCommand.SendEmote(message) } - Command.RAINBOW.command -> { + Command.RAINBOW.command -> { val message = textMessage.subSequence(Command.RAINBOW.command.length, textMessage.length).trim() ParsedCommand.SendRainbow(message) } - Command.RAINBOW_EMOTE.command -> { + Command.RAINBOW_EMOTE.command -> { val message = textMessage.subSequence(Command.RAINBOW_EMOTE.command.length, textMessage.length).trim() ParsedCommand.SendRainbowEmote(message) } - Command.JOIN_ROOM.command -> { + Command.JOIN_ROOM.command -> { if (messageParts.size >= 2) { val roomAlias = messageParts[1] @@ -121,25 +157,23 @@ object CommandParser { ParsedCommand.ErrorSyntax(Command.JOIN_ROOM) } } - Command.PART.command -> { - if (messageParts.size >= 2) { - val roomAlias = messageParts[1] - - if (roomAlias.isNotEmpty()) { - ParsedCommand.PartRoom( - roomAlias, - textMessage.substring(Command.PART.length + roomAlias.length) - .trim() - .takeIf { it.isNotBlank() } - ) - } else { - ParsedCommand.ErrorSyntax(Command.PART) - } - } else { - ParsedCommand.ErrorSyntax(Command.PART) + Command.PART.command -> { + when (messageParts.size) { + 1 -> ParsedCommand.PartRoom(null) + 2 -> ParsedCommand.PartRoom(messageParts[1]) + else -> ParsedCommand.ErrorSyntax(Command.PART) } } - Command.INVITE.command -> { + Command.ROOM_NAME.command -> { + val newRoomName = textMessage.substring(Command.ROOM_NAME.command.length).trim() + + if (newRoomName.isNotEmpty()) { + ParsedCommand.ChangeRoomName(newRoomName) + } else { + ParsedCommand.ErrorSyntax(Command.ROOM_NAME) + } + } + Command.INVITE.command -> { if (messageParts.size >= 2) { val userId = messageParts[1] @@ -166,7 +200,7 @@ object CommandParser { ParsedCommand.ErrorSyntax(Command.INVITE) } } - Command.KICK_USER.command -> { + Command.KICK_USER.command -> { if (messageParts.size >= 2) { val userId = messageParts[1] @@ -184,7 +218,7 @@ object CommandParser { ParsedCommand.ErrorSyntax(Command.KICK_USER) } } - Command.BAN_USER.command -> { + Command.BAN_USER.command -> { if (messageParts.size >= 2) { val userId = messageParts[1] @@ -202,7 +236,7 @@ object CommandParser { ParsedCommand.ErrorSyntax(Command.BAN_USER) } } - Command.UNBAN_USER.command -> { + Command.UNBAN_USER.command -> { if (messageParts.size >= 2) { val userId = messageParts[1] @@ -220,7 +254,33 @@ object CommandParser { ParsedCommand.ErrorSyntax(Command.UNBAN_USER) } } - Command.SET_USER_POWER_LEVEL.command -> { + Command.IGNORE_USER.command -> { + if (messageParts.size == 2) { + val userId = messageParts[1] + + if (MatrixPatterns.isUserId(userId)) { + ParsedCommand.IgnoreUser(userId) + } else { + ParsedCommand.ErrorSyntax(Command.IGNORE_USER) + } + } else { + ParsedCommand.ErrorSyntax(Command.IGNORE_USER) + } + } + Command.UNIGNORE_USER.command -> { + if (messageParts.size == 2) { + val userId = messageParts[1] + + if (MatrixPatterns.isUserId(userId)) { + ParsedCommand.UnignoreUser(userId) + } else { + ParsedCommand.ErrorSyntax(Command.UNIGNORE_USER) + } + } else { + ParsedCommand.ErrorSyntax(Command.UNIGNORE_USER) + } + } + Command.SET_USER_POWER_LEVEL.command -> { if (messageParts.size == 3) { val userId = messageParts[1] if (MatrixPatterns.isUserId(userId)) { @@ -240,7 +300,7 @@ object CommandParser { ParsedCommand.ErrorSyntax(Command.SET_USER_POWER_LEVEL) } } - Command.RESET_USER_POWER_LEVEL.command -> { + Command.RESET_USER_POWER_LEVEL.command -> { if (messageParts.size == 2) { val userId = messageParts[1] @@ -253,7 +313,7 @@ object CommandParser { ParsedCommand.ErrorSyntax(Command.SET_USER_POWER_LEVEL) } } - Command.MARKDOWN.command -> { + Command.MARKDOWN.command -> { if (messageParts.size == 2) { when { "on".equals(messageParts[1], true) -> ParsedCommand.SetMarkdown(true) @@ -264,23 +324,28 @@ object CommandParser { ParsedCommand.ErrorSyntax(Command.MARKDOWN) } } - Command.CLEAR_SCALAR_TOKEN.command -> { + Command.CLEAR_SCALAR_TOKEN.command -> { if (messageParts.size == 1) { ParsedCommand.ClearScalarToken } else { ParsedCommand.ErrorSyntax(Command.CLEAR_SCALAR_TOKEN) } } - Command.SPOILER.command -> { + Command.SPOILER.command -> { val message = textMessage.substring(Command.SPOILER.command.length).trim() ParsedCommand.SendSpoiler(message) } - Command.SHRUG.command -> { + Command.SHRUG.command -> { val message = textMessage.substring(Command.SHRUG.command.length).trim() ParsedCommand.SendShrug(message) } - Command.POLL.command -> { + Command.LENNY.command -> { + val message = textMessage.substring(Command.LENNY.command.length).trim() + + ParsedCommand.SendLenny(message) + } + Command.POLL.command -> { val rawCommand = textMessage.substring(Command.POLL.command.length).trim() val split = rawCommand.split("|").map { it.trim() } if (split.size > 2) { @@ -289,18 +354,31 @@ object CommandParser { ParsedCommand.ErrorSyntax(Command.POLL) } } - Command.DISCARD_SESSION.command -> { + Command.DISCARD_SESSION.command -> { ParsedCommand.DiscardSession } - Command.CONFETTI.command -> { + Command.WHOIS.command -> { + if (messageParts.size == 2) { + val userId = messageParts[1] + + if (MatrixPatterns.isUserId(userId)) { + ParsedCommand.ShowUser(userId) + } else { + ParsedCommand.ErrorSyntax(Command.WHOIS) + } + } else { + ParsedCommand.ErrorSyntax(Command.WHOIS) + } + } + Command.CONFETTI.command -> { val message = textMessage.substring(Command.CONFETTI.command.length).trim() ParsedCommand.SendChatEffect(ChatEffect.CONFETTI, message) } - Command.SNOWFALL.command -> { + Command.SNOWFALL.command -> { val message = textMessage.substring(Command.SNOWFALL.command.length).trim() ParsedCommand.SendChatEffect(ChatEffect.SNOWFALL, message) } - Command.CREATE_SPACE.command -> { + Command.CREATE_SPACE.command -> { val rawCommand = textMessage.substring(Command.CREATE_SPACE.command.length).trim() val split = rawCommand.split(" ").map { it.trim() } if (split.isEmpty()) { @@ -312,25 +390,25 @@ object CommandParser { ) } } - Command.ADD_TO_SPACE.command -> { + Command.ADD_TO_SPACE.command -> { val rawCommand = textMessage.substring(Command.ADD_TO_SPACE.command.length).trim() ParsedCommand.AddToSpace( rawCommand ) } - Command.JOIN_SPACE.command -> { + Command.JOIN_SPACE.command -> { val spaceIdOrAlias = textMessage.substring(Command.JOIN_SPACE.command.length).trim() ParsedCommand.JoinSpace( spaceIdOrAlias ) } - Command.LEAVE_ROOM.command -> { + Command.LEAVE_ROOM.command -> { val spaceIdOrAlias = textMessage.substring(Command.LEAVE_ROOM.command.length).trim() ParsedCommand.LeaveRoom( spaceIdOrAlias ) } - Command.UPGRADE_ROOM.command -> { + Command.UPGRADE_ROOM.command -> { val newVersion = textMessage.substring(Command.UPGRADE_ROOM.command.length).trim() if (newVersion.isEmpty()) { ParsedCommand.ErrorSyntax(Command.UPGRADE_ROOM) @@ -338,7 +416,7 @@ object CommandParser { ParsedCommand.UpgradeRoom(newVersion) } } - else -> { + else -> { // Unknown command ParsedCommand.ErrorUnknownSlashCommand(slashCommand) } diff --git a/vector/src/main/java/im/vector/app/features/command/ParsedCommand.kt b/vector/src/main/java/im/vector/app/features/command/ParsedCommand.kt index 123f1d3a36..89aa8d9188 100644 --- a/vector/src/main/java/im/vector/app/features/command/ParsedCommand.kt +++ b/vector/src/main/java/im/vector/app/features/command/ParsedCommand.kt @@ -42,20 +42,28 @@ sealed class ParsedCommand { class SendRainbowEmote(val message: CharSequence) : ParsedCommand() class BanUser(val userId: String, val reason: String?) : ParsedCommand() class UnbanUser(val userId: String, val reason: String?) : ParsedCommand() + class IgnoreUser(val userId: String) : ParsedCommand() + class UnignoreUser(val userId: String) : ParsedCommand() class SetUserPowerLevel(val userId: String, val powerLevel: Int?) : ParsedCommand() + class ChangeRoomName(val name: String) : ParsedCommand() class Invite(val userId: String, val reason: String?) : ParsedCommand() class Invite3Pid(val threePid: ThreePid) : ParsedCommand() class JoinRoom(val roomAlias: String, val reason: String?) : ParsedCommand() - class PartRoom(val roomAlias: String, val reason: String?) : ParsedCommand() + class PartRoom(val roomAlias: String?) : ParsedCommand() class ChangeTopic(val topic: String) : ParsedCommand() class KickUser(val userId: String, val reason: String?) : ParsedCommand() class ChangeDisplayName(val displayName: String) : ParsedCommand() + class ChangeDisplayNameForRoom(val displayName: String) : ParsedCommand() + class ChangeRoomAvatar(val url: String) : ParsedCommand() + class ChangeAvatarForRoom(val url: String) : ParsedCommand() class SetMarkdown(val enable: Boolean) : ParsedCommand() object ClearScalarToken : ParsedCommand() class SendSpoiler(val message: String) : ParsedCommand() class SendShrug(val message: CharSequence) : ParsedCommand() + class SendLenny(val message: CharSequence) : ParsedCommand() class SendPoll(val question: String, val options: List) : ParsedCommand() object DiscardSession : ParsedCommand() + class ShowUser(val userId: String) : ParsedCommand() class SendChatEffect(val chatEffect: ChatEffect, val message: String) : ParsedCommand() class CreateSpace(val name: String, val invitees: List) : ParsedCommand() class AddToSpace(val spaceId: String) : ParsedCommand() diff --git a/vector/src/main/java/im/vector/app/features/configuration/VectorConfiguration.kt b/vector/src/main/java/im/vector/app/features/configuration/VectorConfiguration.kt index a2d190bd69..2c19e80772 100644 --- a/vector/src/main/java/im/vector/app/features/configuration/VectorConfiguration.kt +++ b/vector/src/main/java/im/vector/app/features/configuration/VectorConfiguration.kt @@ -104,8 +104,8 @@ class VectorConfiguration @Inject constructor(private val context: Context) { * @return the local status value */ fun getHash(): String { - return (VectorLocale.applicationLocale.toString() - + "_" + FontScale.getFontScaleValue(context).preferenceValue - + "_" + ThemeUtils.getApplicationTheme(context)) + return (VectorLocale.applicationLocale.toString() + + "_" + FontScale.getFontScaleValue(context).preferenceValue + + "_" + ThemeUtils.getApplicationTheme(context)) } } diff --git a/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookFragment.kt b/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookFragment.kt index 9291352821..d79ad308de 100644 --- a/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookFragment.kt +++ b/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookFragment.kt @@ -23,30 +23,28 @@ import android.view.ViewGroup import androidx.core.view.isVisible import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.withState -import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.jakewharton.rxbinding3.widget.checkedChanges import com.jakewharton.rxbinding3.widget.textChanges -import im.vector.app.R import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.configureWith import im.vector.app.core.extensions.hideKeyboard import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.core.utils.showIdentityServerConsentDialog import im.vector.app.databinding.FragmentContactsBookBinding +import im.vector.app.features.navigation.SettingsActivityPayload import im.vector.app.features.userdirectory.PendingSelection import im.vector.app.features.userdirectory.UserListAction import im.vector.app.features.userdirectory.UserListSharedAction import im.vector.app.features.userdirectory.UserListSharedActionViewModel import im.vector.app.features.userdirectory.UserListViewModel - import org.matrix.android.sdk.api.session.identity.ThreePid import org.matrix.android.sdk.api.session.user.model.User import java.util.concurrent.TimeUnit import javax.inject.Inject class ContactsBookFragment @Inject constructor( - private val contactsBookViewModelFactory: ContactsBookViewModel.Factory, private val contactsBookController: ContactsBookController -) : VectorBaseFragment(), ContactsBookController.Callback, ContactsBookViewModel.Factory { +) : VectorBaseFragment(), ContactsBookController.Callback { override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentContactsBookBinding { return FragmentContactsBookBinding.inflate(inflater, container, false) @@ -59,10 +57,6 @@ class ContactsBookFragment @Inject constructor( private lateinit var sharedActionViewModel: UserListSharedActionViewModel - override fun create(initialState: ContactsBookViewState): ContactsBookViewModel { - return contactsBookViewModelFactory.create(initialState) - } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) sharedActionViewModel = activityViewModelProvider.get(UserListSharedActionViewModel::class.java) @@ -76,14 +70,13 @@ class ContactsBookFragment @Inject constructor( private fun setupConsentView() { views.phoneBookSearchForMatrixContacts.setOnClickListener { withState(contactsBookViewModel) { state -> - MaterialAlertDialogBuilder(requireActivity()) - .setTitle(R.string.identity_server_consent_dialog_title) - .setMessage(getString(R.string.identity_server_consent_dialog_content, state.identityServerUrl ?: "")) - .setPositiveButton(R.string.yes) { _, _ -> - contactsBookViewModel.handle(ContactsBookAction.UserConsentGranted) - } - .setNegativeButton(R.string.no, null) - .show() + requireContext().showIdentityServerConsentDialog( + state.identityServerUrl, + policyLinkCallback = { + navigator.openSettings(requireContext(), SettingsActivityPayload.DiscoverySettings(expandIdentityPolicies = true)) + }, + consentCallBack = { contactsBookViewModel.handle(ContactsBookAction.UserConsentGranted) } + ) } } } diff --git a/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookViewModel.kt b/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookViewModel.kt index cfbdef8ffb..7f0dbcb7b2 100644 --- a/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookViewModel.kt @@ -17,17 +17,16 @@ package im.vector.app.features.contactsbook import androidx.lifecycle.viewModelScope -import com.airbnb.mvrx.ActivityViewModelContext -import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.Loading -import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.Success -import com.airbnb.mvrx.ViewModelContext import dagger.assisted.Assisted -import dagger.assisted.AssistedInject import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import im.vector.app.core.contacts.ContactsDataSource import im.vector.app.core.contacts.MappedContact +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel @@ -41,24 +40,15 @@ import timber.log.Timber class ContactsBookViewModel @AssistedInject constructor(@Assisted initialState: ContactsBookViewState, private val contactsDataSource: ContactsDataSource, - private val session: Session) - : VectorViewModel(initialState) { + private val session: Session) : + VectorViewModel(initialState) { @AssistedFactory - interface Factory { - fun create(initialState: ContactsBookViewState): ContactsBookViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: ContactsBookViewState): ContactsBookViewModel } - companion object : MvRxViewModelFactory { - - override fun create(viewModelContext: ViewModelContext, state: ContactsBookViewState): ContactsBookViewModel? { - 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") - } - } + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() private var allContacts: List = emptyList() private var mappedContacts: List = emptyList() @@ -66,7 +56,7 @@ class ContactsBookViewModel @AssistedInject constructor(@Assisted init { loadContacts() - selectSubscribe(ContactsBookViewState::searchTerm, ContactsBookViewState::onlyBoundContacts) { _, _ -> + onEach(ContactsBookViewState::searchTerm, ContactsBookViewState::onlyBoundContacts) { _, _ -> updateFilteredMappedContacts() } } @@ -156,8 +146,8 @@ class ContactsBookViewModel @AssistedInject constructor(@Assisted val filteredMappedContacts = mappedContacts .filter { it.displayName.contains(state.searchTerm, true) } .filter { contactModel -> - !state.onlyBoundContacts - || contactModel.emails.any { it.matrixId != null } || contactModel.msisdns.any { it.matrixId != null } + !state.onlyBoundContacts || + contactModel.emails.any { it.matrixId != null } || contactModel.msisdns.any { it.matrixId != null } } setState { diff --git a/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookViewState.kt b/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookViewState.kt index d2ee684c4d..d4f279787d 100644 --- a/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookViewState.kt +++ b/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookViewState.kt @@ -18,7 +18,7 @@ package im.vector.app.features.contactsbook import com.airbnb.mvrx.Async import com.airbnb.mvrx.Loading -import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MavericksState import im.vector.app.core.contacts.MappedContact data class ContactsBookViewState( @@ -36,4 +36,4 @@ data class ContactsBookViewState( val identityServerUrl: String? = null, // User consent to perform lookup (send emails to the identity server) val userConsent: Boolean = false -) : MvRxState +) : MavericksState diff --git a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomAction.kt b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomAction.kt index ffc25210e9..da3425d326 100644 --- a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomAction.kt +++ b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomAction.kt @@ -21,7 +21,6 @@ import im.vector.app.features.userdirectory.PendingSelection sealed class CreateDirectRoomAction : VectorViewModelAction { data class CreateRoomAndInviteSelectedUsers( - val selections: Set, - val existingDmRoomId: String? + val selections: Set ) : CreateDirectRoomAction() } diff --git a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomActivity.kt b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomActivity.kt index 68123d5e82..28da72714a 100644 --- a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomActivity.kt +++ b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomActivity.kt @@ -28,8 +28,8 @@ import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Success import com.airbnb.mvrx.viewModel import com.google.android.material.dialog.MaterialAlertDialogBuilder +import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R -import im.vector.app.core.di.ScreenComponent import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.extensions.addFragment import im.vector.app.core.extensions.addFragmentToBackstack @@ -42,39 +42,22 @@ import im.vector.app.core.utils.checkPermissions import im.vector.app.core.utils.onPermissionDeniedSnackbar import im.vector.app.core.utils.registerForPermissionsResult import im.vector.app.features.contactsbook.ContactsBookFragment -import im.vector.app.features.contactsbook.ContactsBookViewModel -import im.vector.app.features.contactsbook.ContactsBookViewState import im.vector.app.features.userdirectory.UserListFragment import im.vector.app.features.userdirectory.UserListFragmentArgs import im.vector.app.features.userdirectory.UserListSharedAction import im.vector.app.features.userdirectory.UserListSharedActionViewModel -import im.vector.app.features.userdirectory.UserListViewModel -import im.vector.app.features.userdirectory.UserListViewState import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.session.room.failure.CreateRoomFailure import java.net.HttpURLConnection import javax.inject.Inject -class CreateDirectRoomActivity : SimpleFragmentActivity(), UserListViewModel.Factory, CreateDirectRoomViewModel.Factory, ContactsBookViewModel.Factory { +@AndroidEntryPoint +class CreateDirectRoomActivity : SimpleFragmentActivity() { private val viewModel: CreateDirectRoomViewModel by viewModel() private lateinit var sharedActionViewModel: UserListSharedActionViewModel - @Inject lateinit var userListViewModelFactory: UserListViewModel.Factory - @Inject lateinit var createDirectRoomViewModelFactory: CreateDirectRoomViewModel.Factory - @Inject lateinit var contactsBookViewModelFactory: ContactsBookViewModel.Factory @Inject lateinit var errorFormatter: ErrorFormatter - override fun injectWith(injector: ScreenComponent) { - super.injectWith(injector) - injector.inject(this) - } - - override fun create(initialState: UserListViewState) = userListViewModelFactory.create(initialState) - - override fun create(initialState: CreateDirectRoomViewState) = createDirectRoomViewModelFactory.create(initialState) - - override fun create(initialState: ContactsBookViewState) = contactsBookViewModelFactory.create(initialState) - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) views.toolbar.visibility = View.GONE @@ -102,7 +85,7 @@ class CreateDirectRoomActivity : SimpleFragmentActivity(), UserListViewModel.Fac ) ) } - viewModel.selectSubscribe(this, CreateDirectRoomViewState::createAndInviteState) { + viewModel.onEach(CreateDirectRoomViewState::createAndInviteState) { renderCreateAndInviteState(it) } } @@ -138,10 +121,7 @@ class CreateDirectRoomActivity : SimpleFragmentActivity(), UserListViewModel.Fac private fun onMenuItemSelected(action: UserListSharedAction.OnMenuItemSelected) { if (action.itemId == R.id.action_create_direct_room) { - viewModel.handle(CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers( - action.selections, - null - )) + viewModel.handle(CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers(action.selections)) } } diff --git a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomByQrCodeFragment.kt b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomByQrCodeFragment.kt index 8da0147a43..5f089c6448 100644 --- a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomByQrCodeFragment.kt +++ b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomByQrCodeFragment.kt @@ -99,7 +99,6 @@ class CreateDirectRoomByQrCodeFragment @Inject constructor() : VectorBaseFragmen Toast.makeText(requireContext(), R.string.invalid_qr_code_uri, Toast.LENGTH_SHORT).show() requireActivity().finish() } else { - val existingDm = viewModel.session.getExistingDirectRoomWithUser(mxid) // The following assumes MXIDs are case insensitive if (mxid.equals(other = viewModel.session.myUserId, ignoreCase = true)) { Toast.makeText(requireContext(), R.string.cannot_dm_self, Toast.LENGTH_SHORT).show() @@ -109,7 +108,7 @@ class CreateDirectRoomByQrCodeFragment @Inject constructor() : VectorBaseFragmen val qrInvitee = if (viewModel.session.getUser(mxid) != null) viewModel.session.getUser(mxid)!! else User(mxid, null, null) viewModel.handle( - CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers(setOf(PendingSelection.UserPendingSelection(qrInvitee)), existingDm) + CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers(setOf(PendingSelection.UserPendingSelection(qrInvitee))) ) } } diff --git a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomViewModel.kt b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomViewModel.kt index 88e8d68155..41360eab93 100644 --- a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomViewModel.kt @@ -16,16 +16,14 @@ package im.vector.app.features.createdirect -import androidx.lifecycle.viewModelScope -import com.airbnb.mvrx.ActivityViewModelContext -import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.Loading -import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.Success -import com.airbnb.mvrx.ViewModelContext import dagger.assisted.Assisted -import dagger.assisted.AssistedInject import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.extensions.exhaustive import im.vector.app.core.mvrx.runCatchingToAsync import im.vector.app.core.platform.VectorViewModel @@ -41,25 +39,15 @@ import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted initialState: CreateDirectRoomViewState, private val rawService: RawService, - val session: Session) - : VectorViewModel(initialState) { + val session: Session) : + VectorViewModel(initialState) { @AssistedFactory - interface Factory { - fun create(initialState: CreateDirectRoomViewState): CreateDirectRoomViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: CreateDirectRoomViewState): CreateDirectRoomViewModel } - companion object : MvRxViewModelFactory { - - @JvmStatic - override fun create(viewModelContext: ViewModelContext, state: CreateDirectRoomViewState): CreateDirectRoomViewModel? { - 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") - } - } + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() override fun handle(action: CreateDirectRoomAction) { when (action) { @@ -71,10 +59,13 @@ class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted * If users already have a DM room then navigate to it instead of creating a new room. */ private fun onSubmitInvitees(action: CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers) { - if (action.existingDmRoomId != null) { + val existingRoomId = action.selections.singleOrNull()?.getMxId()?.let { userId -> + session.getExistingDirectRoomWithUser(userId) + } + if (existingRoomId != null) { // Do not create a new DM, just tell that the creation is successful by passing the existing roomId setState { - copy(createAndInviteState = Success(action.existingDmRoomId)) + copy(createAndInviteState = Success(existingRoomId)) } } else { // Create the DM diff --git a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomViewState.kt b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomViewState.kt index 27a8cc0a97..41366a7110 100644 --- a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomViewState.kt +++ b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomViewState.kt @@ -17,9 +17,9 @@ package im.vector.app.features.createdirect import com.airbnb.mvrx.Async -import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.Uninitialized data class CreateDirectRoomViewState( val createAndInviteState: Async = Uninitialized -) : MvRxState +) : MavericksState diff --git a/vector/src/main/java/im/vector/app/features/crypto/keys/KeysExporter.kt b/vector/src/main/java/im/vector/app/features/crypto/keys/KeysExporter.kt index 2c66a14cb4..3db67df8e1 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/keys/KeysExporter.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/keys/KeysExporter.kt @@ -18,24 +18,43 @@ package im.vector.app.features.crypto.keys import android.content.Context import android.net.Uri -import kotlinx.coroutines.Dispatchers +import im.vector.app.core.dispatchers.CoroutineDispatchers import kotlinx.coroutines.withContext import org.matrix.android.sdk.api.session.Session import javax.inject.Inject class KeysExporter @Inject constructor( private val session: Session, - private val context: Context + private val context: Context, + private val dispatchers: CoroutineDispatchers ) { /** * Export keys and write them to the provided uri */ suspend fun export(password: String, uri: Uri) { - return withContext(Dispatchers.IO) { + withContext(dispatchers.io) { val data = session.cryptoService().exportRoomKeys(password) context.contentResolver.openOutputStream(uri) ?.use { it.write(data) } - ?: throw IllegalStateException("Unable to open file for writting") + ?: throw IllegalStateException("Unable to open file for writing") + verifyExportedKeysOutputFileSize(uri, expectedSize = data.size.toLong()) + } + } + + private fun verifyExportedKeysOutputFileSize(uri: Uri, expectedSize: Long) { + val output = context.contentResolver.openFileDescriptor(uri, "r", null) + when { + output == null -> throw IllegalStateException("Exported file not found") + output.statSize != expectedSize -> { + throw UnexpectedExportKeysFileSizeException( + expectedFileSize = expectedSize, + actualFileSize = output.statSize + ) + } } } } + +class UnexpectedExportKeysFileSizeException(expectedFileSize: Long, actualFileSize: Long) : IllegalStateException( + "Exported Keys file has unexpected file size, got: $actualFileSize but expected: $expectedFileSize" +) diff --git a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/restore/KeysBackupRestoreActivity.kt b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/restore/KeysBackupRestoreActivity.kt index e80853b035..bdae975846 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/restore/KeysBackupRestoreActivity.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/restore/KeysBackupRestoreActivity.kt @@ -19,7 +19,9 @@ import android.app.Activity import android.content.Context import android.content.Intent import com.google.android.material.dialog.MaterialAlertDialogBuilder +import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R +import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.extensions.addFragmentToBackstack import im.vector.app.core.extensions.observeEvent import im.vector.app.core.extensions.registerStartForActivityResult @@ -28,7 +30,9 @@ import im.vector.app.core.platform.SimpleFragmentActivity import im.vector.app.core.ui.views.KeysBackupBanner import im.vector.app.features.crypto.quads.SharedSecureStorageActivity import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME +import javax.inject.Inject +@AndroidEntryPoint class KeysBackupRestoreActivity : SimpleFragmentActivity() { companion object { @@ -48,10 +52,12 @@ class KeysBackupRestoreActivity : SimpleFragmentActivity() { super.onBackPressed() } + @Inject lateinit var activeSessionHolder: ActiveSessionHolder + override fun initUiAndData() { super.initUiAndData() viewModel = viewModelProvider.get(KeysBackupRestoreSharedViewModel::class.java) - viewModel.initSession(session) + viewModel.initSession(activeSessionHolder.getActiveSession()) viewModel.keySourceModel.observe(this) { keySource -> if (keySource != null && !keySource.isInQuadS && supportFragmentManager.fragments.isEmpty()) { diff --git a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/restore/KeysBackupRestoreFromKeyFragment.kt b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/restore/KeysBackupRestoreFromKeyFragment.kt index 1f8cf4d3a0..435ca6e608 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/restore/KeysBackupRestoreFromKeyFragment.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/restore/KeysBackupRestoreFromKeyFragment.kt @@ -27,12 +27,11 @@ import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.utils.startImportTextFromFileIntent import im.vector.app.databinding.FragmentKeysBackupRestoreFromKeyBinding - import org.matrix.android.sdk.api.extensions.tryOrNull import javax.inject.Inject -class KeysBackupRestoreFromKeyFragment @Inject constructor() - : VectorBaseFragment() { +class KeysBackupRestoreFromKeyFragment @Inject constructor() : + VectorBaseFragment() { override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentKeysBackupRestoreFromKeyBinding { return FragmentKeysBackupRestoreFromKeyBinding.inflate(inflater, container, false) diff --git a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/restore/KeysBackupRestoreFromPassphraseFragment.kt b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/restore/KeysBackupRestoreFromPassphraseFragment.kt index 96d8a257b7..c023e66e44 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/restore/KeysBackupRestoreFromPassphraseFragment.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/restore/KeysBackupRestoreFromPassphraseFragment.kt @@ -27,7 +27,6 @@ import androidx.core.widget.doOnTextChanged import im.vector.app.R import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.FragmentKeysBackupRestoreFromPassphraseBinding - import javax.inject.Inject class KeysBackupRestoreFromPassphraseFragment @Inject constructor() : VectorBaseFragment() { diff --git a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/restore/KeysBackupRestoreSharedViewModel.kt b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/restore/KeysBackupRestoreSharedViewModel.kt index c323024bd9..34a333d588 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/restore/KeysBackupRestoreSharedViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/restore/KeysBackupRestoreSharedViewModel.kt @@ -88,26 +88,26 @@ class KeysBackupRestoreSharedViewModel @Inject constructor( override fun onStepProgress(step: StepProgressListener.Step) { when (step) { is StepProgressListener.Step.ComputingKey -> { - loadingEvent.postValue(WaitingViewData(stringProvider.getString(R.string.keys_backup_restoring_waiting_message) - + "\n" + stringProvider.getString(R.string.keys_backup_restoring_computing_key_waiting_message), + loadingEvent.postValue(WaitingViewData(stringProvider.getString(R.string.keys_backup_restoring_waiting_message) + + "\n" + stringProvider.getString(R.string.keys_backup_restoring_computing_key_waiting_message), step.progress, step.total)) } is StepProgressListener.Step.DownloadingKey -> { - loadingEvent.postValue(WaitingViewData(stringProvider.getString(R.string.keys_backup_restoring_waiting_message) - + "\n" + stringProvider.getString(R.string.keys_backup_restoring_downloading_backup_waiting_message), + loadingEvent.postValue(WaitingViewData(stringProvider.getString(R.string.keys_backup_restoring_waiting_message) + + "\n" + stringProvider.getString(R.string.keys_backup_restoring_downloading_backup_waiting_message), isIndeterminate = true)) } is StepProgressListener.Step.ImportingKey -> { Timber.d("backupKeys.ImportingKey.progress: ${step.progress}") // Progress 0 can take a while, display an indeterminate progress in this case if (step.progress == 0) { - loadingEvent.postValue(WaitingViewData(stringProvider.getString(R.string.keys_backup_restoring_waiting_message) - + "\n" + stringProvider.getString(R.string.keys_backup_restoring_importing_keys_waiting_message), + loadingEvent.postValue(WaitingViewData(stringProvider.getString(R.string.keys_backup_restoring_waiting_message) + + "\n" + stringProvider.getString(R.string.keys_backup_restoring_importing_keys_waiting_message), isIndeterminate = true)) } else { - loadingEvent.postValue(WaitingViewData(stringProvider.getString(R.string.keys_backup_restoring_waiting_message) - + "\n" + stringProvider.getString(R.string.keys_backup_restoring_importing_keys_waiting_message), + loadingEvent.postValue(WaitingViewData(stringProvider.getString(R.string.keys_backup_restoring_waiting_message) + + "\n" + stringProvider.getString(R.string.keys_backup_restoring_importing_keys_waiting_message), step.progress, step.total)) } diff --git a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/restore/KeysBackupRestoreSuccessFragment.kt b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/restore/KeysBackupRestoreSuccessFragment.kt index 8c1124bbbc..66a7df14c3 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/restore/KeysBackupRestoreSuccessFragment.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/restore/KeysBackupRestoreSuccessFragment.kt @@ -24,7 +24,6 @@ import im.vector.app.R import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.utils.LiveEvent import im.vector.app.databinding.FragmentKeysBackupRestoreSuccessBinding - import javax.inject.Inject class KeysBackupRestoreSuccessFragment @Inject constructor() : VectorBaseFragment() { diff --git a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupManageActivity.kt b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupManageActivity.kt index d78fa37d62..2b666e9cf8 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupManageActivity.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupManageActivity.kt @@ -21,13 +21,13 @@ import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Loading import com.airbnb.mvrx.viewModel import com.google.android.material.dialog.MaterialAlertDialogBuilder +import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R -import im.vector.app.core.di.ScreenComponent import im.vector.app.core.extensions.replaceFragment import im.vector.app.core.platform.SimpleFragmentActivity import im.vector.app.core.platform.WaitingViewData -import javax.inject.Inject +@AndroidEntryPoint class KeysBackupManageActivity : SimpleFragmentActivity() { companion object { @@ -40,12 +40,6 @@ class KeysBackupManageActivity : SimpleFragmentActivity() { override fun getTitleRes() = R.string.encryption_message_recovery private val viewModel: KeysBackupSettingsViewModel by viewModel() - @Inject lateinit var keysBackupSettingsViewModelFactory: KeysBackupSettingsViewModel.Factory - - override fun injectWith(injector: ScreenComponent) { - super.injectWith(injector) - injector.inject(this) - } override fun initUiAndData() { super.initUiAndData() @@ -55,7 +49,7 @@ class KeysBackupManageActivity : SimpleFragmentActivity() { } // Observe the deletion of keys backup - viewModel.selectSubscribe(this, KeysBackupSettingViewState::deleteBackupRequest) { asyncDelete -> + viewModel.onEach(KeysBackupSettingViewState::deleteBackupRequest) { asyncDelete -> when (asyncDelete) { is Fail -> { updateWaitingView(null) diff --git a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupSettingViewState.kt b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupSettingViewState.kt index 874e3765ca..438b502b42 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupSettingViewState.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupSettingViewState.kt @@ -17,7 +17,7 @@ package im.vector.app.features.crypto.keysbackup.settings import com.airbnb.mvrx.Async -import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.Uninitialized import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupState import org.matrix.android.sdk.internal.crypto.keysbackup.model.KeysBackupVersionTrust @@ -26,5 +26,5 @@ import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersionR data class KeysBackupSettingViewState(val keysBackupVersionTrust: Async = Uninitialized, val keysBackupState: KeysBackupState? = null, val keysBackupVersion: KeysVersionResult? = null, - val deleteBackupRequest: Async = Uninitialized) - : MvRxState + val deleteBackupRequest: Async = Uninitialized) : + MavericksState diff --git a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupSettingsFragment.kt b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupSettingsFragment.kt index d2ee1bd637..cf6bd99f23 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupSettingsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupSettingsFragment.kt @@ -29,11 +29,10 @@ import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.FragmentKeysBackupSettingsBinding import im.vector.app.features.crypto.keysbackup.restore.KeysBackupRestoreActivity import im.vector.app.features.crypto.keysbackup.setup.KeysBackupSetupActivity - import javax.inject.Inject -class KeysBackupSettingsFragment @Inject constructor(private val keysBackupSettingsRecyclerViewController: KeysBackupSettingsRecyclerViewController) - : VectorBaseFragment(), +class KeysBackupSettingsFragment @Inject constructor(private val keysBackupSettingsRecyclerViewController: KeysBackupSettingsRecyclerViewController) : + VectorBaseFragment(), KeysBackupSettingsRecyclerViewController.Listener { override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentKeysBackupSettingsBinding { diff --git a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupSettingsViewModel.kt b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupSettingsViewModel.kt index cb8a6ce4e9..d6d9e10669 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupSettingsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupSettingsViewModel.kt @@ -15,16 +15,16 @@ */ package im.vector.app.features.crypto.keysbackup.settings -import com.airbnb.mvrx.ActivityViewModelContext import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Loading -import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized -import com.airbnb.mvrx.ViewModelContext import dagger.assisted.Assisted -import dagger.assisted.AssistedInject import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel import org.matrix.android.sdk.api.MatrixCallback @@ -41,18 +41,11 @@ class KeysBackupSettingsViewModel @AssistedInject constructor(@Assisted initialS KeysBackupStateListener { @AssistedFactory - interface Factory { - fun create(initialState: KeysBackupSettingViewState): KeysBackupSettingsViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: KeysBackupSettingViewState): KeysBackupSettingsViewModel } - companion object : MvRxViewModelFactory { - - @JvmStatic - override fun create(viewModelContext: ViewModelContext, state: KeysBackupSettingViewState): KeysBackupSettingsViewModel? { - val activity: KeysBackupManageActivity = (viewModelContext as ActivityViewModelContext).activity() - return activity.keysBackupSettingsViewModelFactory.create(state) - } - } + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() private val keysBackupService: KeysBackupService = session.cryptoService().keysBackupService() @@ -163,7 +156,7 @@ class KeysBackupSettingsViewModel @AssistedInject constructor(@Assisted initialS fun canExit(): Boolean { val currentBackupState = keysBackupService.state - return currentBackupState == KeysBackupState.Unknown - || currentBackupState == KeysBackupState.CheckingBackUpOnHomeserver + return currentBackupState == KeysBackupState.Unknown || + currentBackupState == KeysBackupState.CheckingBackUpOnHomeserver } } diff --git a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/setup/KeysBackupSetupActivity.kt b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/setup/KeysBackupSetupActivity.kt index 7cc46ef62c..0f7c09ca16 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/setup/KeysBackupSetupActivity.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/setup/KeysBackupSetupActivity.kt @@ -23,8 +23,9 @@ import androidx.core.view.isVisible import androidx.fragment.app.FragmentManager import androidx.lifecycle.lifecycleScope import com.google.android.material.dialog.MaterialAlertDialogBuilder +import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R -import im.vector.app.core.di.ScreenComponent +import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.dialogs.ExportKeysDialog import im.vector.app.core.extensions.observeEvent import im.vector.app.core.extensions.queryExportKeys @@ -36,6 +37,7 @@ import im.vector.app.features.crypto.keys.KeysExporter import kotlinx.coroutines.launch import javax.inject.Inject +@AndroidEntryPoint class KeysBackupSetupActivity : SimpleFragmentActivity() { override fun getTitleRes() = R.string.title_activity_keys_backup_setup @@ -43,10 +45,10 @@ class KeysBackupSetupActivity : SimpleFragmentActivity() { private lateinit var viewModel: KeysBackupSetupSharedViewModel @Inject lateinit var keysExporter: KeysExporter + @Inject lateinit var activeSessionHolder: ActiveSessionHolder - override fun injectWith(injector: ScreenComponent) { - super.injectWith(injector) - injector.inject(this) + private val session by lazy { + activeSessionHolder.getActiveSession() } override fun initUiAndData() { diff --git a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/setup/KeysBackupSetupStep1Fragment.kt b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/setup/KeysBackupSetupStep1Fragment.kt index cf65a9942c..08496c490f 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/setup/KeysBackupSetupStep1Fragment.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/setup/KeysBackupSetupStep1Fragment.kt @@ -23,7 +23,6 @@ import android.view.ViewGroup import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.utils.LiveEvent import im.vector.app.databinding.FragmentKeysBackupSetupStep1Binding - import javax.inject.Inject class KeysBackupSetupStep1Fragment @Inject constructor() : VectorBaseFragment() { diff --git a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/setup/KeysBackupSetupStep3Fragment.kt b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/setup/KeysBackupSetupStep3Fragment.kt index 5739da11bb..2befc4e79d 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/setup/KeysBackupSetupStep3Fragment.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/setup/KeysBackupSetupStep3Fragment.kt @@ -36,7 +36,6 @@ import im.vector.app.core.utils.copyToClipboard import im.vector.app.core.utils.selectTxtFileToWrite import im.vector.app.core.utils.startSharePlainTextIntent import im.vector.app.databinding.FragmentKeysBackupSetupStep3Binding - import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext diff --git a/vector/src/main/java/im/vector/app/features/crypto/keysrequest/KeyRequestHandler.kt b/vector/src/main/java/im/vector/app/features/crypto/keysrequest/KeyRequestHandler.kt index a961c31292..85250a94ce 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/keysrequest/KeyRequestHandler.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/keysrequest/KeyRequestHandler.kt @@ -226,9 +226,9 @@ class KeyRequestHandler @Inject constructor( val alertMgrUniqueKey = alertManagerId(userId, deviceId) alertsToRequests[alertMgrUniqueKey]?.removeAll { - it.deviceId == request.deviceId - && it.userId == request.userId - && it.requestId == request.requestId + it.deviceId == request.deviceId && + it.userId == request.userId && + it.requestId == request.requestId } if (alertsToRequests[alertMgrUniqueKey]?.isEmpty() == true) { popupAlertManager.cancelAlert(alertMgrUniqueKey) diff --git a/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageActivity.kt b/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageActivity.kt index 51159e62bf..61c8ab8f0a 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageActivity.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageActivity.kt @@ -25,11 +25,11 @@ import android.view.View import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentOnAttachListener -import com.airbnb.mvrx.MvRx +import com.airbnb.mvrx.Mavericks import com.airbnb.mvrx.viewModel import com.google.android.material.dialog.MaterialAlertDialogBuilder +import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R -import im.vector.app.core.di.ScreenComponent import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.extensions.commitTransaction import im.vector.app.core.platform.SimpleFragmentActivity @@ -39,6 +39,7 @@ import kotlinx.parcelize.Parcelize import javax.inject.Inject import kotlin.reflect.KClass +@AndroidEntryPoint class SharedSecureStorageActivity : SimpleFragmentActivity(), VectorBaseBottomSheetDialogFragment.ResultListener, @@ -52,14 +53,8 @@ class SharedSecureStorageActivity : ) : Parcelable private val viewModel: SharedSecureStorageViewModel by viewModel() - @Inject lateinit var viewModelFactory: SharedSecureStorageViewModel.Factory @Inject lateinit var errorFormatter: ErrorFormatter - override fun injectWith(injector: ScreenComponent) { - super.injectWith(injector) - injector.inject(this) - } - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) supportFragmentManager.addFragmentOnAttachListener(this) @@ -159,7 +154,7 @@ class SharedSecureStorageActivity : resultKeyStoreAlias: String = DEFAULT_RESULT_KEYSTORE_ALIAS): Intent { require(requestedSecrets.isNotEmpty()) return Intent(context, SharedSecureStorageActivity::class.java).also { - it.putExtra(MvRx.KEY_ARG, Args( + it.putExtra(Mavericks.KEY_ARG, Args( keyId, requestedSecrets, resultKeyStoreAlias diff --git a/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageViewModel.kt b/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageViewModel.kt index 9a5fc4ca06..8994ad901b 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageViewModel.kt @@ -16,25 +16,25 @@ package im.vector.app.features.crypto.quads -import androidx.lifecycle.viewModelScope import com.airbnb.mvrx.Async import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Loading -import com.airbnb.mvrx.MvRx -import com.airbnb.mvrx.MvRxState -import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.MavericksState +import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized -import com.airbnb.mvrx.ViewModelContext import dagger.assisted.Assisted -import dagger.assisted.AssistedInject import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import im.vector.app.R +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.WaitingViewData import im.vector.app.core.resources.StringProvider import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.matrix.android.sdk.api.listeners.ProgressListener @@ -42,8 +42,8 @@ import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.securestorage.IntegrityResult import org.matrix.android.sdk.api.session.securestorage.KeyInfoResult import org.matrix.android.sdk.api.session.securestorage.RawBytesKeySpec +import org.matrix.android.sdk.flow.flow import org.matrix.android.sdk.internal.crypto.crosssigning.toBase64NoPadding -import org.matrix.android.sdk.rx.rx import timber.log.Timber import java.io.ByteArrayOutputStream @@ -54,8 +54,18 @@ data class SharedSecureStorageViewState( val step: Step = Step.EnterPassphrase, val activeDeviceCount: Int = 0, val showResetAllAction: Boolean = false, - val userId: String = "" -) : MvRxState { + val userId: String = "", + val keyId: String?, + val requestedSecrets: List, + val resultKeyStoreAlias: String +) : MavericksState { + + constructor(args: SharedSecureStorageActivity.Args) : this( + keyId = args.keyId, + requestedSecrets = args.requestedSecrets, + resultKeyStoreAlias = args.resultKeyStoreAlias + ) + enum class Step { EnterPassphrase, EnterKey, @@ -64,24 +74,22 @@ data class SharedSecureStorageViewState( } class SharedSecureStorageViewModel @AssistedInject constructor( - @Assisted initialState: SharedSecureStorageViewState, - @Assisted val args: SharedSecureStorageActivity.Args, + @Assisted private val initialState: SharedSecureStorageViewState, private val stringProvider: StringProvider, - private val session: Session) - : VectorViewModel(initialState) { + private val session: Session) : + VectorViewModel(initialState) { @AssistedFactory - interface Factory { - fun create(initialState: SharedSecureStorageViewState, args: SharedSecureStorageActivity.Args): SharedSecureStorageViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: SharedSecureStorageViewState): SharedSecureStorageViewModel } init { - setState { copy(userId = session.myUserId) } - val isValid = session.sharedSecretStorageService.checkShouldBeAbleToAccessSecrets(args.requestedSecrets, args.keyId) is IntegrityResult.Success - if (!isValid) { + val integrityResult = session.sharedSecretStorageService.checkShouldBeAbleToAccessSecrets(initialState.requestedSecrets, initialState.keyId) + if (integrityResult !is IntegrityResult.Success) { _viewEvents.post( SharedSecureStorageViewEvent.Error( stringProvider.getString(R.string.enter_secret_storage_invalid), @@ -89,7 +97,7 @@ class SharedSecureStorageViewModel @AssistedInject constructor( ) ) } - val keyResult = args.keyId?.let { session.sharedSecretStorageService.getKey(it) } + val keyResult = initialState.keyId?.let { session.sharedSecretStorageService.getKey(it) } ?: session.sharedSecretStorageService.getDefaultKey() if (!keyResult.isSuccess()) { @@ -115,7 +123,7 @@ class SharedSecureStorageViewModel @AssistedInject constructor( } } - session.rx() + session.flow() .liveUserCryptoDevices(session.myUserId) .distinctUntilChanged() .execute { @@ -167,10 +175,14 @@ class SharedSecureStorageViewModel @AssistedInject constructor( if (state.checkingSSSSAction is Loading) return@withState // ignore when (state.step) { SharedSecureStorageViewState.Step.EnterKey -> { - setState { - copy( - step = SharedSecureStorageViewState.Step.EnterPassphrase - ) + if (state.hasPassphrase) { + setState { + copy( + step = SharedSecureStorageViewState.Step.EnterPassphrase + ) + } + } else { + _viewEvents.post(SharedSecureStorageViewEvent.Dismiss) } } SharedSecureStorageViewState.Step.ResetAll -> { @@ -215,7 +227,7 @@ class SharedSecureStorageViewModel @AssistedInject constructor( } withContext(Dispatchers.IO) { - args.requestedSecrets.forEach { + initialState.requestedSecrets.forEach { if (session.accountDataService().getUserAccountDataEvent(it) != null) { val res = session.sharedSecretStorageService.getSecret( name = it, @@ -232,7 +244,7 @@ class SharedSecureStorageViewModel @AssistedInject constructor( _viewEvents.post(SharedSecureStorageViewEvent.HideModalLoading) val safeForIntentCypher = ByteArrayOutputStream().also { it.use { - session.securelyStoreObject(decryptedSecretMap as Map, args.resultKeyStoreAlias, it) + session.securelyStoreObject(decryptedSecretMap as Map, initialState.resultKeyStoreAlias, it) } }.toByteArray().toBase64NoPadding() _viewEvents.post(SharedSecureStorageViewEvent.FinishSuccess(safeForIntentCypher)) @@ -284,7 +296,7 @@ class SharedSecureStorageViewModel @AssistedInject constructor( ) withContext(Dispatchers.IO) { - args.requestedSecrets.forEach { + initialState.requestedSecrets.forEach { if (session.accountDataService().getUserAccountDataEvent(it) != null) { val res = session.sharedSecretStorageService.getSecret( name = it, @@ -301,7 +313,7 @@ class SharedSecureStorageViewModel @AssistedInject constructor( _viewEvents.post(SharedSecureStorageViewEvent.HideModalLoading) val safeForIntentCypher = ByteArrayOutputStream().also { it.use { - session.securelyStoreObject(decryptedSecretMap as Map, args.resultKeyStoreAlias, it) + session.securelyStoreObject(decryptedSecretMap as Map, initialState.resultKeyStoreAlias, it) } }.toByteArray().toBase64NoPadding() _viewEvents.post(SharedSecureStorageViewEvent.FinishSuccess(safeForIntentCypher)) @@ -317,13 +329,5 @@ class SharedSecureStorageViewModel @AssistedInject constructor( _viewEvents.post(SharedSecureStorageViewEvent.Dismiss) } - companion object : MvRxViewModelFactory { - - @JvmStatic - override fun create(viewModelContext: ViewModelContext, state: SharedSecureStorageViewState): SharedSecureStorageViewModel? { - val activity: SharedSecureStorageActivity = viewModelContext.activity() - val args: SharedSecureStorageActivity.Args = activity.intent.getParcelableExtra(MvRx.KEY_ARG) ?: error("Missing args") - return activity.viewModelFactory.create(state, args) - } - } + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() } diff --git a/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecuredStorageKeyFragment.kt b/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecuredStorageKeyFragment.kt index feff326cfd..1ba0198cb4 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecuredStorageKeyFragment.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecuredStorageKeyFragment.kt @@ -31,7 +31,6 @@ import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.utils.startImportTextFromFileIntent import im.vector.app.databinding.FragmentSsssAccessFromKeyBinding import io.reactivex.android.schedulers.AndroidSchedulers - import org.matrix.android.sdk.api.extensions.tryOrNull import java.util.concurrent.TimeUnit import javax.inject.Inject diff --git a/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecuredStorageResetAllFragment.kt b/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecuredStorageResetAllFragment.kt index b640577797..670e5c610a 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecuredStorageResetAllFragment.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecuredStorageResetAllFragment.kt @@ -27,11 +27,10 @@ import im.vector.app.core.extensions.setTextOrHide import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.FragmentSsssResetAllBinding import im.vector.app.features.roommemberprofile.devices.DeviceListBottomSheet - import javax.inject.Inject -class SharedSecuredStorageResetAllFragment @Inject constructor() - : VectorBaseFragment() { +class SharedSecuredStorageResetAllFragment @Inject constructor() : + VectorBaseFragment() { override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentSsssResetAllBinding { return FragmentSsssResetAllBinding.inflate(inflater, container, false) diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapActions.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapActions.kt index 869f0ed8ec..363416b7de 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapActions.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapActions.kt @@ -36,6 +36,7 @@ sealed class BootstrapActions : VectorViewModelAction { object DoInitializeGeneratedKey : BootstrapActions() data class UpdateCandidatePassphrase(val pass: String) : BootstrapActions() data class UpdateConfirmCandidatePassphrase(val pass: String) : BootstrapActions() + // data class ReAuth(val pass: String) : BootstrapActions() object RecoveryKeySaved : BootstrapActions() object Completed : BootstrapActions() @@ -47,7 +48,7 @@ sealed class BootstrapActions : VectorViewModelAction { data class DoMigrateWithPassphrase(val passphrase: String) : BootstrapActions() data class DoMigrateWithRecoveryKey(val recoveryKey: String) : BootstrapActions() - object SsoAuthDone: BootstrapActions() - data class PasswordAuthDone(val password: String): BootstrapActions() - object ReAuthCancelled: BootstrapActions() + object SsoAuthDone : BootstrapActions() + data class PasswordAuthDone(val password: String) : BootstrapActions() + object ReAuthCancelled : BootstrapActions() } diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapBottomSheet.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapBottomSheet.kt index a42c3d2dda..50f9526da5 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapBottomSheet.kt @@ -33,8 +33,8 @@ import androidx.fragment.app.FragmentManager import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState import com.google.android.material.dialog.MaterialAlertDialogBuilder +import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R -import im.vector.app.core.di.ScreenComponent import im.vector.app.core.extensions.commitTransaction import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.registerStartForActivityResult @@ -43,9 +43,9 @@ import im.vector.app.databinding.BottomSheetBootstrapBinding import im.vector.app.features.auth.ReAuthActivity import kotlinx.parcelize.Parcelize import org.matrix.android.sdk.api.auth.data.LoginFlowTypes -import javax.inject.Inject import kotlin.reflect.KClass +@AndroidEntryPoint class BootstrapBottomSheet : VectorBaseBottomSheetDialogFragment() { @Parcelize @@ -55,15 +55,8 @@ class BootstrapBottomSheet : VectorBaseBottomSheetDialogFragment() { +class BootstrapConfirmPassphraseFragment @Inject constructor() : + VectorBaseFragment() { override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentBootstrapEnterPassphraseBinding { return FragmentBootstrapEnterPassphraseBinding.inflate(inflater, container, false) diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapCrossSigningTask.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapCrossSigningTask.kt index 70cda4bf79..cc863346aa 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapCrossSigningTask.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapCrossSigningTask.kt @@ -81,9 +81,9 @@ class BootstrapCrossSigningTask @Inject constructor( Timber.d("## BootstrapCrossSigningTask: mode:${params.setupMode} Starting...") // Ensure cross-signing is initialized. Due to migration it is maybe not always correctly initialized - val shouldSetCrossSigning = !crossSigningService.isCrossSigningInitialized() - || (params.setupMode == SetupMode.PASSPHRASE_AND_NEEDED_SECRETS_RESET && !crossSigningService.allPrivateKeysKnown()) - || (params.setupMode == SetupMode.HARD_RESET) + val shouldSetCrossSigning = !crossSigningService.isCrossSigningInitialized() || + (params.setupMode == SetupMode.PASSPHRASE_AND_NEEDED_SECRETS_RESET && !crossSigningService.allPrivateKeysKnown()) || + (params.setupMode == SetupMode.HARD_RESET) if (shouldSetCrossSigning) { Timber.d("## BootstrapCrossSigningTask: Cross signing not enabled, so initialize") params.progressListener?.onProgress( @@ -227,9 +227,9 @@ class BootstrapCrossSigningTask @Inject constructor( val knownMegolmSecret = session.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo() val isMegolmBackupSecretKnown = knownMegolmSecret != null && knownMegolmSecret.version == serverVersion?.version - val shouldCreateKeyBackup = serverVersion == null - || (params.setupMode == SetupMode.PASSPHRASE_AND_NEEDED_SECRETS_RESET && !isMegolmBackupSecretKnown) - || (params.setupMode == SetupMode.HARD_RESET) + val shouldCreateKeyBackup = serverVersion == null || + (params.setupMode == SetupMode.PASSPHRASE_AND_NEEDED_SECRETS_RESET && !isMegolmBackupSecretKnown) || + (params.setupMode == SetupMode.HARD_RESET) if (shouldCreateKeyBackup) { // clear all existing backups while (serverVersion != null) { diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapEnterPassphraseFragment.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapEnterPassphraseFragment.kt index 9ecc7719a5..f43ddb8888 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapEnterPassphraseFragment.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapEnterPassphraseFragment.kt @@ -30,12 +30,11 @@ import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.FragmentBootstrapEnterPassphraseBinding import im.vector.app.features.settings.VectorLocale import io.reactivex.android.schedulers.AndroidSchedulers - import java.util.concurrent.TimeUnit import javax.inject.Inject -class BootstrapEnterPassphraseFragment @Inject constructor() - : VectorBaseFragment() { +class BootstrapEnterPassphraseFragment @Inject constructor() : + VectorBaseFragment() { override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentBootstrapEnterPassphraseBinding { return FragmentBootstrapEnterPassphraseBinding.inflate(inflater, container, false) diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapMigrateBackupFragment.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapMigrateBackupFragment.kt index 23d3068e34..b40a194a15 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapMigrateBackupFragment.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapMigrateBackupFragment.kt @@ -40,7 +40,6 @@ import im.vector.app.core.utils.colorizeMatchingText import im.vector.app.core.utils.startImportTextFromFileIntent import im.vector.app.databinding.FragmentBootstrapMigrateBackupBinding import io.reactivex.android.schedulers.AndroidSchedulers - import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.internal.crypto.keysbackup.util.isValidRecoveryKey import java.util.concurrent.TimeUnit diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapReAuthFragment.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapReAuthFragment.kt index 507050c2e8..c46ccbf08e 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapReAuthFragment.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapReAuthFragment.kt @@ -27,7 +27,6 @@ import im.vector.app.core.extensions.setTextOrHide import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.resources.ColorProvider import im.vector.app.databinding.FragmentBootstrapReauthBinding - import javax.inject.Inject class BootstrapReAuthFragment @Inject constructor( diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSaveRecoveryKeyFragment.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSaveRecoveryKeyFragment.kt index 0e11c35289..8a41c7ce4d 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSaveRecoveryKeyFragment.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSaveRecoveryKeyFragment.kt @@ -34,7 +34,6 @@ import im.vector.app.core.resources.ColorProvider import im.vector.app.core.utils.startSharePlainTextIntent import im.vector.app.core.utils.toast import im.vector.app.databinding.FragmentBootstrapSaveKeyBinding - import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import javax.inject.Inject diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSetupRecoveryKeyFragment.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSetupRecoveryKeyFragment.kt index 8676f1fb6b..2765dfefd3 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSetupRecoveryKeyFragment.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSetupRecoveryKeyFragment.kt @@ -26,11 +26,10 @@ import com.airbnb.mvrx.withState import im.vector.app.R import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.FragmentBootstrapSetupRecoveryBinding - import javax.inject.Inject -class BootstrapSetupRecoveryKeyFragment @Inject constructor() - : VectorBaseFragment() { +class BootstrapSetupRecoveryKeyFragment @Inject constructor() : + VectorBaseFragment() { override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentBootstrapSetupRecoveryBinding { return FragmentBootstrapSetupRecoveryBinding.inflate(inflater, container, false) diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSharedViewModel.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSharedViewModel.kt index 866a87d7f9..f75ab634b8 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSharedViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSharedViewModel.kt @@ -16,41 +16,39 @@ package im.vector.app.features.crypto.recover -import androidx.lifecycle.viewModelScope import com.airbnb.mvrx.Fail -import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.Loading -import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized -import com.airbnb.mvrx.ViewModelContext import com.nulabinc.zxcvbn.Zxcvbn import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import im.vector.app.R +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.WaitingViewData import im.vector.app.core.resources.StringProvider import im.vector.app.features.auth.ReAuthActivity -import im.vector.app.features.login.ReAuthHelper import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.auth.UIABaseAuth import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor +import org.matrix.android.sdk.api.auth.UserPasswordAuth import org.matrix.android.sdk.api.auth.data.LoginFlowTypes +import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse +import org.matrix.android.sdk.api.auth.registration.nextUncompletedStage import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.securestorage.RawBytesKeySpec -import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse -import org.matrix.android.sdk.api.auth.registration.nextUncompletedStage import org.matrix.android.sdk.internal.crypto.crosssigning.fromBase64 import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersionResult import org.matrix.android.sdk.internal.crypto.keysbackup.util.extractCurveKeyFromRecoveryKey import org.matrix.android.sdk.internal.crypto.model.rest.DefaultBaseAuth -import org.matrix.android.sdk.api.auth.UIABaseAuth -import org.matrix.android.sdk.api.auth.UserPasswordAuth import org.matrix.android.sdk.internal.util.awaitCallback import java.io.OutputStream import kotlin.coroutines.Continuation @@ -59,13 +57,11 @@ import kotlin.coroutines.resumeWithException class BootstrapSharedViewModel @AssistedInject constructor( @Assisted initialState: BootstrapViewState, - @Assisted val args: BootstrapBottomSheet.Args, private val stringProvider: StringProvider, private val errorFormatter: ErrorFormatter, private val session: Session, private val bootstrapTask: BootstrapCrossSigningTask, private val migrationTask: BackupToQuadSMigrationTask, - private val reAuthHelper: ReAuthHelper ) : VectorViewModel(initialState) { private var doesKeyBackupExist: Boolean = false @@ -73,10 +69,12 @@ class BootstrapSharedViewModel @AssistedInject constructor( private val zxcvbn = Zxcvbn() @AssistedFactory - interface Factory { - fun create(initialState: BootstrapViewState, args: BootstrapBottomSheet.Args): BootstrapSharedViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: BootstrapViewState): BootstrapSharedViewModel } + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() + // private var _pendingSession: String? = null var uiaContinuation: Continuation? = null @@ -84,7 +82,7 @@ class BootstrapSharedViewModel @AssistedInject constructor( init { - when (args.setUpMode) { + when (initialState.setupMode) { SetupMode.PASSPHRASE_RESET, SetupMode.PASSPHRASE_AND_NEEDED_SECRETS_RESET, SetupMode.HARD_RESET -> { @@ -410,7 +408,7 @@ class BootstrapSharedViewModel @AssistedInject constructor( progressListener = progressListener, passphrase = state.passphrase, keySpec = state.migrationRecoveryKey?.let { extractCurveKeyFromRecoveryKey(it)?.let { RawBytesKeySpec(it) } }, - setupMode = args.setUpMode + setupMode = state.setupMode ) ) { bootstrapResult -> when (bootstrapResult) { @@ -437,9 +435,9 @@ class BootstrapSharedViewModel @AssistedInject constructor( } } is BootstrapResult.Failure -> { - if (bootstrapResult is BootstrapResult.GenericError - && bootstrapResult.failure is Failure.OtherServerError - && bootstrapResult.failure.httpCode == 401) { + if (bootstrapResult is BootstrapResult.GenericError && + bootstrapResult.failure is Failure.OtherServerError && + bootstrapResult.failure.httpCode == 401) { // Ignore this error } else { _viewEvents.post(BootstrapViewEvents.ModalError(bootstrapResult.error ?: stringProvider.getString(R.string.matrix_error))) @@ -516,7 +514,7 @@ class BootstrapSharedViewModel @AssistedInject constructor( BootstrapStep.CheckingMigration -> Unit is BootstrapStep.FirstForm -> { _viewEvents.post( - when (args.setUpMode) { + when (state.setupMode) { SetupMode.CROSS_SIGNING_ONLY, SetupMode.NORMAL -> BootstrapViewEvents.SkipBootstrap() else -> BootstrapViewEvents.Dismiss(success = false) @@ -547,18 +545,4 @@ class BootstrapSharedViewModel @AssistedInject constructor( else -> stringProvider.getString(R.string.unexpected_error) } } - - // ====================================== - // Companion, view model assisted creation - // ====================================== - - companion object : MvRxViewModelFactory { - - override fun create(viewModelContext: ViewModelContext, state: BootstrapViewState): BootstrapSharedViewModel? { - val fragment: BootstrapBottomSheet = (viewModelContext as FragmentViewModelContext).fragment() - val args: BootstrapBottomSheet.Args = fragment.arguments?.getParcelable(BootstrapBottomSheet.EXTRA_ARGS) - ?: BootstrapBottomSheet.Args(SetupMode.CROSS_SIGNING_ONLY) - return fragment.bootstrapViewModelFactory.create(state, args) - } - } } diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapViewState.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapViewState.kt index 4e94ddf0bf..9d5760cbf9 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapViewState.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapViewState.kt @@ -17,13 +17,14 @@ package im.vector.app.features.crypto.recover import com.airbnb.mvrx.Async -import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.Uninitialized import com.nulabinc.zxcvbn.Strength import im.vector.app.core.platform.WaitingViewData import org.matrix.android.sdk.api.session.securestorage.SsssKeyCreationInfo data class BootstrapViewState( + val setupMode: SetupMode, val step: BootstrapStep = BootstrapStep.CheckingMigration, val passphrase: String? = null, val migrationRecoveryKey: String? = null, @@ -34,4 +35,7 @@ data class BootstrapViewState( val recoveryKeyCreationInfo: SsssKeyCreationInfo? = null, val initializationWaitingViewData: WaitingViewData? = null, val recoverySaveFileProcess: Async = Uninitialized -) : MvRxState +) : MavericksState { + + constructor(args: BootstrapBottomSheet.Args) : this(setupMode = args.setUpMode) +} diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapWaitingFragment.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapWaitingFragment.kt index 7852ff172c..8cf48a7c66 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapWaitingFragment.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapWaitingFragment.kt @@ -23,11 +23,10 @@ import com.airbnb.mvrx.parentFragmentViewModel import com.airbnb.mvrx.withState import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.FragmentBootstrapWaitingBinding - import javax.inject.Inject -class BootstrapWaitingFragment @Inject constructor() - : VectorBaseFragment() { +class BootstrapWaitingFragment @Inject constructor() : + VectorBaseFragment() { override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentBootstrapWaitingBinding { return FragmentBootstrapWaitingBinding.inflate(inflater, container, false) diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/IncomingVerificationRequestHandler.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/IncomingVerificationRequestHandler.kt index 0b93120b52..6c009d3786 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/IncomingVerificationRequestHandler.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/IncomingVerificationRequestHandler.kt @@ -18,6 +18,7 @@ package im.vector.app.features.crypto.verification import android.content.Context import im.vector.app.R import im.vector.app.core.platform.VectorBaseActivity +import im.vector.app.features.displayname.getBestName 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 @@ -63,7 +64,7 @@ class IncomingVerificationRequestHandler @Inject constructor( is VerificationTxState.OnStarted -> { // Add a notification for every incoming request val user = session?.getUser(tx.otherUserId) - val name = user?.getBestName() ?: tx.otherUserId + val name = user?.toMatrixItem()?.getBestName() ?: tx.otherUserId val alert = VerificationVectorAlert( uid, context.getString(R.string.sas_incoming_request_notif_title), diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheet.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheet.kt index 47033199f4..4ef0109227 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheet.kt @@ -24,12 +24,12 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment -import com.airbnb.mvrx.MvRx +import com.airbnb.mvrx.Mavericks import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState import com.google.android.material.dialog.MaterialAlertDialogBuilder +import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R -import im.vector.app.core.di.ScreenComponent import im.vector.app.core.extensions.commitTransaction import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.registerStartForActivityResult @@ -45,6 +45,7 @@ import im.vector.app.features.crypto.verification.emoji.VerificationEmojiCodeFra import im.vector.app.features.crypto.verification.qrconfirmation.VerificationQRWaitingFragment import im.vector.app.features.crypto.verification.qrconfirmation.VerificationQrScannedByOtherFragment import im.vector.app.features.crypto.verification.request.VerificationRequestFragment +import im.vector.app.features.displayname.getBestName import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.settings.VectorSettingsActivity import kotlinx.parcelize.Parcelize @@ -60,6 +61,7 @@ import timber.log.Timber import javax.inject.Inject import kotlin.reflect.KClass +@AndroidEntryPoint class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() { @Parcelize @@ -74,18 +76,11 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment if (state.isMe) { avatarRenderer.render(matrixItem, views.otherUserAvatarImageView) - if (state.sasTransactionState == VerificationTxState.Verified - || state.qrTransactionState == VerificationTxState.Verified - || state.verifiedFromPrivateKeys) { + if (state.sasTransactionState == VerificationTxState.Verified || + state.qrTransactionState == VerificationTxState.Verified || + state.verifiedFromPrivateKeys) { views.otherUserShield.render(RoomEncryptionTrustLevel.Trusted) } else { views.otherUserShield.render(RoomEncryptionTrustLevel.Warning) @@ -184,7 +179,7 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment { showFragment(VerificationEmojiCodeFragment::class, Bundle().apply { - putParcelable(MvRx.KEY_ARG, VerificationArgs( + putParcelable(Mavericks.KEY_ARG, VerificationArgs( state.otherUserMxItem?.id ?: "", // If it was outgoing it.transaction id would be null, but the pending request // would be updated (from localId to txId) @@ -244,12 +239,12 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment { showFragment(VerificationConclusionFragment::class, Bundle().apply { - putParcelable(MvRx.KEY_ARG, VerificationConclusionFragment.Args(true, null, state.isMe)) + putParcelable(Mavericks.KEY_ARG, VerificationConclusionFragment.Args(true, null, state.isMe)) }) } is VerificationTxState.Cancelled -> { showFragment(VerificationConclusionFragment::class, Bundle().apply { - putParcelable(MvRx.KEY_ARG, VerificationConclusionFragment.Args(false, state.sasTransactionState.cancelCode.value, state.isMe)) + putParcelable(Mavericks.KEY_ARG, VerificationConclusionFragment.Args(false, state.sasTransactionState.cancelCode.value, state.isMe)) }) } } @@ -265,7 +260,7 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment { showFragment(VerificationQRWaitingFragment::class, Bundle().apply { - putParcelable(MvRx.KEY_ARG, VerificationQRWaitingFragment.Args( + putParcelable(Mavericks.KEY_ARG, VerificationQRWaitingFragment.Args( isMe = state.isMe, otherUserName = state.otherUserMxItem?.getBestName() ?: "" )) @@ -274,13 +269,13 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment { showFragment(VerificationConclusionFragment::class, Bundle().apply { - putParcelable(MvRx.KEY_ARG, VerificationConclusionFragment.Args(true, null, state.isMe)) + putParcelable(Mavericks.KEY_ARG, VerificationConclusionFragment.Args(true, null, state.isMe)) }) return@withState } is VerificationTxState.Cancelled -> { showFragment(VerificationConclusionFragment::class, Bundle().apply { - putParcelable(MvRx.KEY_ARG, VerificationConclusionFragment.Args(false, state.qrTransactionState.cancelCode.value, state.isMe)) + putParcelable(Mavericks.KEY_ARG, VerificationConclusionFragment.Args(false, state.qrTransactionState.cancelCode.value, state.isMe)) }) return@withState } @@ -294,7 +289,7 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment = Uninitialized, val pendingLocalId: String? = null, val sasTransactionState: VerificationTxState? = null, val qrTransactionState: VerificationTxState? = null, val transactionId: String? = null, - // true when we display the loading and we wait for the other (incoming request) - val selfVerificationMode: Boolean = false, val verifiedFromPrivateKeys: Boolean = false, val verifyingFrom4S: Boolean = false, val isMe: Boolean = false, @@ -79,29 +80,41 @@ data class VerificationBottomSheetViewState( val quadSContainsSecrets: Boolean = true, val quadSHasBeenReset: Boolean = false, val hasAnyOtherSession: Boolean = false -) : MvRxState +) : MavericksState { + + constructor(args: VerificationBottomSheet.VerificationArgs) : this( + otherUserId = args.otherUserId, + verificationId = args.verificationId, + roomId = args.roomId, + selfVerificationMode = args.selfVerificationMode + ) +} class VerificationBottomSheetViewModel @AssistedInject constructor( @Assisted initialState: VerificationBottomSheetViewState, - @Assisted val args: VerificationBottomSheet.VerificationArgs, private val session: Session, private val supportedVerificationMethodsProvider: SupportedVerificationMethodsProvider, - private val stringProvider: StringProvider) - : VectorViewModel(initialState), + private val stringProvider: StringProvider) : + VectorViewModel(initialState), VerificationService.Listener { + @AssistedFactory + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: VerificationBottomSheetViewState): VerificationBottomSheetViewModel + } + + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() + init { session.cryptoService().verificationService().addListener(this) - val userItem = session.getUser(args.otherUserId) - - val selfVerificationMode = args.selfVerificationMode + val userItem = session.getUser(initialState.otherUserId) var autoReady = false - val pr = if (selfVerificationMode) { + val pr = if (initialState.selfVerificationMode) { // See if active tx for this user and take it - session.cryptoService().verificationService().getExistingVerificationRequests(args.otherUserId) + session.cryptoService().verificationService().getExistingVerificationRequests(initialState.otherUserId) .lastOrNull { !it.isFinished } ?.also { verificationRequest -> if (verificationRequest.isIncoming && !verificationRequest.isReady) { @@ -110,15 +123,15 @@ class VerificationBottomSheetViewModel @AssistedInject constructor( } } } else { - session.cryptoService().verificationService().getExistingVerificationRequest(args.otherUserId, args.verificationId) + session.cryptoService().verificationService().getExistingVerificationRequest(initialState.otherUserId, initialState.verificationId) } - val sasTx = (pr?.transactionId ?: args.verificationId)?.let { - session.cryptoService().verificationService().getExistingTransaction(args.otherUserId, it) as? SasVerificationTransaction + val sasTx = (pr?.transactionId ?: initialState.verificationId)?.let { + session.cryptoService().verificationService().getExistingTransaction(initialState.otherUserId, it) as? SasVerificationTransaction } - val qrTx = (pr?.transactionId ?: args.verificationId)?.let { - session.cryptoService().verificationService().getExistingTransaction(args.otherUserId, it) as? QrCodeVerificationTransaction + val qrTx = (pr?.transactionId ?: initialState.verificationId)?.let { + session.cryptoService().verificationService().getExistingTransaction(initialState.otherUserId, it) as? QrCodeVerificationTransaction } val hasAnyOtherSession = session.cryptoService() @@ -132,11 +145,9 @@ class VerificationBottomSheetViewModel @AssistedInject constructor( otherUserMxItem = userItem?.toMatrixItem(), sasTransactionState = sasTx?.state, qrTransactionState = qrTx?.state, - transactionId = pr?.transactionId ?: args.verificationId, + transactionId = pr?.transactionId ?: initialState.verificationId, pendingRequest = if (pr != null) Success(pr) else Uninitialized, - selfVerificationMode = selfVerificationMode, - roomId = args.roomId, - isMe = args.otherUserId == session.myUserId, + isMe = initialState.otherUserId == session.myUserId, currentDeviceCanCrossSign = session.cryptoService().crossSigningService().canCrossSign(), quadSContainsSecrets = session.sharedSecretStorageService.isRecoverySetup(), hasAnyOtherSession = hasAnyOtherSession @@ -159,12 +170,6 @@ class VerificationBottomSheetViewModel @AssistedInject constructor( super.onCleared() } - @AssistedFactory - interface Factory { - fun create(initialState: VerificationBottomSheetViewState, - args: VerificationBottomSheet.VerificationArgs): VerificationBottomSheetViewModel - } - fun queryCancel() = withState { state -> if (state.userThinkItsNotHim) { setState { @@ -172,9 +177,9 @@ class VerificationBottomSheetViewModel @AssistedInject constructor( } } else { // if the verification is already done you can't cancel anymore - if (state.pendingRequest.invoke()?.cancelConclusion != null - || state.sasTransactionState is VerificationTxState.TerminalTxState - || state.verifyingFrom4S) { + if (state.pendingRequest.invoke()?.cancelConclusion != null || + state.sasTransactionState is VerificationTxState.TerminalTxState || + state.verifyingFrom4S) { // you cannot cancel anymore } else { setState { @@ -223,16 +228,6 @@ class VerificationBottomSheetViewModel @AssistedInject constructor( _viewEvents.post(VerificationBottomSheetViewEvents.GoToSettings) } - companion object : MvRxViewModelFactory { - - override fun create(viewModelContext: ViewModelContext, state: VerificationBottomSheetViewState): VerificationBottomSheetViewModel? { - val fragment: VerificationBottomSheet = (viewModelContext as FragmentViewModelContext).fragment() - val args: VerificationBottomSheet.VerificationArgs = viewModelContext.args() - - return fragment.verificationViewModelFactory.create(state, args) - } - } - override fun handle(action: VerificationAction) = withState { state -> val otherUserId = state.otherUserMxItem?.id ?: return@withState val roomId = state.roomId @@ -537,12 +532,12 @@ class VerificationBottomSheetViewModel @AssistedInject constructor( } } - if (pr.localId == state.pendingLocalId - || pr.localId == state.pendingRequest.invoke()?.localId - || state.pendingRequest.invoke()?.transactionId == pr.transactionId) { + if (pr.localId == state.pendingLocalId || + pr.localId == state.pendingRequest.invoke()?.localId || + state.pendingRequest.invoke()?.transactionId == pr.transactionId) { setState { copy( - transactionId = args.verificationId ?: pr.transactionId, + transactionId = state.verificationId ?: pr.transactionId, pendingRequest = Success(pr) ) } diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/cancel/VerificationCancelFragment.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/cancel/VerificationCancelFragment.kt index 497617f1ad..62bab05e42 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/cancel/VerificationCancelFragment.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/cancel/VerificationCancelFragment.kt @@ -27,7 +27,6 @@ import im.vector.app.core.extensions.configureWith import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.BottomSheetVerificationChildFragmentBinding import im.vector.app.features.crypto.verification.VerificationBottomSheetViewModel - import javax.inject.Inject class VerificationCancelFragment @Inject constructor( diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/cancel/VerificationNotMeFragment.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/cancel/VerificationNotMeFragment.kt index 46cb3a97a2..635a56a6c1 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/cancel/VerificationNotMeFragment.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/cancel/VerificationNotMeFragment.kt @@ -27,7 +27,6 @@ import im.vector.app.core.extensions.configureWith import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.BottomSheetVerificationChildFragmentBinding import im.vector.app.features.crypto.verification.VerificationBottomSheetViewModel - import javax.inject.Inject class VerificationNotMeFragment @Inject constructor( diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/choose/VerificationChooseMethodFragment.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/choose/VerificationChooseMethodFragment.kt index d3f24816a5..31a7956db3 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/choose/VerificationChooseMethodFragment.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/choose/VerificationChooseMethodFragment.kt @@ -36,12 +36,10 @@ import im.vector.app.databinding.BottomSheetVerificationChildFragmentBinding import im.vector.app.features.crypto.verification.VerificationAction import im.vector.app.features.crypto.verification.VerificationBottomSheetViewModel import im.vector.app.features.qrcode.QrCodeScannerActivity - import timber.log.Timber import javax.inject.Inject class VerificationChooseMethodFragment @Inject constructor( - val verificationChooseMethodViewModelFactory: VerificationChooseMethodViewModel.Factory, val controller: VerificationChooseMethodController ) : VectorBaseFragment(), VerificationChooseMethodController.Listener { diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/choose/VerificationChooseMethodViewModel.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/choose/VerificationChooseMethodViewModel.kt index 7b9acd2f57..7696bb8f5b 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/choose/VerificationChooseMethodViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/choose/VerificationChooseMethodViewModel.kt @@ -15,14 +15,16 @@ */ package im.vector.app.features.crypto.verification.choose -import com.airbnb.mvrx.FragmentViewModelContext -import com.airbnb.mvrx.MvRxState -import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.MavericksState +import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.ViewModelContext import dagger.assisted.Assisted -import dagger.assisted.AssistedInject import dagger.assisted.AssistedFactory -import im.vector.app.core.di.HasScreenInjector +import dagger.assisted.AssistedInject +import dagger.hilt.EntryPoints +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.SingletonEntryPoint +import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.platform.EmptyAction import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel @@ -43,13 +45,17 @@ data class VerificationChooseMethodViewState( val sasModeAvailable: Boolean = false, val isMe: Boolean = false, val canCrossSign: Boolean = false -) : MvRxState +) : MavericksState class VerificationChooseMethodViewModel @AssistedInject constructor( @Assisted initialState: VerificationChooseMethodViewState, private val session: Session ) : VectorViewModel(initialState), VerificationService.Listener { + init { + session.cryptoService().verificationService().addListener(this) + } + override fun transactionCreated(tx: VerificationTransaction) { transactionUpdated(tx) } @@ -81,28 +87,15 @@ class VerificationChooseMethodViewModel @AssistedInject constructor( } @AssistedFactory - interface Factory { - fun create(initialState: VerificationChooseMethodViewState): VerificationChooseMethodViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: VerificationChooseMethodViewState): VerificationChooseMethodViewModel } - init { - session.cryptoService().verificationService().addListener(this) - } + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() { - override fun onCleared() { - session.cryptoService().verificationService().removeListener(this) - super.onCleared() - } - - companion object : MvRxViewModelFactory { - override fun create(viewModelContext: ViewModelContext, state: VerificationChooseMethodViewState): VerificationChooseMethodViewModel? { - val fragment: VerificationChooseMethodFragment = (viewModelContext as FragmentViewModelContext).fragment() - return fragment.verificationChooseMethodViewModelFactory.create(state) - } - - override fun initialState(viewModelContext: ViewModelContext): VerificationChooseMethodViewState? { + override fun initialState(viewModelContext: ViewModelContext): VerificationChooseMethodViewState { val args: VerificationBottomSheet.VerificationArgs = viewModelContext.args() - val session = (viewModelContext.activity as HasScreenInjector).injector().activeSessionHolder().getActiveSession() + val session = EntryPoints.get(viewModelContext.app(), SingletonEntryPoint::class.java).activeSessionHolder().getActiveSession() val verificationService = session.cryptoService().verificationService() val pvr = verificationService.getExistingVerificationRequest(args.otherUserId, args.verificationId) @@ -121,5 +114,10 @@ class VerificationChooseMethodViewModel @AssistedInject constructor( } } + override fun onCleared() { + session.cryptoService().verificationService().removeListener(this) + super.onCleared() + } + override fun handle(action: EmptyAction) {} } diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/conclusion/VerificationConclusionViewModel.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/conclusion/VerificationConclusionViewModel.kt index 50912af1c2..82bdbccdb3 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/conclusion/VerificationConclusionViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/conclusion/VerificationConclusionViewModel.kt @@ -15,8 +15,8 @@ */ package im.vector.app.features.crypto.verification.conclusion -import com.airbnb.mvrx.MvRxState -import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.MavericksState +import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.ViewModelContext import im.vector.app.core.platform.EmptyAction import im.vector.app.core.platform.EmptyViewEvents @@ -27,7 +27,7 @@ import org.matrix.android.sdk.api.session.crypto.verification.safeValueOf data class VerificationConclusionViewState( val conclusionState: ConclusionState = ConclusionState.CANCELLED, val isSelfVerification: Boolean = false -) : MvRxState +) : MavericksState enum class ConclusionState { SUCCESS, @@ -35,10 +35,10 @@ enum class ConclusionState { CANCELLED } -class VerificationConclusionViewModel(initialState: VerificationConclusionViewState) - : VectorViewModel(initialState) { +class VerificationConclusionViewModel(initialState: VerificationConclusionViewState) : + VectorViewModel(initialState) { - companion object : MvRxViewModelFactory { + companion object : MavericksViewModelFactory { override fun initialState(viewModelContext: ViewModelContext): VerificationConclusionViewState? { val args = viewModelContext.args() diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/emoji/VerificationEmojiCodeController.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/emoji/VerificationEmojiCodeController.kt index 5159a69090..eab53ea954 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/emoji/VerificationEmojiCodeController.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/emoji/VerificationEmojiCodeController.kt @@ -30,6 +30,7 @@ import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationD import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationEmojisItem import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationNoticeItem import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationWaitingItem +import im.vector.app.features.displayname.getBestName import javax.inject.Inject class VerificationEmojiCodeController @Inject constructor( diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/emoji/VerificationEmojiCodeFragment.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/emoji/VerificationEmojiCodeFragment.kt index 97f521ee72..3f4eaf8ac9 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/emoji/VerificationEmojiCodeFragment.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/emoji/VerificationEmojiCodeFragment.kt @@ -28,11 +28,9 @@ import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.BottomSheetVerificationChildFragmentBinding import im.vector.app.features.crypto.verification.VerificationAction import im.vector.app.features.crypto.verification.VerificationBottomSheetViewModel - import javax.inject.Inject class VerificationEmojiCodeFragment @Inject constructor( - val viewModelFactory: VerificationEmojiCodeViewModel.Factory, val controller: VerificationEmojiCodeController ) : VectorBaseFragment(), VerificationEmojiCodeController.Listener { diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/emoji/VerificationEmojiCodeViewModel.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/emoji/VerificationEmojiCodeViewModel.kt index 44f0e752c8..6f213adb7e 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/emoji/VerificationEmojiCodeViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/emoji/VerificationEmojiCodeViewModel.kt @@ -17,17 +17,19 @@ package im.vector.app.features.crypto.verification.emoji import com.airbnb.mvrx.Async import com.airbnb.mvrx.Fail -import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.Loading -import com.airbnb.mvrx.MvRxState -import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.MavericksState +import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.ViewModelContext import dagger.assisted.Assisted -import dagger.assisted.AssistedInject import dagger.assisted.AssistedFactory -import im.vector.app.core.di.HasScreenInjector +import dagger.assisted.AssistedInject +import dagger.hilt.EntryPoints +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.SingletonEntryPoint +import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.platform.EmptyAction import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel @@ -48,7 +50,7 @@ data class VerificationEmojiCodeViewState( val emojiDescription: Async> = Uninitialized, val decimalDescription: Async = Uninitialized, val isWaitingFromOther: Boolean = false -) : MvRxState +) : MavericksState class VerificationEmojiCodeViewModel @AssistedInject constructor( @Assisted initialState: VerificationEmojiCodeViewState, @@ -151,20 +153,15 @@ class VerificationEmojiCodeViewModel @AssistedInject constructor( } @AssistedFactory - interface Factory { - fun create(initialState: VerificationEmojiCodeViewState): VerificationEmojiCodeViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: VerificationEmojiCodeViewState): VerificationEmojiCodeViewModel } - companion object : MvRxViewModelFactory { - - override fun create(viewModelContext: ViewModelContext, state: VerificationEmojiCodeViewState): VerificationEmojiCodeViewModel? { - val factory = (viewModelContext as FragmentViewModelContext).fragment().viewModelFactory - return factory.create(state) - } + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() { override fun initialState(viewModelContext: ViewModelContext): VerificationEmojiCodeViewState? { val args = viewModelContext.args() - val session = (viewModelContext.activity as HasScreenInjector).injector().activeSessionHolder().getActiveSession() + val session = EntryPoints.get(viewModelContext.app(), SingletonEntryPoint::class.java).activeSessionHolder().getActiveSession() val matrixItem = session.getUser(args.otherUserId)?.toMatrixItem() return VerificationEmojiCodeViewState( diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/qrconfirmation/VerificationQRWaitingFragment.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/qrconfirmation/VerificationQRWaitingFragment.kt index 12a136f70c..441885dd10 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/qrconfirmation/VerificationQRWaitingFragment.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/qrconfirmation/VerificationQRWaitingFragment.kt @@ -21,7 +21,7 @@ import android.os.Parcelable import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import com.airbnb.mvrx.MvRx +import com.airbnb.mvrx.Mavericks import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.configureWith import im.vector.app.core.platform.VectorBaseFragment @@ -46,7 +46,7 @@ class VerificationQRWaitingFragment @Inject constructor( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) setupRecyclerView() - (arguments?.getParcelable(MvRx.KEY_ARG) as? Args)?.let { + (arguments?.getParcelable(Mavericks.KEY_ARG) as? Args)?.let { controller.update(it) } } diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/qrconfirmation/VerificationQrScannedByOtherController.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/qrconfirmation/VerificationQrScannedByOtherController.kt index 01862a5ab4..f3990842f9 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/qrconfirmation/VerificationQrScannedByOtherController.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/qrconfirmation/VerificationQrScannedByOtherController.kt @@ -25,6 +25,7 @@ import im.vector.app.features.crypto.verification.VerificationBottomSheetViewSta import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationActionItem import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationBigImageItem import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationNoticeItem +import im.vector.app.features.displayname.getBestName import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel import javax.inject.Inject diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/qrconfirmation/VerificationQrScannedByOtherFragment.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/qrconfirmation/VerificationQrScannedByOtherFragment.kt index 2ef015edbc..9e77506e3b 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/qrconfirmation/VerificationQrScannedByOtherFragment.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/qrconfirmation/VerificationQrScannedByOtherFragment.kt @@ -27,7 +27,6 @@ import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.BottomSheetVerificationChildFragmentBinding import im.vector.app.features.crypto.verification.VerificationAction import im.vector.app.features.crypto.verification.VerificationBottomSheetViewModel - import javax.inject.Inject class VerificationQrScannedByOtherFragment @Inject constructor( diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/request/VerificationRequestController.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/request/VerificationRequestController.kt index 260bcf597e..6440c0032b 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/request/VerificationRequestController.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/request/VerificationRequestController.kt @@ -31,6 +31,7 @@ import im.vector.app.features.crypto.verification.epoxy.bottomSheetSelfWaitItem import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationActionItem import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationNoticeItem import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationWaitingItem +import im.vector.app.features.displayname.getBestName import javax.inject.Inject class VerificationRequestController @Inject constructor( diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/request/VerificationRequestFragment.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/request/VerificationRequestFragment.kt index 479dc7b3c7..238249683f 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/request/VerificationRequestFragment.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/request/VerificationRequestFragment.kt @@ -27,7 +27,6 @@ import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.BottomSheetVerificationChildFragmentBinding import im.vector.app.features.crypto.verification.VerificationAction import im.vector.app.features.crypto.verification.VerificationBottomSheetViewModel - import javax.inject.Inject class VerificationRequestFragment @Inject constructor( 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 36ccef1fca..772ef99931 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 @@ -27,14 +27,14 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Loading -import com.airbnb.mvrx.MvRx +import com.airbnb.mvrx.Mavericks import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.viewModel import com.airbnb.mvrx.withState import com.google.android.material.dialog.MaterialAlertDialogBuilder +import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R -import im.vector.app.core.di.ScreenComponent import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.replaceFragment import im.vector.app.core.extensions.toMvRxBundle @@ -45,10 +45,9 @@ import kotlinx.parcelize.Parcelize import org.billcarsonfr.jsonviewer.JSonViewerFragment import javax.inject.Inject -class RoomDevToolActivity : SimpleFragmentActivity(), RoomDevToolViewModel.Factory, - FragmentManager.OnBackStackChangedListener { +@AndroidEntryPoint +class RoomDevToolActivity : SimpleFragmentActivity(), FragmentManager.OnBackStackChangedListener { - @Inject lateinit var viewModelFactory: RoomDevToolViewModel.Factory @Inject lateinit var colorProvider: ColorProvider // private lateinit var viewModel: RoomDevToolViewModel @@ -65,15 +64,6 @@ class RoomDevToolActivity : SimpleFragmentActivity(), RoomDevToolViewModel.Facto val roomId: String ) : Parcelable - override fun injectWith(injector: ScreenComponent) { - super.injectWith(injector) - injector.inject(this) - } - - override fun create(initialState: RoomDevToolViewState): RoomDevToolViewModel { - return viewModelFactory.create(initialState) - } - override fun initUiAndData() { super.initUiAndData() viewModel.subscribe(this) { @@ -194,8 +184,8 @@ class RoomDevToolActivity : SimpleFragmentActivity(), RoomDevToolViewModel.Facto state.displayMode is RoomDevToolViewState.Mode.StateEventDetail } R.id.menuItemSend -> { - state.displayMode is RoomDevToolViewState.Mode.EditEventContent - || state.displayMode is RoomDevToolViewState.Mode.SendEventForm + state.displayMode is RoomDevToolViewState.Mode.EditEventContent || + state.displayMode is RoomDevToolViewState.Mode.SendEventForm } else -> true } @@ -208,7 +198,7 @@ class RoomDevToolActivity : SimpleFragmentActivity(), RoomDevToolViewModel.Facto fun intent(context: Context, roomId: String): Intent { return Intent(context, RoomDevToolActivity::class.java).apply { - putExtra(MvRx.KEY_ARG, Args(roomId)) + putExtra(Mavericks.KEY_ARG, Args(roomId)) } } } diff --git a/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolEditFragment.kt b/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolEditFragment.kt index 98fe19a765..9af51f67b3 100644 --- a/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolEditFragment.kt +++ b/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolEditFragment.kt @@ -28,8 +28,8 @@ import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.FragmentDevtoolsEditorBinding import javax.inject.Inject -class RoomDevToolEditFragment @Inject constructor() - : VectorBaseFragment() { +class RoomDevToolEditFragment @Inject constructor() : + VectorBaseFragment() { private val sharedViewModel: RoomDevToolViewModel by activityViewModel() diff --git a/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolViewModel.kt b/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolViewModel.kt index 9fffe70872..04d90a63e7 100644 --- a/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolViewModel.kt @@ -16,19 +16,17 @@ package im.vector.app.features.devtools -import androidx.lifecycle.viewModelScope -import com.airbnb.mvrx.ActivityViewModelContext import com.airbnb.mvrx.Fail -import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.Loading -import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.Success -import com.airbnb.mvrx.ViewModelContext import com.squareup.moshi.Types import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import im.vector.app.R +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider @@ -40,8 +38,8 @@ 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.message.MessageContent import org.matrix.android.sdk.api.util.JsonDict +import org.matrix.android.sdk.flow.flow import org.matrix.android.sdk.internal.di.MoshiProvider -import org.matrix.android.sdk.rx.rx class RoomDevToolViewModel @AssistedInject constructor( @Assisted val initialState: RoomDevToolViewState, @@ -51,25 +49,15 @@ class RoomDevToolViewModel @AssistedInject constructor( ) : VectorViewModel(initialState) { @AssistedFactory - interface Factory { - fun create(initialState: RoomDevToolViewState): RoomDevToolViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: RoomDevToolViewState): RoomDevToolViewModel } - companion object : MvRxViewModelFactory { - - @JvmStatic - override fun create(viewModelContext: ViewModelContext, state: RoomDevToolViewState): RoomDevToolViewModel { - 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") - } - } + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() init { session.getRoom(initialState.roomId) - ?.rx() + ?.flow() ?.liveStateEvents(emptySet()) ?.execute { async -> copy(stateEvents = async) diff --git a/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolViewState.kt b/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolViewState.kt index 885de005b0..a5b7db9821 100644 --- a/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolViewState.kt +++ b/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolViewState.kt @@ -17,7 +17,7 @@ package im.vector.app.features.devtools import com.airbnb.mvrx.Async -import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.Uninitialized import org.matrix.android.sdk.api.session.events.model.Event @@ -31,7 +31,7 @@ data class RoomDevToolViewState( val editedContent: String? = null, val modalLoading: Async = Uninitialized, val sendEventDraft: SendEventDraft? = null -) : MvRxState { +) : MavericksState { constructor(args: RoomDevToolActivity.Args) : this(roomId = args.roomId, displayMode = Mode.Root) diff --git a/vector/src/main/java/im/vector/app/features/discovery/DiscoveryPolicyItem.kt b/vector/src/main/java/im/vector/app/features/discovery/DiscoveryPolicyItem.kt new file mode 100644 index 0000000000..c97a2286ae --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/discovery/DiscoveryPolicyItem.kt @@ -0,0 +1,51 @@ +/* + * 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.discovery + +import android.widget.TextView +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import com.airbnb.epoxy.EpoxyModelWithHolder +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.onClick + +@EpoxyModelClass(layout = R.layout.item_discovery_policy) +abstract class DiscoveryPolicyItem : EpoxyModelWithHolder() { + + @EpoxyAttribute + var name: String? = null + + @EpoxyAttribute + var url: String? = null + + @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) + var clickListener: ClickListener? = null + + override fun bind(holder: Holder) { + super.bind(holder) + holder.title.text = name + holder.url.text = url + holder.view.onClick(clickListener) + } + + class Holder : VectorEpoxyHolder() { + val title by bind(R.id.discovery_policy_name) + val url by bind(R.id.discovery_policy_url) + } +} diff --git a/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsAction.kt b/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsAction.kt index 426f1321e7..a2f855c499 100644 --- a/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsAction.kt +++ b/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsAction.kt @@ -31,4 +31,5 @@ sealed class DiscoverySettingsAction : VectorViewModelAction { data class FinalizeBind3pid(val threePid: ThreePid) : DiscoverySettingsAction() data class SubmitMsisdnToken(val threePid: ThreePid.Msisdn, val code: String) : DiscoverySettingsAction() data class CancelBinding(val threePid: ThreePid) : DiscoverySettingsAction() + data class SetPoliciesExpandState(val expanded: Boolean) : DiscoverySettingsAction() } diff --git a/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsController.kt b/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsController.kt index 1e8db2571d..d8c67592f1 100644 --- a/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsController.kt +++ b/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsController.kt @@ -31,6 +31,7 @@ import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.extensions.getFormattedValue import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.StringProvider +import im.vector.app.features.form.formAdvancedToggleItem import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.session.identity.SharedState import org.matrix.android.sdk.api.session.identity.ThreePid @@ -62,7 +63,7 @@ class DiscoverySettingsController @Inject constructor( } is Success -> { buildIdentityServerSection(data) - val hasIdentityServer = data.identityServer().isNullOrBlank().not() + val hasIdentityServer = data.identityServer()?.serverUrl.isNullOrBlank().not() if (hasIdentityServer && !data.termsNotSigned) { buildConsentSection(data) buildEmailsSection(data.emailList) @@ -106,7 +107,8 @@ class DiscoverySettingsController @Inject constructor( } private fun buildIdentityServerSection(data: DiscoverySettingsState) { - val identityServer = data.identityServer() ?: stringProvider.getString(R.string.none) + val identityServer = data.identityServer() + val identityServerUrl = identityServer?.serverUrl ?: stringProvider.getString(R.string.none) val host = this settingsSectionTitleItem { @@ -116,28 +118,58 @@ class DiscoverySettingsController @Inject constructor( settingsItem { id("idServer") - title(identityServer) + title(identityServerUrl) } - if (data.identityServer() != null && data.termsNotSigned) { + val policies = identityServer?.policies + if (policies != null) { + formAdvancedToggleItem { + id("policy-urls") + val titleRes = if (data.isIdentityPolicyUrlsExpanded) { + R.string.settings_discovery_hide_identity_server_policy_title + } else R.string.settings_discovery_show_identity_server_policy_title + title(host.stringProvider.getString(titleRes)) + expanded(data.isIdentityPolicyUrlsExpanded) + listener { host.listener?.onPolicyUrlsExpandedStateToggled(!data.isIdentityPolicyUrlsExpanded) } + } + if (data.isIdentityPolicyUrlsExpanded) { + if (policies.isEmpty()) { + settingsInfoItem { + id("emptyPolicy") + helperText(host.stringProvider.getString(R.string.settings_discovery_no_policy_provided)) + } + } else { + policies.forEach { policy -> + discoveryPolicyItem { + id(policy.url) + name(policy.name) + url(policy.url) + clickListener { host.listener?.onPolicyTapped(policy) } + } + } + } + } + } + + if (identityServer != null && data.termsNotSigned) { settingsInfoItem { id("idServerFooter") - helperText(host.stringProvider.getString(R.string.settings_agree_to_terms, identityServer)) + helperText(host.stringProvider.getString(R.string.settings_agree_to_terms, identityServerUrl)) showCompoundDrawable(true) itemClickListener { host.listener?.openIdentityServerTerms() } } settingsButtonItem { id("seeTerms") colorProvider(host.colorProvider) - buttonTitle(host.stringProvider.getString(R.string.open_terms_of, identityServer)) + buttonTitle(host.stringProvider.getString(R.string.open_terms_of, identityServerUrl)) buttonClickListener { host.listener?.openIdentityServerTerms() } } } else { settingsInfoItem { id("idServerFooter") showCompoundDrawable(false) - if (data.identityServer() != null) { - helperText(host.stringProvider.getString(R.string.settings_discovery_identity_server_info, identityServer)) + if (identityServer != null) { + helperText(host.stringProvider.getString(R.string.settings_discovery_identity_server_info, identityServerUrl)) } else { helperTextResId(R.string.settings_discovery_identity_server_info_none) } @@ -147,7 +179,7 @@ class DiscoverySettingsController @Inject constructor( settingsButtonItem { id("change") colorProvider(host.colorProvider) - if (data.identityServer() == null) { + if (identityServer == null) { buttonTitleId(R.string.add_identity_server) } else { buttonTitleId(R.string.change_identity_server) @@ -155,7 +187,7 @@ class DiscoverySettingsController @Inject constructor( buttonClickListener { host.listener?.onTapChangeIdentityServer() } } - if (data.identityServer() != null) { + if (identityServer != null) { settingsInfoItem { id("removeInfo") helperTextResId(R.string.settings_discovery_disconnect_identity_server_info) @@ -282,8 +314,8 @@ class DiscoverySettingsController @Inject constructor( val error = pidInfo.finalRequest.error // Deal with error 500 // Ref: https://github.com/matrix-org/sydent/issues/292 - if (error is Failure.ServerError - && error.httpCode == HttpsURLConnection.HTTP_INTERNAL_ERROR /* 500 */) { + if (error is Failure.ServerError && + error.httpCode == HttpsURLConnection.HTTP_INTERNAL_ERROR /* 500 */) { stringProvider.getString(R.string.settings_text_message_sent_wrong_code) } else { errorFormatter.toHumanReadable(error) @@ -400,5 +432,7 @@ class DiscoverySettingsController @Inject constructor( fun onTapDisconnectIdentityServer() fun onTapUpdateUserConsent(newValue: Boolean) fun onTapRetryToRetrieveBindings() + fun onPolicyUrlsExpandedStateToggled(newExpandedState: Boolean) + fun onPolicyTapped(policy: IdentityServerPolicy) } } diff --git a/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsFragment.kt b/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsFragment.kt index 0b8674ec6f..7306146027 100644 --- a/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsFragment.kt @@ -21,6 +21,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.appcompat.app.AppCompatActivity +import com.airbnb.mvrx.args import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState import com.google.android.material.dialog.MaterialAlertDialogBuilder @@ -32,18 +33,19 @@ import im.vector.app.core.extensions.observeEvent import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.utils.ensureProtocol +import im.vector.app.core.utils.openUrlInChromeCustomTab +import im.vector.app.core.utils.showIdentityServerConsentDialog import im.vector.app.databinding.FragmentGenericRecyclerBinding import im.vector.app.features.discovery.change.SetIdentityServerFragment +import im.vector.app.features.navigation.SettingsActivityPayload import im.vector.app.features.settings.VectorSettingsActivity - import org.matrix.android.sdk.api.session.identity.SharedState import org.matrix.android.sdk.api.session.identity.ThreePid import org.matrix.android.sdk.api.session.terms.TermsService import javax.inject.Inject class DiscoverySettingsFragment @Inject constructor( - private val controller: DiscoverySettingsController, - val viewModelFactory: DiscoverySettingsViewModel.Factory + private val controller: DiscoverySettingsController ) : VectorBaseFragment(), DiscoverySettingsController.Listener { @@ -52,6 +54,7 @@ class DiscoverySettingsFragment @Inject constructor( } private val viewModel by fragmentViewModel(DiscoverySettingsViewModel::class) + private val discoveryArgs: SettingsActivityPayload.DiscoverySettings by args() lateinit var sharedViewModel: DiscoverySharedViewModel @@ -77,6 +80,9 @@ class DiscoverySettingsFragment @Inject constructor( } }.exhaustive } + if (discoveryArgs.expandIdentityPolicies) { + viewModel.handle(DiscoverySettingsAction.SetPoliciesExpandState(expanded = true)) + } } override fun onDestroyView() { @@ -111,7 +117,7 @@ class DiscoverySettingsFragment @Inject constructor( requireContext(), termsActivityResultLauncher, TermsService.ServiceType.IdentityService, - state.identityServer()?.ensureProtocol() ?: "", + state.identityServer()?.serverUrl?.ensureProtocol() ?: "", null) } } @@ -179,14 +185,11 @@ class DiscoverySettingsFragment @Inject constructor( override fun onTapUpdateUserConsent(newValue: Boolean) { if (newValue) { withState(viewModel) { state -> - MaterialAlertDialogBuilder(requireActivity()) - .setTitle(R.string.identity_server_consent_dialog_title) - .setMessage(getString(R.string.identity_server_consent_dialog_content, state.identityServer.invoke())) - .setPositiveButton(R.string.yes) { _, _ -> - viewModel.handle(DiscoverySettingsAction.UpdateUserConsent(true)) - } - .setNegativeButton(R.string.no, null) - .show() + requireContext().showIdentityServerConsentDialog( + state.identityServer.invoke()?.serverUrl, + policyLinkCallback = { viewModel.handle(DiscoverySettingsAction.SetPoliciesExpandState(expanded = true)) }, + consentCallBack = { viewModel.handle(DiscoverySettingsAction.UpdateUserConsent(true)) } + ) } } else { viewModel.handle(DiscoverySettingsAction.UpdateUserConsent(false)) @@ -197,6 +200,14 @@ class DiscoverySettingsFragment @Inject constructor( viewModel.handle(DiscoverySettingsAction.RetrieveBinding) } + override fun onPolicyUrlsExpandedStateToggled(newExpandedState: Boolean) { + viewModel.handle(DiscoverySettingsAction.SetPoliciesExpandState(expanded = newExpandedState)) + } + + override fun onPolicyTapped(policy: IdentityServerPolicy) { + openUrlInChromeCustomTab(requireContext(), null, policy.url) + } + private fun navigateToChangeIdentityServerFragment() { (vectorBaseActivity as? VectorSettingsActivity)?.navigateTo(SetIdentityServerFragment::class.java) } diff --git a/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsState.kt b/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsState.kt index 21fbcf1ca7..3f5be60f2e 100644 --- a/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsState.kt +++ b/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsState.kt @@ -17,14 +17,22 @@ package im.vector.app.features.discovery import com.airbnb.mvrx.Async -import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.Uninitialized data class DiscoverySettingsState( - val identityServer: Async = Uninitialized, + val identityServer: Async = Uninitialized, val emailList: Async> = Uninitialized, val phoneNumbersList: Async> = Uninitialized, // Can be true if terms are updated val termsNotSigned: Boolean = false, - val userConsent: Boolean = false -) : MvRxState + val userConsent: Boolean = false, + val isIdentityPolicyUrlsExpanded: Boolean = false +) : MavericksState + +data class IdentityServerWithTerms( + val serverUrl: String, + val policies: List +) + +data class IdentityServerPolicy(val name: String, val url: String) diff --git a/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsViewModel.kt b/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsViewModel.kt index 11fd796534..b02784dad9 100644 --- a/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsViewModel.kt @@ -15,67 +15,73 @@ */ package im.vector.app.features.discovery -import androidx.lifecycle.viewModelScope import com.airbnb.mvrx.Async import com.airbnb.mvrx.Fail -import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.Loading -import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized -import com.airbnb.mvrx.ViewModelContext import dagger.assisted.Assisted -import dagger.assisted.AssistedInject import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import im.vector.app.R +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel +import im.vector.app.core.resources.StringProvider +import im.vector.app.core.utils.ensureProtocol +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.identity.IdentityServiceError import org.matrix.android.sdk.api.session.identity.IdentityServiceListener import org.matrix.android.sdk.api.session.identity.SharedState import org.matrix.android.sdk.api.session.identity.ThreePid -import org.matrix.android.sdk.rx.rx +import org.matrix.android.sdk.api.session.terms.TermsService +import org.matrix.android.sdk.flow.flow class DiscoverySettingsViewModel @AssistedInject constructor( @Assisted initialState: DiscoverySettingsState, - private val session: Session) - : VectorViewModel(initialState) { + private val session: Session, + private val stringProvider: StringProvider +) : VectorViewModel(initialState) { @AssistedFactory - interface Factory { - fun create(initialState: DiscoverySettingsState): DiscoverySettingsViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: DiscoverySettingsState): DiscoverySettingsViewModel } - companion object : MvRxViewModelFactory { - - @JvmStatic - override fun create(viewModelContext: ViewModelContext, state: DiscoverySettingsState): DiscoverySettingsViewModel? { - val fragment: DiscoverySettingsFragment = (viewModelContext as FragmentViewModelContext).fragment() - return fragment.viewModelFactory.create(state) - } - } + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() private val identityService = session.identityService() + private val termsService: TermsService = session private val identityServerManagerListener = object : IdentityServiceListener { override fun onIdentityServerChange() = withState { state -> - val identityServerUrl = identityService.getCurrentIdentityServerUrl() - val currentIS = state.identityServer() - setState { - copy( - identityServer = Success(identityServerUrl), - userConsent = false + viewModelScope.launch { + runCatching { fetchIdentityServerWithTerms() }.fold( + onSuccess = { + val currentIS = state.identityServer() + setState { + copy( + identityServer = Success(it), + userConsent = identityService.getUserConsent() + ) + } + if (currentIS != it) retrieveBinding() + }, + onFailure = { _viewEvents.post(DiscoverySettingsViewEvents.Failure(it)) } ) } - if (currentIS != identityServerUrl) retrieveBinding() } } init { setState { copy( - identityServer = Success(identityService.getCurrentIdentityServerUrl()), + identityServer = Success(identityService.getCurrentIdentityServerUrl()?.let { IdentityServerWithTerms(it, emptyList()) }), userConsent = identityService.getUserConsent() ) } @@ -84,12 +90,12 @@ class DiscoverySettingsViewModel @AssistedInject constructor( } private fun observeThreePids() { - session.rx() + session.flow() .liveThreePIds(true) - .subscribe { + .onEach { retrieveBinding(it) } - .disposeOnClear() + .launchIn(viewModelScope) } override fun onCleared() { @@ -99,16 +105,17 @@ class DiscoverySettingsViewModel @AssistedInject constructor( override fun handle(action: DiscoverySettingsAction) { when (action) { - DiscoverySettingsAction.Refresh -> refreshPendingEmailBindings() - DiscoverySettingsAction.RetrieveBinding -> retrieveBinding() - DiscoverySettingsAction.DisconnectIdentityServer -> disconnectIdentityServer() - is DiscoverySettingsAction.ChangeIdentityServer -> changeIdentityServer(action) - is DiscoverySettingsAction.UpdateUserConsent -> handleUpdateUserConsent(action) - is DiscoverySettingsAction.RevokeThreePid -> revokeThreePid(action) - is DiscoverySettingsAction.ShareThreePid -> shareThreePid(action) - is DiscoverySettingsAction.FinalizeBind3pid -> finalizeBind3pid(action, true) - is DiscoverySettingsAction.SubmitMsisdnToken -> submitMsisdnToken(action) - is DiscoverySettingsAction.CancelBinding -> cancelBinding(action) + DiscoverySettingsAction.Refresh -> fetchContent() + DiscoverySettingsAction.RetrieveBinding -> retrieveBinding() + DiscoverySettingsAction.DisconnectIdentityServer -> disconnectIdentityServer() + is DiscoverySettingsAction.SetPoliciesExpandState -> updatePolicyUrlsExpandedState(action.expanded) + is DiscoverySettingsAction.ChangeIdentityServer -> changeIdentityServer(action) + is DiscoverySettingsAction.UpdateUserConsent -> handleUpdateUserConsent(action) + is DiscoverySettingsAction.RevokeThreePid -> revokeThreePid(action) + is DiscoverySettingsAction.ShareThreePid -> shareThreePid(action) + is DiscoverySettingsAction.FinalizeBind3pid -> finalizeBind3pid(action, true) + is DiscoverySettingsAction.SubmitMsisdnToken -> submitMsisdnToken(action) + is DiscoverySettingsAction.CancelBinding -> cancelBinding(action) }.exhaustive } @@ -135,6 +142,10 @@ class DiscoverySettingsViewModel @AssistedInject constructor( } } + private fun updatePolicyUrlsExpandedState(isExpanded: Boolean) { + setState { copy(isIdentityPolicyUrlsExpanded = isExpanded) } + } + private fun changeIdentityServer(action: DiscoverySettingsAction.ChangeIdentityServer) { setState { copy(identityServer = Loading()) } @@ -143,7 +154,7 @@ class DiscoverySettingsViewModel @AssistedInject constructor( val data = session.identityService().setNewIdentityServer(action.url) setState { copy( - identityServer = Success(data), + identityServer = Success(IdentityServerWithTerms(data, emptyList())), userConsent = false ) } @@ -287,7 +298,7 @@ class DiscoverySettingsViewModel @AssistedInject constructor( } private fun retrieveBinding(threePids: List) = withState { state -> - if (state.identityServer().isNullOrBlank()) return@withState + if (state.identityServer()?.serverUrl.isNullOrBlank()) return@withState val emails = threePids.filterIsInstance() val msisdns = threePids.filterIsInstance() @@ -335,7 +346,7 @@ class DiscoverySettingsViewModel @AssistedInject constructor( } private fun submitMsisdnToken(action: DiscoverySettingsAction.SubmitMsisdnToken) = withState { state -> - if (state.identityServer().isNullOrBlank()) return@withState + if (state.identityServer()?.serverUrl.isNullOrBlank()) return@withState changeThreePidSubmitState(action.threePid, Loading()) @@ -378,12 +389,37 @@ class DiscoverySettingsViewModel @AssistedInject constructor( } } - private fun refreshPendingEmailBindings() = withState { state -> + private fun fetchContent() = withState { state -> state.emailList()?.forEach { info -> when (info.isShared()) { SharedState.BINDING_IN_PROGRESS -> finalizeBind3pid(DiscoverySettingsAction.FinalizeBind3pid(info.threePid), false) else -> Unit } } + viewModelScope.launch { + runCatching { fetchIdentityServerWithTerms() }.fold( + onSuccess = { setState { copy(identityServer = Success(it)) } }, + onFailure = { _viewEvents.post(DiscoverySettingsViewEvents.Failure(it)) } + ) + } + } + + private suspend fun fetchIdentityServerWithTerms(): IdentityServerWithTerms? { + val identityServerUrl = identityService.getCurrentIdentityServerUrl() + return identityServerUrl?.let { + val terms = termsService.getTerms(TermsService.ServiceType.IdentityService, identityServerUrl.ensureProtocol()) + .serverResponse + .getLocalizedTerms(stringProvider.getString(R.string.resources_language)) + val policyUrls = terms.mapNotNull { + val name = it.localizedName ?: it.policyName + val url = it.localizedUrl + if (name == null || url == null) { + null + } else { + IdentityServerPolicy(name = name, url = url) + } + } + IdentityServerWithTerms(identityServerUrl, policyUrls) + } } } diff --git a/vector/src/main/java/im/vector/app/features/discovery/change/SetIdentityServerFragment.kt b/vector/src/main/java/im/vector/app/features/discovery/change/SetIdentityServerFragment.kt index 36e77fb6a0..15e4e65d3b 100644 --- a/vector/src/main/java/im/vector/app/features/discovery/change/SetIdentityServerFragment.kt +++ b/vector/src/main/java/im/vector/app/features/discovery/change/SetIdentityServerFragment.kt @@ -37,12 +37,10 @@ import im.vector.app.core.resources.ColorProvider import im.vector.app.core.utils.colorizeMatchingText import im.vector.app.databinding.FragmentSetIdentityServerBinding import im.vector.app.features.discovery.DiscoverySharedViewModel - import org.matrix.android.sdk.api.session.terms.TermsService import javax.inject.Inject class SetIdentityServerFragment @Inject constructor( - val viewModelFactory: SetIdentityServerViewModel.Factory, val colorProvider: ColorProvider ) : VectorBaseFragment() { diff --git a/vector/src/main/java/im/vector/app/features/discovery/change/SetIdentityServerState.kt b/vector/src/main/java/im/vector/app/features/discovery/change/SetIdentityServerState.kt index 4c2f437e07..558cc4dcf7 100644 --- a/vector/src/main/java/im/vector/app/features/discovery/change/SetIdentityServerState.kt +++ b/vector/src/main/java/im/vector/app/features/discovery/change/SetIdentityServerState.kt @@ -16,10 +16,10 @@ package im.vector.app.features.discovery.change -import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MavericksState data class SetIdentityServerState( val homeServerUrl: String = "", // Will contain the default identity server url if any val defaultIdentityServerUrl: String? = null -) : MvRxState +) : MavericksState diff --git a/vector/src/main/java/im/vector/app/features/discovery/change/SetIdentityServerViewModel.kt b/vector/src/main/java/im/vector/app/features/discovery/change/SetIdentityServerViewModel.kt index 08632a2bd1..c258652b49 100644 --- a/vector/src/main/java/im/vector/app/features/discovery/change/SetIdentityServerViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/discovery/change/SetIdentityServerViewModel.kt @@ -15,15 +15,16 @@ */ package im.vector.app.features.discovery.change -import androidx.lifecycle.viewModelScope -import com.airbnb.mvrx.FragmentViewModelContext -import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.ViewModelContext import dagger.assisted.Assisted -import dagger.assisted.AssistedInject import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import dagger.hilt.EntryPoints import im.vector.app.R -import im.vector.app.core.di.HasScreenInjector +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.SingletonEntryPoint +import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider @@ -38,30 +39,23 @@ import java.net.UnknownHostException class SetIdentityServerViewModel @AssistedInject constructor( @Assisted initialState: SetIdentityServerState, private val mxSession: Session, - stringProvider: StringProvider) - : VectorViewModel(initialState) { + stringProvider: StringProvider) : + VectorViewModel(initialState) { @AssistedFactory - interface Factory { - fun create(initialState: SetIdentityServerState): SetIdentityServerViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: SetIdentityServerState): SetIdentityServerViewModel } - companion object : MvRxViewModelFactory { - - override fun initialState(viewModelContext: ViewModelContext): SetIdentityServerState? { - val session = (viewModelContext.activity as HasScreenInjector).injector().activeSessionHolder().getActiveSession() + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() { + override fun initialState(viewModelContext: ViewModelContext): SetIdentityServerState { + val session = EntryPoints.get(viewModelContext.app(), SingletonEntryPoint::class.java).activeSessionHolder().getActiveSession() return SetIdentityServerState( homeServerUrl = session.sessionParams.homeServerUrl, defaultIdentityServerUrl = session.identityService().getDefaultIdentityServer() ) } - - @JvmStatic - override fun create(viewModelContext: ViewModelContext, state: SetIdentityServerState): SetIdentityServerViewModel? { - val fragment: SetIdentityServerFragment = (viewModelContext as FragmentViewModelContext).fragment() - return fragment.viewModelFactory.create(state) - } } var currentWantedUrl: String? = null diff --git a/vector/src/main/java/im/vector/app/features/displayname/Extension.kt b/vector/src/main/java/im/vector/app/features/displayname/Extension.kt new file mode 100644 index 0000000000..6ca1d48464 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/displayname/Extension.kt @@ -0,0 +1,31 @@ +/* + * 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.displayname + +import org.matrix.android.sdk.api.util.MatrixItem + +fun MatrixItem.getBestName(): String { + // Note: this code is copied from [DisplayNameResolver] in the SDK + return if (this is MatrixItem.GroupItem || this is MatrixItem.RoomAliasItem) { + // Best name is the id, and we keep the displayName of the room for the case we need the first letter + id + } else { + displayName + ?.takeIf { it.isNotBlank() } + ?: VectorMatrixItemDisplayNameFallbackProvider.getDefaultName(this) + } +} diff --git a/vector/src/main/java/im/vector/app/features/displayname/VectorMatrixItemDisplayNameFallbackProvider.kt b/vector/src/main/java/im/vector/app/features/displayname/VectorMatrixItemDisplayNameFallbackProvider.kt new file mode 100644 index 0000000000..23b55335b8 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/displayname/VectorMatrixItemDisplayNameFallbackProvider.kt @@ -0,0 +1,28 @@ +/* + * 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.displayname + +import org.matrix.android.sdk.api.MatrixItemDisplayNameFallbackProvider +import org.matrix.android.sdk.api.util.MatrixItem + +// Used to provide the fallback to the MatrixSDK, in the MatrixConfiguration +object VectorMatrixItemDisplayNameFallbackProvider : MatrixItemDisplayNameFallbackProvider { + override fun getDefaultName(matrixItem: MatrixItem): String { + // Can customize something from the id if necessary here + return matrixItem.id + } +} diff --git a/vector/src/main/java/im/vector/app/features/form/FormEditTextItem.kt b/vector/src/main/java/im/vector/app/features/form/FormEditTextItem.kt index 2b2fddd0c9..7b7896316b 100644 --- a/vector/src/main/java/im/vector/app/features/form/FormEditTextItem.kt +++ b/vector/src/main/java/im/vector/app/features/form/FormEditTextItem.kt @@ -17,6 +17,8 @@ package im.vector.app.features.form import android.text.Editable +import android.text.InputFilter +import android.text.InputType import android.view.View import android.view.inputmethod.EditorInfo import android.widget.TextView @@ -77,6 +79,9 @@ abstract class FormEditTextItem : VectorEpoxyModel() { @EpoxyAttribute var suffixText: String? = null + @EpoxyAttribute + var maxLength: Int? = null + private val onTextChangeListener = object : SimpleTextWatcher() { override fun afterTextChanged(s: Editable) { onTextChange?.invoke(s.toString()) @@ -102,13 +107,53 @@ abstract class FormEditTextItem : VectorEpoxyModel() { } holder.textInputEditText.isEnabled = enabled - inputType?.let { holder.textInputEditText.inputType = it } - holder.textInputEditText.isSingleLine = singleLine - holder.textInputEditText.imeOptions = imeOptions ?: EditorInfo.IME_ACTION_NONE + + configureInputType(holder) + configureImeOptions(holder) holder.textInputEditText.addTextChangedListenerOnce(onTextChangeListener) holder.textInputEditText.setOnEditorActionListener(editorActionListener) holder.textInputEditText.onFocusChangeListener = onFocusChangedListener + + if (maxLength != null) { + holder.textInputEditText.filters = arrayOf(InputFilter.LengthFilter(maxLength!!)) + holder.textInputLayout.isCounterEnabled = true + holder.textInputLayout.counterMaxLength = maxLength!! + } else { + holder.textInputEditText.filters = arrayOf() + holder.textInputLayout.isCounterEnabled = false + } + } + + /** + * Configure the inputType of the EditText, input type should be always defined + * especially when we want to use a single line, we set the InputType to InputType.TYPE_CLASS_TEXT + * while the default for the EditText is InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_MULTI_LINE + */ + private fun configureInputType(holder: Holder) { + val newInputType = + inputType ?: when (singleLine) { + true -> InputType.TYPE_CLASS_TEXT + false -> InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_MULTI_LINE + } + + // This is a must in order to avoid extreme lag in some devices, on fast typing + if (holder.textInputEditText.inputType != newInputType) { + holder.textInputEditText.inputType = newInputType + } + } + + /** + * Configure the imeOptions of the EditText, when imeOptions are not defined by the developer + * EditorInfo.IME_ACTION_NEXT will be used for singleLine EditTexts to disable "new line" + * while EditorInfo.IME_ACTION_NONE will be used for all the other cases + */ + private fun configureImeOptions(holder: Holder) { + holder.textInputEditText.imeOptions = + imeOptions ?: when (singleLine) { + true -> EditorInfo.IME_ACTION_NEXT + false -> EditorInfo.IME_ACTION_NONE + } } override fun shouldSaveViewState(): Boolean { diff --git a/vector/src/main/java/im/vector/app/features/grouplist/HomeSpaceSummaryItem.kt b/vector/src/main/java/im/vector/app/features/grouplist/HomeSpaceSummaryItem.kt index bbced2e8e1..bd414c0c3f 100644 --- a/vector/src/main/java/im/vector/app/features/grouplist/HomeSpaceSummaryItem.kt +++ b/vector/src/main/java/im/vector/app/features/grouplist/HomeSpaceSummaryItem.kt @@ -39,7 +39,7 @@ abstract class HomeSpaceSummaryItem : VectorEpoxyModel(), ToolbarConfigurable, - UnknownDeviceDetectorSharedViewModel.Factory, - ServerBackupStatusViewModel.Factory, - UnreadMessagesSharedViewModel.Factory, - PromoteRestrictedViewModel.Factory, NavigationInterceptor, - SpaceInviteBottomSheet.InteractionListener { + SpaceInviteBottomSheet.InteractionListener, + MatrixToBottomSheet.InteractionListener { private lateinit var sharedActionViewModel: HomeSharedActionViewModel private val homeActivityViewModel: HomeActivityViewModel by viewModel() - @Inject lateinit var viewModelFactory: HomeActivityViewModel.Factory 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 @@ -111,8 +112,6 @@ class HomeActivity : @Inject lateinit var vectorPreferences: VectorPreferences @Inject lateinit var popupAlertManager: PopupAlertManager @Inject lateinit var shortcutsHandler: ShortcutsHandler - @Inject lateinit var unknownDeviceViewModelFactory: UnknownDeviceDetectorSharedViewModel.Factory - @Inject lateinit var unreadMessagesSharedViewModelFactory: UnreadMessagesSharedViewModel.Factory @Inject lateinit var permalinkHandler: PermalinkHandler @Inject lateinit var avatarRenderer: AvatarRenderer @Inject lateinit var initSyncStepFormatter: InitSyncStepFormatter @@ -141,6 +140,22 @@ class HomeActivity : } } + private val fragmentLifecycleCallbacks = object : FragmentManager.FragmentLifecycleCallbacks() { + override fun onFragmentResumed(fm: FragmentManager, f: Fragment) { + if (f is MatrixToBottomSheet) { + f.interactionListener = this@HomeActivity + } + super.onFragmentResumed(fm, f) + } + + override fun onFragmentPaused(fm: FragmentManager, f: Fragment) { + if (f is MatrixToBottomSheet) { + f.interactionListener = null + } + super.onFragmentPaused(fm, f) + } + } + private val drawerListener = object : DrawerLayout.SimpleDrawerListener() { override fun onDrawerStateChanged(newState: Int) { hideKeyboard() @@ -151,24 +166,9 @@ class HomeActivity : override fun getBinding() = ActivityHomeBinding.inflate(layoutInflater) - override fun injectWith(injector: ScreenComponent) { - injector.inject(this) - } - - override fun create(initialState: UnknownDevicesState): UnknownDeviceDetectorSharedViewModel { - return unknownDeviceViewModelFactory.create(initialState) - } - - override fun create(initialState: ServerBackupStatusViewState): ServerBackupStatusViewModel { - return serverBackupviewModelFactory.create(initialState) - } - - override fun create(initialState: UnreadMessagesState): UnreadMessagesSharedViewModel { - return unreadMessagesSharedViewModelFactory.create(initialState) - } - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, false) FcmHelper.ensureFcmTokenIsRetrieved(this, pushManager, vectorPreferences.areNotificationEnabledForDevice()) sharedActionViewModel = viewModelProvider.get(HomeSharedActionViewModel::class.java) views.drawerLayout.addDrawerListener(drawerListener) @@ -224,11 +224,16 @@ class HomeActivity : } .disposeOnDestroy() - val args = intent.getParcelableExtra(MvRx.KEY_ARG) + val args = intent.getParcelableExtra(Mavericks.KEY_ARG) if (args?.clearNotification == true) { notificationDrawerManager.clearAllEvents() } + if (args?.inviteNotificationRoomId != null) { + activeSessionHolder.getSafeActiveSession()?.permalinkService()?.createPermalink(args.inviteNotificationRoomId)?.let { + navigator.openMatrixToBottomSheet(this, it) + } + } homeActivityViewModel.observeViewEvents { when (it) { @@ -245,8 +250,8 @@ class HomeActivity : if (!vectorPreferences.didPromoteNewRestrictedFeature()) { promoteRestrictedViewModel.subscribe(this) { - if (it.activeSpaceSummary != null && !it.activeSpaceSummary.isPublic - && it.activeSpaceSummary.otherMemberIds.isNotEmpty()) { + 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()) { @@ -266,47 +271,42 @@ class HomeActivity : private fun handleIntent(intent: Intent?) { intent?.dataString?.let { deepLink -> val resolvedLink = when { - deepLink.startsWith(PermalinkService.MATRIX_TO_URL_BASE) -> deepLink - deepLink.startsWith(MATRIX_TO_CUSTOM_SCHEME_URL_BASE) -> { - // This is a bit ugly, but for now just convert to matrix.to link for compatibility + // Element custom scheme is not handled by the sdk, convert it to matrix.to link for compatibility + deepLink.startsWith(MATRIX_TO_CUSTOM_SCHEME_URL_BASE) -> { when { deepLink.startsWith(USER_LINK_PREFIX) -> deepLink.substring(USER_LINK_PREFIX.length) deepLink.startsWith(ROOM_LINK_PREFIX) -> deepLink.substring(ROOM_LINK_PREFIX.length) else -> null - }?.let { - activeSessionHolder.getSafeActiveSession()?.permalinkService()?.createPermalink(it) + }?.let { permalinkId -> + activeSessionHolder.getSafeActiveSession()?.permalinkService()?.createPermalink(permalinkId) } } - else -> return@let + else -> deepLink } - permalinkHandler.launch( - context = this, - deepLink = resolvedLink, - navigationInterceptor = this, - buildTask = true - ) - // .delay(500, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { isHandled -> - if (!isHandled) { - MaterialAlertDialogBuilder(this) - .setTitle(R.string.dialog_title_error) - .setMessage(R.string.permalink_malformed) - .setPositiveButton(R.string.ok, null) - .show() - } - } - .disposeOnDestroy() + lifecycleScope.launch { + val isHandled = permalinkHandler.launch( + context = this@HomeActivity, + deepLink = resolvedLink, + navigationInterceptor = this@HomeActivity, + buildTask = true + ) + if (!isHandled) { + val isMatrixToLink = deepLink.startsWith(PermalinkService.MATRIX_TO_URL_BASE) || + deepLink.startsWith(MATRIX_TO_CUSTOM_SCHEME_URL_BASE) + MaterialAlertDialogBuilder(this@HomeActivity) + .setTitle(R.string.dialog_title_error) + .setMessage(if (isMatrixToLink) R.string.permalink_malformed else R.string.universal_link_malformed) + .setPositiveButton(R.string.ok, null) + .show() + } + } } } private fun renderState(state: HomeActivityViewState) { - when (val status = state.initialSyncProgressServiceStatus) { - is InitialSyncProgressService.Status.Idle -> { - views.waitingView.root.isVisible = false - } - is InitialSyncProgressService.Status.Progressing -> { + when (val status = state.syncStatusServiceStatus) { + is SyncStatusService.Status.Progressing -> { val initSyncStepStr = initSyncStepFormatter.format(status.initSyncStep) Timber.v("$initSyncStepStr ${status.percentProgress}") views.waitingView.root.setOnClickListener { @@ -324,6 +324,10 @@ class HomeActivity : } views.waitingView.root.isVisible = true } + else -> { + // Idle or Incremental sync status + views.waitingView.root.isVisible = false + } }.exhaustive } @@ -422,14 +426,23 @@ class HomeActivity : override fun onNewIntent(intent: Intent?) { super.onNewIntent(intent) - if (intent?.getParcelableExtra(MvRx.KEY_ARG)?.clearNotification == true) { + val parcelableExtra = intent?.getParcelableExtra(Mavericks.KEY_ARG) + if (parcelableExtra?.clearNotification == true) { notificationDrawerManager.clearAllEvents() } + if (parcelableExtra?.inviteNotificationRoomId != null) { + activeSessionHolder.getSafeActiveSession() + ?.permalinkService() + ?.createPermalink(parcelableExtra.inviteNotificationRoomId)?.let { + navigator.openMatrixToBottomSheet(this, it) + } + } handleIntent(intent) } override fun onDestroy() { views.drawerLayout.removeDrawerListener(drawerListener) + supportFragmentManager.unregisterFragmentLifecycleCallbacks(fragmentLifecycleCallbacks) super.onDestroy() } @@ -460,8 +473,8 @@ class HomeActivity : override fun getMenuRes() = R.menu.home override fun onPrepareOptionsMenu(menu: Menu): Boolean { - menu.findItem(R.id.menu_home_init_sync_legacy)?.isVisible = vectorPreferences.developerMode() - menu.findItem(R.id.menu_home_init_sync_optimized)?.isVisible = vectorPreferences.developerMode() + menu.findItem(R.id.menu_home_init_sync_legacy).isVisible = vectorPreferences.developerMode() + menu.findItem(R.id.menu_home_init_sync_optimized).isVisible = vectorPreferences.developerMode() return super.onPrepareOptionsMenu(menu) } @@ -511,30 +524,15 @@ class HomeActivity : } override fun navToMemberProfile(userId: String, deepLink: Uri): Boolean { - val listener = object : MatrixToBottomSheet.InteractionListener { - override fun navigateToRoom(roomId: String) { - navigator.openRoom(this@HomeActivity, roomId) - } - } // TODO check if there is already one?? - MatrixToBottomSheet.withLink(deepLink.toString(), listener) + MatrixToBottomSheet.withLink(deepLink.toString()) .show(supportFragmentManager, "HA#MatrixToBottomSheet") return true } override fun navToRoom(roomId: String?, eventId: String?, deepLink: Uri?): Boolean { if (roomId == null) return false - val listener = object : MatrixToBottomSheet.InteractionListener { - override fun navigateToRoom(roomId: String) { - navigator.openRoom(this@HomeActivity, roomId) - } - - override fun switchToSpace(spaceId: String) { - navigator.switchToSpace(this@HomeActivity, spaceId, Navigator.PostSwitchSpaceAction.None) - } - } - - MatrixToBottomSheet.withLink(deepLink.toString(), listener) + MatrixToBottomSheet.withLink(deepLink.toString()) .show(supportFragmentManager, "HA#MatrixToBottomSheet") return true } @@ -548,22 +546,29 @@ class HomeActivity : } companion object { - fun newIntent(context: Context, clearNotification: Boolean = false, accountCreation: Boolean = false): Intent { + fun newIntent(context: Context, + clearNotification: Boolean = false, + accountCreation: Boolean = false, + inviteNotificationRoomId: String? = null + ): Intent { val args = HomeActivityArgs( clearNotification = clearNotification, - accountCreation = accountCreation + accountCreation = accountCreation, + inviteNotificationRoomId = inviteNotificationRoomId ) return Intent(context, HomeActivity::class.java) .apply { - putExtra(MvRx.KEY_ARG, args) + putExtra(Mavericks.KEY_ARG, args) } } - - private const val MATRIX_TO_CUSTOM_SCHEME_URL_BASE = "element://" - private const val ROOM_LINK_PREFIX = "${MATRIX_TO_CUSTOM_SCHEME_URL_BASE}room/" - private const val USER_LINK_PREFIX = "${MATRIX_TO_CUSTOM_SCHEME_URL_BASE}user/" } - override fun create(initialState: ActiveSpaceViewState) = promoteRestrictedViewModelFactory.create(initialState) + override fun mxToBottomSheetNavigateToRoom(roomId: String) { + navigator.openRoom(this, roomId) + } + + override fun mxToBottomSheetSwitchToSpace(spaceId: String) { + navigator.switchToSpace(this, spaceId, Navigator.PostSwitchSpaceAction.None) + } } diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt index bfedbd6f52..59b9cafd6e 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt @@ -16,14 +16,14 @@ package im.vector.app.features.home -import androidx.lifecycle.viewModelScope -import com.airbnb.mvrx.MvRx -import com.airbnb.mvrx.MvRxViewModelFactory -import com.airbnb.mvrx.ViewModelContext +import androidx.lifecycle.asFlow +import com.airbnb.mvrx.MavericksViewModelFactory import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import im.vector.app.core.di.ActiveSessionHolder +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import im.vector.app.features.login.ReAuthHelper @@ -31,6 +31,8 @@ import im.vector.app.features.session.coroutineScope import im.vector.app.features.settings.VectorPreferences import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import org.matrix.android.sdk.api.auth.UIABaseAuth import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor @@ -40,15 +42,14 @@ import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse import org.matrix.android.sdk.api.auth.registration.nextUncompletedStage import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.pushrules.RuleIds -import org.matrix.android.sdk.api.session.initsync.InitialSyncProgressService +import org.matrix.android.sdk.api.session.initsync.SyncStatusService import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams import org.matrix.android.sdk.api.util.toMatrixItem +import org.matrix.android.sdk.flow.flow import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap import org.matrix.android.sdk.internal.util.awaitCallback -import org.matrix.android.sdk.rx.asObservable -import org.matrix.android.sdk.rx.rx import timber.log.Timber import kotlin.coroutines.Continuation import kotlin.coroutines.resume @@ -56,26 +57,17 @@ import kotlin.coroutines.resumeWithException class HomeActivityViewModel @AssistedInject constructor( @Assisted initialState: HomeActivityViewState, - @Assisted private val args: HomeActivityArgs, private val activeSessionHolder: ActiveSessionHolder, private val reAuthHelper: ReAuthHelper, private val vectorPreferences: VectorPreferences ) : VectorViewModel(initialState) { @AssistedFactory - interface Factory { - fun create(initialState: HomeActivityViewState, args: HomeActivityArgs): HomeActivityViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: HomeActivityViewState): HomeActivityViewModel } - companion object : MvRxViewModelFactory { - - @JvmStatic - override fun create(viewModelContext: ViewModelContext, state: HomeActivityViewState): HomeActivityViewModel? { - val activity: HomeActivity = viewModelContext.activity() - val args: HomeActivityArgs? = activity.intent.getParcelableExtra(MvRx.KEY_ARG) - return activity.viewModelFactory.create(state, args ?: HomeActivityArgs(clearNotification = false, accountCreation = false)) - } - } + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() private var checkBootstrap = false private var onceTrusted = false @@ -100,9 +92,9 @@ class HomeActivityViewModel @AssistedInject constructor( .crossSigningService().allPrivateKeysKnown() safeActiveSession - .rx() + .flow() .liveCrossSigningInfo(safeActiveSession.myUserId) - .subscribe { + .onEach { val isVerified = it.getOrNull()?.isTrusted() ?: false if (!isVerified && onceTrusted) { // cross signing keys have been reset @@ -116,35 +108,36 @@ class HomeActivityViewModel @AssistedInject constructor( } onceTrusted = isVerified } - .disposeOnClear() + .launchIn(viewModelScope) } private fun observeInitialSync() { val session = activeSessionHolder.getSafeActiveSession() ?: return - session.getInitialSyncProgressStatus() - .asObservable() - .subscribe { status -> + session.getSyncStatusLive() + .asFlow() + .onEach { status -> when (status) { - is InitialSyncProgressService.Status.Progressing -> { + is SyncStatusService.Status.Progressing -> { // Schedule a check of the bootstrap when the init sync will be finished checkBootstrap = true } - is InitialSyncProgressService.Status.Idle -> { + is SyncStatusService.Status.Idle -> { if (checkBootstrap) { checkBootstrap = false maybeBootstrapCrossSigningAfterInitialSync() } } + else -> Unit } setState { copy( - initialSyncProgressServiceStatus = status + syncStatusServiceStatus = status ) } } - .disposeOnClear() + .launchIn(viewModelScope) } /** @@ -217,9 +210,9 @@ class HomeActivityViewModel @AssistedInject constructor( object : UserInteractiveAuthInterceptor { override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { // We missed server grace period or it's not setup, see if we remember locally password - if (flowResponse.nextUncompletedStage() == LoginFlowTypes.PASSWORD - && errCode == null - && reAuthHelper.data != null) { + if (flowResponse.nextUncompletedStage() == LoginFlowTypes.PASSWORD && + errCode == null && + reAuthHelper.data != null) { promise.resume( UserPasswordAuth( session = flowResponse.session, diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewState.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewState.kt index d4df7cd073..68131e8569 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewState.kt @@ -16,9 +16,9 @@ package im.vector.app.features.home -import com.airbnb.mvrx.MvRxState -import org.matrix.android.sdk.api.session.initsync.InitialSyncProgressService +import com.airbnb.mvrx.MavericksState +import org.matrix.android.sdk.api.session.initsync.SyncStatusService data class HomeActivityViewState( - val initialSyncProgressServiceStatus: InitialSyncProgressService.Status = InitialSyncProgressService.Status.Idle -) : MvRxState + val syncStatusServiceStatus: SyncStatusService.Status = SyncStatusService.Status.Idle +) : MavericksState diff --git a/vector/src/main/java/im/vector/app/features/home/HomeDetailAction.kt b/vector/src/main/java/im/vector/app/features/home/HomeDetailAction.kt index b466f204ec..26f0e56f23 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeDetailAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeDetailAction.kt @@ -21,5 +21,5 @@ import im.vector.app.core.platform.VectorViewModelAction sealed class HomeDetailAction : VectorViewModelAction { data class SwitchTab(val tab: HomeTab) : HomeDetailAction() object MarkAllRoomsRead : HomeDetailAction() - data class StartCallWithPhoneNumber(val phoneNumber: String): HomeDetailAction() + data class StartCallWithPhoneNumber(val phoneNumber: String) : HomeDetailAction() } 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 95430746a4..80351a437e 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 @@ -57,15 +57,12 @@ import im.vector.app.features.settings.VectorSettingsActivity.Companion.EXTRA_DI import im.vector.app.features.themes.ThemeUtils import im.vector.app.features.workers.signout.BannerState import im.vector.app.features.workers.signout.ServerBackupStatusViewModel -import im.vector.app.features.workers.signout.ServerBackupStatusViewState import org.matrix.android.sdk.api.session.group.model.GroupSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo import javax.inject.Inject class HomeDetailFragment @Inject constructor( - val homeDetailViewModelFactory: HomeDetailViewModel.Factory, - private val serverBackupStatusViewModelFactory: ServerBackupStatusViewModel.Factory, private val avatarRenderer: AvatarRenderer, private val colorProvider: ColorProvider, private val alertManager: PopupAlertManager, @@ -74,8 +71,7 @@ class HomeDetailFragment @Inject constructor( private val appStateHandler: AppStateHandler ) : VectorBaseFragment(), KeysBackupBanner.Delegate, - CurrentCallsView.Callback, - ServerBackupStatusViewModel.Factory { + CurrentCallsView.Callback { private val viewModel: HomeDetailViewModel by fragmentViewModel() private val unknownDeviceDetectorSharedViewModel: UnknownDeviceDetectorSharedViewModel by activityViewModel() @@ -108,9 +104,9 @@ class HomeDetailFragment @Inject constructor( override fun onPrepareOptionsMenu(menu: Menu) { withState(viewModel) { state -> - menu.iterator().forEach { it.isVisible = state.currentTab is HomeTab.RoomList } + val isRoomList = state.currentTab is HomeTab.RoomList + menu.findItem(R.id.menu_home_mark_all_as_read).isVisible = isRoomList && hasUnreadRooms } - menu.findItem(R.id.menu_home_mark_all_as_read).isVisible = hasUnreadRooms super.onPrepareOptionsMenu(menu) } @@ -134,7 +130,7 @@ class HomeDetailFragment @Inject constructor( views.bottomNavigationView.selectedItemId = it.currentTab.toMenuId() } - viewModel.selectSubscribe(this, HomeDetailViewState::roomGroupingMethod) { roomGroupingMethod -> + viewModel.onEach(HomeDetailViewState::roomGroupingMethod) { roomGroupingMethod -> when (roomGroupingMethod) { is RoomGroupingMethod.ByLegacyGroup -> { onGroupChange(roomGroupingMethod.groupSummary) @@ -145,23 +141,23 @@ class HomeDetailFragment @Inject constructor( } } - viewModel.selectSubscribe(this, HomeDetailViewState::currentTab) { currentTab -> + viewModel.onEach(HomeDetailViewState::currentTab) { currentTab -> updateUIForTab(currentTab) } - viewModel.selectSubscribe(this, HomeDetailViewState::showDialPadTab) { showDialPadTab -> + viewModel.onEach(HomeDetailViewState::showDialPadTab) { showDialPadTab -> updateTabVisibilitySafely(R.id.bottom_action_dial_pad, showDialPadTab) } viewModel.observeViewEvents { viewEvent -> when (viewEvent) { - HomeDetailViewEvents.CallStarted -> dismissLoadingDialog() + HomeDetailViewEvents.CallStarted -> handleCallStarted() is HomeDetailViewEvents.FailToCall -> showFailure(viewEvent.failure) HomeDetailViewEvents.Loading -> showLoadingDialog() } } - unknownDeviceDetectorSharedViewModel.subscribe { state -> + unknownDeviceDetectorSharedViewModel.onEach { state -> state.unknownSessions.invoke()?.let { unknownDevices -> // Timber.v("## Detector Triggerred in fragment - ${unknownDevices.firstOrNull()}") if (unknownDevices.firstOrNull()?.currentSessionTrust == true) { @@ -179,7 +175,7 @@ class HomeDetailFragment @Inject constructor( } } - unreadMessagesSharedViewModel.subscribe { state -> + unreadMessagesSharedViewModel.onEach { state -> views.drawerUnreadCounterBadgeView.render( UnreadCounterBadgeView.State( count = state.otherSpacesUnread.totalCount, @@ -190,10 +186,16 @@ class HomeDetailFragment @Inject constructor( sharedCallActionViewModel .liveKnownCalls - .observe(viewLifecycleOwner, { + .observe(viewLifecycleOwner) { currentCallsViewPresenter.updateCall(callManager.getCurrentCall(), callManager.getCalls()) invalidateOptionsMenu() - }) + } + } + + private fun handleCallStarted() { + dismissLoadingDialog() + val fragmentTag = HomeTab.DialPad.toFragmentTag() + (childFragmentManager.findFragmentByTag(fragmentTag) as? DialPadFragment)?.clear() } override fun onDestroyView() { @@ -370,8 +372,10 @@ class HomeDetailFragment @Inject constructor( invalidateOptionsMenu() } + private fun HomeTab.toFragmentTag() = "FRAGMENT_TAG_$this" + private fun updateSelectedFragment(tab: HomeTab) { - val fragmentTag = "FRAGMENT_TAG_$tab" + val fragmentTag = tab.toFragmentTag() val fragmentToShow = childFragmentManager.findFragmentByTag(fragmentTag) childFragmentManager.commitTransaction { childFragmentManager.fragments @@ -440,7 +444,11 @@ class HomeDetailFragment @Inject constructor( views.bottomNavigationView.getOrCreateBadge(R.id.bottom_action_people).render(it.notificationCountPeople, it.notificationHighlightPeople) views.bottomNavigationView.getOrCreateBadge(R.id.bottom_action_rooms).render(it.notificationCountRooms, it.notificationHighlightRooms) views.bottomNavigationView.getOrCreateBadge(R.id.bottom_action_notification).render(it.notificationCountCatchup, it.notificationHighlightCatchup) - views.syncStateView.render(it.syncState) + views.syncStateView.render( + it.syncState, + it.incrementalSyncStatus, + it.pushCounter, + vectorPreferences.developerShowDebugInfo()) hasUnreadRooms = it.hasUnreadMessages } @@ -491,8 +499,4 @@ class HomeDetailFragment @Inject constructor( } return this } - - override fun create(initialState: ServerBackupStatusViewState): ServerBackupStatusViewModel { - return serverBackupStatusViewModelFactory.create(initialState) - } } diff --git a/vector/src/main/java/im/vector/app/features/home/HomeDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/HomeDetailViewModel.kt index b960402f90..73e50ad5f1 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeDetailViewModel.kt @@ -16,16 +16,17 @@ package im.vector.app.features.home -import androidx.lifecycle.viewModelScope -import com.airbnb.mvrx.FragmentViewModelContext -import com.airbnb.mvrx.MvRxViewModelFactory +import androidx.lifecycle.asFlow +import com.airbnb.mvrx.MavericksViewModelFactory 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.HasScreenInjector +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory +import im.vector.app.core.extensions.singletonEntryPoint import im.vector.app.core.platform.VectorViewModel import im.vector.app.features.call.dialpad.DialPadLookup import im.vector.app.features.call.lookup.CallProtocolsChecker @@ -33,19 +34,23 @@ import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.createdirect.DirectRoomHelper import im.vector.app.features.invite.AutoAcceptInvites import im.vector.app.features.invite.showInvites +import im.vector.app.features.settings.VectorDataStore import im.vector.app.features.ui.UiStateRepository import io.reactivex.schedulers.Schedulers import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.launch import org.matrix.android.sdk.api.query.ActiveSpaceFilter import org.matrix.android.sdk.api.query.RoomCategoryFilter import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.initsync.SyncStatusService import org.matrix.android.sdk.api.session.room.RoomSortOrder import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams import org.matrix.android.sdk.api.util.toMatrixItem +import org.matrix.android.sdk.flow.flow import org.matrix.android.sdk.rx.asObservable -import org.matrix.android.sdk.rx.rx import timber.log.Timber import java.util.concurrent.TimeUnit @@ -56,32 +61,27 @@ import java.util.concurrent.TimeUnit class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: HomeDetailViewState, private val session: Session, private val uiStateRepository: UiStateRepository, + private val vectorDataStore: VectorDataStore, private val callManager: WebRtcCallManager, private val directRoomHelper: DirectRoomHelper, private val appStateHandler: AppStateHandler, -private val autoAcceptInvites: AutoAcceptInvites) - : VectorViewModel(initialState), + private val autoAcceptInvites: AutoAcceptInvites) : + VectorViewModel(initialState), CallProtocolsChecker.Listener { @AssistedFactory - interface Factory { - fun create(initialState: HomeDetailViewState): HomeDetailViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: HomeDetailViewState): HomeDetailViewModel } - companion object : MvRxViewModelFactory { + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() { - override fun initialState(viewModelContext: ViewModelContext): HomeDetailViewState? { - val uiStateRepository = (viewModelContext.activity as HasScreenInjector).injector().uiStateRepository() + override fun initialState(viewModelContext: ViewModelContext): HomeDetailViewState { + val uiStateRepository = viewModelContext.activity.singletonEntryPoint().uiStateRepository() return HomeDetailViewState( currentTab = HomeTab.RoomList(uiStateRepository.getDisplayMode()) ) } - - @JvmStatic - override fun create(viewModelContext: ViewModelContext, state: HomeDetailViewState): HomeDetailViewModel? { - val fragment: HomeDetailFragment = (viewModelContext as FragmentViewModelContext).fragment() - return fragment.homeDetailViewModelFactory.create(state) - } } init { @@ -89,14 +89,27 @@ private val autoAcceptInvites: AutoAcceptInvites) observeRoomGroupingMethod() observeRoomSummaries() updateShowDialPadTab() + observeDataStore() callManager.addProtocolsCheckerListener(this) - session.rx().liveUser(session.myUserId).execute { + session.flow().liveUser(session.myUserId).execute { copy( myMatrixItem = it.invoke()?.getOrNull()?.toMatrixItem() ) } } + private fun observeDataStore() { + viewModelScope.launch { + vectorDataStore.pushCounterFlow.collect { nbOfPush -> + setState { + copy( + pushCounter = nbOfPush + ) + } + } + } + } + override fun handle(action: HomeDetailAction) { when (action) { is HomeDetailAction.SwitchTab -> handleSwitchTab(action) @@ -165,14 +178,18 @@ private val autoAcceptInvites: AutoAcceptInvites) } private fun observeSyncState() { - session.rx() + session.flow() .liveSyncState() - .subscribe { syncState -> - setState { - copy(syncState = syncState) - } + .setOnEach { syncState -> + copy(syncState = syncState) + } + + session.getSyncStatusLive() + .asFlow() + .filterIsInstance() + .setOnEach { + copy(incrementalSyncStatus = it) } - .disposeOnClear() } private fun observeRoomGroupingMethod() { diff --git a/vector/src/main/java/im/vector/app/features/home/HomeDetailViewState.kt b/vector/src/main/java/im/vector/app/features/home/HomeDetailViewState.kt index 304444abdd..7665dd41e5 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeDetailViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeDetailViewState.kt @@ -18,10 +18,11 @@ package im.vector.app.features.home import androidx.annotation.StringRes import com.airbnb.mvrx.Async -import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.Uninitialized import im.vector.app.R import im.vector.app.RoomGroupingMethod +import org.matrix.android.sdk.api.session.initsync.SyncStatusService import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.sync.SyncState import org.matrix.android.sdk.api.util.MatrixItem @@ -39,8 +40,10 @@ data class HomeDetailViewState( val notificationHighlightRooms: Boolean = false, val hasUnreadMessages: Boolean = false, val syncState: SyncState = SyncState.Idle, + val incrementalSyncStatus: SyncStatusService.Status.IncrementalSyncStatus = SyncStatusService.Status.IncrementalSyncIdle, + val pushCounter: Int = 0, val showDialPadTab: Boolean = false -) : MvRxState +) : MavericksState sealed class HomeTab(@StringRes val titleRes: Int) { data class RoomList(val displayMode: RoomListDisplayMode) : HomeTab(displayMode.titleRes) diff --git a/vector/src/main/java/im/vector/app/features/home/HomeDrawerFragment.kt b/vector/src/main/java/im/vector/app/features/home/HomeDrawerFragment.kt index 9ff865b9d1..3accc24740 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeDrawerFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeDrawerFragment.kt @@ -30,13 +30,11 @@ import im.vector.app.core.extensions.replaceChildFragment import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.utils.startSharePlainTextIntent import im.vector.app.databinding.FragmentHomeDrawerBinding -// import im.vector.app.features.grouplist.GroupListFragment import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorSettingsActivity import im.vector.app.features.spaces.SpaceListFragment import im.vector.app.features.usercode.UserCodeActivity import im.vector.app.features.workers.signout.SignOutUiWorker - import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.util.toMatrixItem import javax.inject.Inject diff --git a/vector/src/main/java/im/vector/app/features/home/HomeModule.kt b/vector/src/main/java/im/vector/app/features/home/HomeModule.kt index 5c34d0715d..0782bbb573 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeModule.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeModule.kt @@ -19,9 +19,11 @@ package im.vector.app.features.home import android.os.Handler import dagger.Module import dagger.Provides +import dagger.hilt.migration.DisableInstallInCheck import im.vector.app.features.home.room.detail.timeline.TimelineEventControllerHandler import im.vector.app.features.home.room.detail.timeline.helper.TimelineAsyncHelper +@DisableInstallInCheck @Module object HomeModule { diff --git a/vector/src/main/java/im/vector/app/features/home/LoadingFragment.kt b/vector/src/main/java/im/vector/app/features/home/LoadingFragment.kt index 2de36f2f5e..47cfd1c28f 100644 --- a/vector/src/main/java/im/vector/app/features/home/LoadingFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/LoadingFragment.kt @@ -23,7 +23,6 @@ import android.view.View import android.view.ViewGroup import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.FragmentLoadingBinding - import javax.inject.Inject class LoadingFragment @Inject constructor() : VectorBaseFragment() { 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 index ae7b495aa2..218574c03e 100644 --- a/vector/src/main/java/im/vector/app/features/home/PromoteRestrictedViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/PromoteRestrictedViewModel.kt @@ -16,17 +16,16 @@ 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 com.airbnb.mvrx.MavericksState +import com.airbnb.mvrx.MavericksViewModelFactory 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.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.platform.EmptyAction import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel @@ -41,7 +40,7 @@ data class ActiveSpaceViewState( val isInSpaceMode: Boolean = false, val activeSpaceSummary: RoomSummary? = null, val canUserManageSpace: Boolean = false -) : MvRxState +) : MavericksState class PromoteRestrictedViewModel @AssistedInject constructor( @Assisted initialState: ActiveSpaceViewState, @@ -72,21 +71,11 @@ class PromoteRestrictedViewModel @AssistedInject constructor( } @AssistedFactory - interface Factory { - fun create(initialState: ActiveSpaceViewState): PromoteRestrictedViewModel + interface Factory : MavericksAssistedViewModelFactory { + override 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") - } - } + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() 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 fc204a0c56..ee7edc021d 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 @@ -78,7 +78,6 @@ class ShortcutCreator @Inject constructor( .setLongLived(true) .setRank(rank) .setCategories(categories) - .build() } 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 c3249f5b26..612e2dcf87 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 @@ -22,19 +22,28 @@ import android.os.Build import androidx.core.content.getSystemService import androidx.core.content.pm.ShortcutManagerCompat import im.vector.app.core.di.ActiveSessionHolder +import im.vector.app.features.pin.PinCodeStore +import im.vector.app.features.pin.PinCodeStoreListener import io.reactivex.disposables.Disposable import io.reactivex.disposables.Disposables 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.model.RoomSummary import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams import org.matrix.android.sdk.rx.asObservable +import timber.log.Timber import javax.inject.Inject class ShortcutsHandler @Inject constructor( private val context: Context, private val shortcutCreator: ShortcutCreator, - private val activeSessionHolder: ActiveSessionHolder -) { + private val activeSessionHolder: ActiveSessionHolder, + private val pinCodeStore: PinCodeStore +) : PinCodeStoreListener { + private val isRequestPinShortcutSupported = ShortcutManagerCompat.isRequestPinShortcutSupported(context) + + // Value will be set correctly if necessary + private var hasPinCode = true fun observeRoomsAndBuildShortcuts(): Disposable { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) { @@ -42,31 +51,56 @@ class ShortcutsHandler @Inject constructor( return Disposables.empty() } - return activeSessionHolder.getSafeActiveSession() - ?.getPagedRoomSummariesLive( - roomSummaryQueryParams { - memberships = listOf(Membership.JOIN) - }, - sortOrder = RoomSortOrder.PRIORITY_AND_ACTIVITY - ) - ?.asObservable() - ?.subscribe { rooms -> + hasPinCode = pinCodeStore.getEncodedPin() != null + + val session = activeSessionHolder.getSafeActiveSession() ?: return Disposables.empty() + return session.getRoomSummariesLive( + roomSummaryQueryParams { + memberships = listOf(Membership.JOIN) + }, + sortOrder = RoomSortOrder.PRIORITY_AND_ACTIVITY + ) + .asObservable() + .doOnSubscribe { pinCodeStore.addListener(this) } + .doFinally { pinCodeStore.removeListener(this) } + .subscribe { rooms -> // 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) + removeDeadShortcut(rooms.map { it.roomId }) - val shortcuts = rooms.mapIndexed { index, room -> - shortcutCreator.create(room, index) - } - - shortcuts.forEach { shortcut -> - ShortcutManagerCompat.pushDynamicShortcut(context, shortcut) - } + // Create shortcuts + createShortcuts(rooms) } - ?: Disposables.empty() + } + + private fun removeDeadShortcut(roomIds: List) { + val deadShortcutIds = ShortcutManagerCompat.getShortcuts(context, ShortcutManagerCompat.FLAG_MATCH_DYNAMIC) + .map { it.id } + .filter { !roomIds.contains(it) } + + if (deadShortcutIds.isNotEmpty()) { + Timber.d("Removing shortcut(s) $deadShortcutIds") + ShortcutManagerCompat.removeLongLivedShortcuts(context, deadShortcutIds) + if (isRequestPinShortcutSupported) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { + context.getSystemService()?.disableShortcuts(deadShortcutIds) + } + } + } + } + + private fun createShortcuts(rooms: List) { + if (hasPinCode) { + // No shortcut in this case (privacy) + ShortcutManagerCompat.removeAllDynamicShortcuts(context) + } else { + val shortcuts = rooms.mapIndexed { index, room -> + shortcutCreator.create(room, index) + } + + shortcuts.forEach { shortcut -> + ShortcutManagerCompat.pushDynamicShortcut(context, shortcut) + } + } } fun clearShortcuts() { @@ -75,10 +109,14 @@ class ShortcutsHandler @Inject constructor( return } - ShortcutManagerCompat.removeAllDynamicShortcuts(context) + // according to Android documentation + // removeLongLivedShortcuts for API 29 and lower should behave like removeDynamicShortcuts(Context, List) + // getDynamicShortcuts: returns all dynamic shortcuts from the app. + val shortcuts = ShortcutManagerCompat.getDynamicShortcuts(context).map { it.id } + ShortcutManagerCompat.removeLongLivedShortcuts(context, shortcuts) // We can only disabled pinned shortcuts with the API, but at least it will prevent the crash - if (ShortcutManagerCompat.isRequestPinShortcutSupported(context)) { + if (isRequestPinShortcutSupported) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { context.getSystemService() ?.let { @@ -87,4 +125,14 @@ class ShortcutsHandler @Inject constructor( } } } + + override fun onPinSetUpChange(isConfigured: Boolean) { + hasPinCode = isConfigured + if (isConfigured) { + // Remove shortcuts immediately + ShortcutManagerCompat.removeAllDynamicShortcuts(context) + } + // Else shortcut will be created next time any room summary is updated, or + // next time the app is started which is acceptable + } } diff --git a/vector/src/main/java/im/vector/app/features/home/UnknownDeviceDetectorSharedViewModel.kt b/vector/src/main/java/im/vector/app/features/home/UnknownDeviceDetectorSharedViewModel.kt index 988b4fbabe..8a36a4c19e 100644 --- a/vector/src/main/java/im/vector/app/features/home/UnknownDeviceDetectorSharedViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/UnknownDeviceDetectorSharedViewModel.kt @@ -16,39 +16,38 @@ package im.vector.app.features.home -import com.airbnb.mvrx.ActivityViewModelContext import com.airbnb.mvrx.Async -import com.airbnb.mvrx.FragmentViewModelContext -import com.airbnb.mvrx.MvRxState -import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.MavericksState +import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized -import com.airbnb.mvrx.ViewModelContext import dagger.assisted.Assisted -import dagger.assisted.AssistedInject import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModelAction import im.vector.app.features.settings.VectorPreferences -import io.reactivex.Observable +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.sample import org.matrix.android.sdk.api.NoOpMatrixCallback 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.Optional import org.matrix.android.sdk.api.util.toMatrixItem -import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo +import org.matrix.android.sdk.flow.flow import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo -import org.matrix.android.sdk.internal.crypto.store.PrivateKeysInfo -import org.matrix.android.sdk.rx.rx import timber.log.Timber -import java.util.concurrent.TimeUnit data class UnknownDevicesState( val myMatrixItem: MatrixItem.UserItem? = null, val unknownSessions: Async> = Uninitialized -) : MvRxState +) : MavericksState data class DeviceDetectionInfo( val deviceInfo: DeviceInfo, @@ -58,29 +57,19 @@ data class DeviceDetectionInfo( class UnknownDeviceDetectorSharedViewModel @AssistedInject constructor(@Assisted initialState: UnknownDevicesState, session: Session, - private val vectorPreferences: VectorPreferences) - : VectorViewModel(initialState) { + private val vectorPreferences: VectorPreferences) : + VectorViewModel(initialState) { sealed class Action : VectorViewModelAction { data class IgnoreDevice(val deviceIds: List) : Action() } @AssistedFactory - interface Factory { - fun create(initialState: UnknownDevicesState): UnknownDeviceDetectorSharedViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: UnknownDevicesState): UnknownDeviceDetectorSharedViewModel } - companion object : MvRxViewModelFactory { - - @JvmStatic - override fun create(viewModelContext: ViewModelContext, state: UnknownDevicesState): UnknownDeviceDetectorSharedViewModel? { - 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") - } - } + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() private val ignoredDeviceList = ArrayList() @@ -98,31 +87,30 @@ class UnknownDeviceDetectorSharedViewModel @AssistedInject constructor(@Assisted } ) - Observable.combineLatest, List, Optional, List>( - session.rx().liveUserCryptoDevices(session.myUserId), - session.rx().liveMyDevicesInfo(), - session.rx().liveCrossSigningPrivateKeys(), - { cryptoList, infoList, pInfo -> - // Timber.v("## Detector trigger ${cryptoList.map { "${it.deviceId} ${it.trustLevel}" }}") + combine( + session.flow().liveUserCryptoDevices(session.myUserId), + session.flow().liveMyDevicesInfo(), + session.flow().liveCrossSigningPrivateKeys() + ) { cryptoList, infoList, pInfo -> + // Timber.v("## Detector trigger ${cryptoList.map { "${it.deviceId} ${it.trustLevel}" }}") // Timber.v("## Detector trigger canCrossSign ${pInfo.get().selfSigned != null}") - infoList - .filter { info -> - // filter verified session, by checking the crypto device info - cryptoList.firstOrNull { info.deviceId == it.deviceId }?.isVerified?.not().orFalse() - } - // filter out ignored devices - .filter { !ignoredDeviceList.contains(it.deviceId) } - .sortedByDescending { it.lastSeenTs } - .map { deviceInfo -> - val deviceKnownSince = cryptoList.firstOrNull { it.deviceId == deviceInfo.deviceId }?.firstTimeSeenLocalTs ?: 0 - DeviceDetectionInfo( - deviceInfo, - deviceKnownSince > currentSessionTs + 60_000, // short window to avoid false positive, - pInfo.getOrNull()?.selfSigned != null // adding this to pass distinct when cross sign change - ) - } - } - ) + infoList + .filter { info -> + // filter verified session, by checking the crypto device info + cryptoList.firstOrNull { info.deviceId == it.deviceId }?.isVerified?.not().orFalse() + } + // filter out ignored devices + .filter { !ignoredDeviceList.contains(it.deviceId) } + .sortedByDescending { it.lastSeenTs } + .map { deviceInfo -> + val deviceKnownSince = cryptoList.firstOrNull { it.deviceId == deviceInfo.deviceId }?.firstTimeSeenLocalTs ?: 0 + DeviceDetectionInfo( + deviceInfo, + deviceKnownSince > currentSessionTs + 60_000, // short window to avoid false positive, + pInfo.getOrNull()?.selfSigned != null // adding this to pass distinct when cross sign change + ) + } + } .distinctUntilChanged() .execute { async -> // Timber.v("## Detector trigger passed distinct") @@ -132,14 +120,14 @@ class UnknownDeviceDetectorSharedViewModel @AssistedInject constructor(@Assisted ) } - session.rx().liveUserCryptoDevices(session.myUserId) + session.flow().liveUserCryptoDevices(session.myUserId) .distinctUntilChanged() - .throttleLast(5_000, TimeUnit.MILLISECONDS) - .subscribe { + .sample(5_000) + .onEach { // If we have a new crypto device change, we might want to trigger refresh of device info session.cryptoService().fetchDevicesList(NoOpMatrixCallback()) } - .disposeOnClear() + .launchIn(viewModelScope) // trigger a refresh of lastSeen / last Ip session.cryptoService().fetchDevicesList(NoOpMatrixCallback()) 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 f02711690a..5bdbc95b48 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 @@ -16,16 +16,15 @@ 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 com.airbnb.mvrx.MavericksState +import com.airbnb.mvrx.MavericksViewModelFactory 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.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.platform.EmptyAction import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel @@ -38,6 +37,7 @@ import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.room.RoomSortOrder import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams +import org.matrix.android.sdk.api.session.room.spaceSummaryQueryParams import org.matrix.android.sdk.api.session.room.summary.RoomAggregateNotificationCount import org.matrix.android.sdk.rx.asObservable import java.util.concurrent.TimeUnit @@ -45,7 +45,7 @@ import java.util.concurrent.TimeUnit data class UnreadMessagesState( val homeSpaceUnread: RoomAggregateNotificationCount = RoomAggregateNotificationCount(0, 0), val otherSpacesUnread: RoomAggregateNotificationCount = RoomAggregateNotificationCount(0, 0) -) : MvRxState +) : MavericksState data class CountInfo( val homeCount: RoomAggregateNotificationCount, @@ -56,25 +56,15 @@ class UnreadMessagesSharedViewModel @AssistedInject constructor(@Assisted initia session: Session, private val vectorPreferences: VectorPreferences, appStateHandler: AppStateHandler, - private val autoAcceptInvites: AutoAcceptInvites) - : VectorViewModel(initialState) { + private val autoAcceptInvites: AutoAcceptInvites) : + VectorViewModel(initialState) { @AssistedFactory - interface Factory { - fun create(initialState: UnreadMessagesState): UnreadMessagesSharedViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: UnreadMessagesState): UnreadMessagesSharedViewModel } - companion object : MvRxViewModelFactory { - - @JvmStatic - override fun create(viewModelContext: ViewModelContext, state: UnreadMessagesState): UnreadMessagesSharedViewModel? { - 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") - } - } + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() override fun handle(action: EmptyAction) {} @@ -143,6 +133,17 @@ class UnreadMessagesSharedViewModel @AssistedInject constructor(@Assisted initia roomSummaryQueryParams { this.memberships = listOf(Membership.INVITE) } ).size } + + val spaceInviteCount = if (autoAcceptInvites.hideInvites) { + 0 + } else { + session.getRoomSummaries( + spaceSummaryQueryParams { + this.memberships = listOf(Membership.INVITE) + } + ).size + } + val totalCount = session.getNotificationCountForRooms( roomSummaryQueryParams { this.memberships = listOf(Membership.JOIN) @@ -161,15 +162,16 @@ class UnreadMessagesSharedViewModel @AssistedInject constructor(@Assisted initia // filter out current selection it.roomId != selectedSpace } + CountInfo( homeCount = counts, otherCount = RoomAggregateNotificationCount( - rootCounts.fold(0, { acc, rs -> - acc + rs.notificationCount - }) + (counts.notificationCount.takeIf { selectedSpace != null } ?: 0), - rootCounts.fold(0, { acc, rs -> - acc + rs.highlightCount - }) + (counts.highlightCount.takeIf { selectedSpace != null } ?: 0) + notificationCount = rootCounts.fold(0, { acc, rs -> acc + rs.notificationCount }) + + (counts.notificationCount.takeIf { selectedSpace != null } ?: 0) + + spaceInviteCount, + highlightCount = rootCounts.fold(0, { acc, rs -> acc + rs.highlightCount }) + + (counts.highlightCount.takeIf { selectedSpace != null } ?: 0) + + spaceInviteCount ) ) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/breadcrumbs/BreadcrumbsFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/breadcrumbs/BreadcrumbsFragment.kt index d66f3848a5..4d44ff775a 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/breadcrumbs/BreadcrumbsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/breadcrumbs/BreadcrumbsFragment.kt @@ -28,12 +28,10 @@ import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.FragmentBreadcrumbsBinding import im.vector.app.features.home.room.detail.RoomDetailSharedAction import im.vector.app.features.home.room.detail.RoomDetailSharedActionViewModel - import javax.inject.Inject class BreadcrumbsFragment @Inject constructor( - private val breadcrumbsController: BreadcrumbsController, - val breadcrumbsViewModelFactory: BreadcrumbsViewModel.Factory + private val breadcrumbsController: BreadcrumbsController ) : VectorBaseFragment(), BreadcrumbsController.Listener { diff --git a/vector/src/main/java/im/vector/app/features/home/room/breadcrumbs/BreadcrumbsItem.kt b/vector/src/main/java/im/vector/app/features/home/room/breadcrumbs/BreadcrumbsItem.kt index fd912a4953..52996c74de 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/breadcrumbs/BreadcrumbsItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/breadcrumbs/BreadcrumbsItem.kt @@ -27,6 +27,7 @@ import im.vector.app.core.epoxy.ClickListener import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.epoxy.onClick +import im.vector.app.features.displayname.getBestName import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.list.UnreadCounterBadgeView import org.matrix.android.sdk.api.util.MatrixItem diff --git a/vector/src/main/java/im/vector/app/features/home/room/breadcrumbs/BreadcrumbsViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/breadcrumbs/BreadcrumbsViewModel.kt index d3825de4ef..112b7e8574 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/breadcrumbs/BreadcrumbsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/breadcrumbs/BreadcrumbsViewModel.kt @@ -16,39 +16,31 @@ package im.vector.app.features.home.room.breadcrumbs -import com.airbnb.mvrx.FragmentViewModelContext -import com.airbnb.mvrx.MvRxViewModelFactory -import com.airbnb.mvrx.ViewModelContext +import com.airbnb.mvrx.MavericksViewModelFactory import dagger.assisted.Assisted -import dagger.assisted.AssistedInject import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.platform.EmptyAction import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel -import io.reactivex.schedulers.Schedulers import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.Session 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.rx +import org.matrix.android.sdk.flow.flow class BreadcrumbsViewModel @AssistedInject constructor(@Assisted initialState: BreadcrumbsViewState, - private val session: Session) - : VectorViewModel(initialState) { + private val session: Session) : + VectorViewModel(initialState) { @AssistedFactory - interface Factory { - fun create(initialState: BreadcrumbsViewState): BreadcrumbsViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: BreadcrumbsViewState): BreadcrumbsViewModel } - companion object : MvRxViewModelFactory { - - @JvmStatic - override fun create(viewModelContext: ViewModelContext, state: BreadcrumbsViewState): BreadcrumbsViewModel? { - val fragment: BreadcrumbsFragment = (viewModelContext as FragmentViewModelContext).fragment() - return fragment.breadcrumbsViewModelFactory.create(state) - } - } + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() init { observeBreadcrumbs() @@ -61,12 +53,11 @@ class BreadcrumbsViewModel @AssistedInject constructor(@Assisted initialState: B // PRIVATE METHODS ***************************************************************************** private fun observeBreadcrumbs() { - session.rx() + session.flow() .liveBreadcrumbs(roomSummaryQueryParams { displayName = QueryStringValue.NoCondition memberships = listOf(Membership.JOIN) }) - .observeOn(Schedulers.computation()) .execute { asyncBreadcrumbs -> copy(asyncBreadcrumbs = asyncBreadcrumbs) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/breadcrumbs/BreadcrumbsViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/breadcrumbs/BreadcrumbsViewState.kt index f067591e83..f2971db093 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/breadcrumbs/BreadcrumbsViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/breadcrumbs/BreadcrumbsViewState.kt @@ -17,10 +17,10 @@ package im.vector.app.features.home.room.breadcrumbs import com.airbnb.mvrx.Async -import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.Uninitialized import org.matrix.android.sdk.api.session.room.model.RoomSummary data class BreadcrumbsViewState( val asyncBreadcrumbs: Async> = Uninitialized -) : MvRxState +) : MavericksState diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/AutoCompleter.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/AutoCompleter.kt index 8e6343519a..1d6530218d 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/AutoCompleter.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/AutoCompleter.kt @@ -36,6 +36,7 @@ import im.vector.app.features.autocomplete.group.AutocompleteGroupPresenter import im.vector.app.features.autocomplete.member.AutocompleteMemberPresenter import im.vector.app.features.autocomplete.room.AutocompleteRoomPresenter import im.vector.app.features.command.Command +import im.vector.app.features.displayname.getBestName import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.html.PillImageSpan import im.vector.app.features.themes.ThemeUtils @@ -219,8 +220,15 @@ class AutoCompleter @AssistedInject constructor( // Replace the word by its completion val displayName = matrixItem.getBestName() - // with a trailing space - editable.replace(startIndex, endIndex, "$displayName ") + // Adding trailing space " " or ": " if the user started mention someone + val displayNameSuffix = + if (firstChar == "@" && startIndex == 0) { + ": " + } else { + " " + } + + editable.replace(startIndex, endIndex, "$displayName$displayNameSuffix") // Add the span val span = PillImageSpan( diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/ChatEffectManager.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/ChatEffectManager.kt index 24151c5c10..f95baae36b 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/ChatEffectManager.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/ChatEffectManager.kt @@ -105,8 +105,8 @@ class ChatEffectManager @Inject constructor() { } private fun hasAlreadyPlayed(event: TimelineEvent): Boolean { - return alreadyPlayed.contains(event.eventId) - || (event.root.unsignedData?.transactionId?.let { alreadyPlayed.contains(it) } ?: false) + return alreadyPlayed.contains(event.eventId) || + (event.root.unsignedData?.transactionId?.let { alreadyPlayed.contains(it) } ?: false) } private fun findEffect(content: MessageContent, event: TimelineEvent): ChatEffect? { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/JoinReplacementRoomBottomSheet.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/JoinReplacementRoomBottomSheet.kt index 54681366e0..ba559677c9 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/JoinReplacementRoomBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/JoinReplacementRoomBottomSheet.kt @@ -25,8 +25,8 @@ import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.parentFragmentViewModel +import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R -import im.vector.app.core.di.ScreenComponent import im.vector.app.core.epoxy.ClickListener import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.platform.ButtonStateView @@ -34,6 +34,7 @@ import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment import im.vector.app.databinding.BottomSheetTombstoneJoinBinding import javax.inject.Inject +@AndroidEntryPoint class JoinReplacementRoomBottomSheet : VectorBaseBottomSheetDialogFragment() { @@ -43,10 +44,6 @@ class JoinReplacementRoomBottomSheet : @Inject lateinit var errorFormatter: ErrorFormatter - override fun injectWith(injector: ScreenComponent) { - injector.inject(this) - } - private val viewModel: RoomDetailViewModel by parentFragmentViewModel() override val showExpanded: Boolean @@ -61,7 +58,7 @@ class JoinReplacementRoomBottomSheet : } } - viewModel.selectSubscribe(this, RoomDetailViewState::joinUpgradedRoomAsync) { joinState -> + viewModel.onEach(RoomDetailViewState::joinUpgradedRoomAsync) { joinState -> when (joinState) { // it should never be Uninitialized Uninitialized, 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 9bb82cdc27..a9b9f8000b 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 @@ -30,10 +30,7 @@ import org.matrix.android.sdk.api.session.widgets.model.Widget import org.matrix.android.sdk.api.util.MatrixItem sealed class RoomDetailAction : VectorViewModelAction { - data class UserIsTyping(val isTyping: Boolean) : RoomDetailAction() - data class SaveDraft(val draft: String) : RoomDetailAction() data class SendSticker(val stickerContent: MessageStickerContent) : RoomDetailAction() - data class SendMessage(val text: CharSequence, val autoMarkdown: Boolean) : RoomDetailAction() data class SendMedia(val attachments: List, val compressBeforeSending: Boolean) : RoomDetailAction() data class TimelineEventTurnsVisible(val event: TimelineEvent) : RoomDetailAction() data class TimelineEventTurnsInvisible(val event: TimelineEvent) : RoomDetailAction() @@ -52,11 +49,6 @@ sealed class RoomDetailAction : VectorViewModelAction { object EnterTrackingUnreadMessagesState : RoomDetailAction() object ExitTrackingUnreadMessagesState : RoomDetailAction() - data class EnterEditMode(val eventId: String, val text: String) : RoomDetailAction() - data class EnterQuoteMode(val eventId: String, val text: String) : RoomDetailAction() - data class EnterReplyMode(val eventId: String, val text: String) : RoomDetailAction() - data class EnterRegularMode(val text: String, val fromSharing: Boolean) : RoomDetailAction() - data class ResendMessage(val eventId: String) : RoomDetailAction() data class RemoveFailedEcho(val eventId: String) : RoomDetailAction() data class CancelSend(val eventId: String, val force: Boolean) : RoomDetailAction() @@ -75,7 +67,7 @@ sealed class RoomDetailAction : VectorViewModelAction { object ResendAll : RoomDetailAction() data class StartCall(val isVideo: Boolean) : RoomDetailAction() - data class AcceptCall(val callId: String): RoomDetailAction() + data class AcceptCall(val callId: String) : RoomDetailAction() object EndCall : RoomDetailAction() data class AcceptVerificationRequest(val transactionId: String, val otherUserId: String) : RoomDetailAction() @@ -91,13 +83,13 @@ sealed class RoomDetailAction : VectorViewModelAction { data class AddJitsiWidget(val withVideo: Boolean) : RoomDetailAction() data class RemoveWidget(val widgetId: String) : RoomDetailAction() - object JoinJitsiCall: RoomDetailAction() - object LeaveJitsiCall: RoomDetailAction() + object JoinJitsiCall : RoomDetailAction() + object LeaveJitsiCall : RoomDetailAction() data class EnsureNativeWidgetAllowed(val widget: Widget, val userJustAccepted: Boolean, val grantedEvents: RoomDetailViewEvents) : RoomDetailAction() - data class UpdateJoinJitsiCallStatus(val conferenceEvent: ConferenceEvent): RoomDetailAction() + data class UpdateJoinJitsiCallStatus(val conferenceEvent: ConferenceEvent) : RoomDetailAction() data class OpenOrCreateDm(val userId: String) : RoomDetailAction() data class JumpToReadReceipt(val userId: String) : RoomDetailAction() @@ -115,7 +107,7 @@ sealed class RoomDetailAction : VectorViewModelAction { // Failed messages object RemoveAllFailedMessages : RoomDetailAction() - data class RoomUpgradeSuccess(val replacementRoomId: String): RoomDetailAction() + data class RoomUpgradeSuccess(val replacementRoomId: String) : RoomDetailAction() // Voice Message object StartRecordingVoiceMessage : RoomDetailAction() @@ -123,5 +115,5 @@ sealed class RoomDetailAction : VectorViewModelAction { object PauseRecordingVoiceMessage : RoomDetailAction() data class PlayOrPauseVoicePlayback(val eventId: String, val messageAudioContent: MessageAudioContent) : RoomDetailAction() object PlayOrPauseRecordingPlayback : RoomDetailAction() - object EndAllVoiceActions : RoomDetailAction() + data class EndAllVoiceActions(val deleteRecord: Boolean = true) : RoomDetailAction() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailActivity.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailActivity.kt index 25428bbfbf..ba53f75eca 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailActivity.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailActivity.kt @@ -20,65 +20,65 @@ import android.content.Context import android.content.Intent import android.os.Bundle import android.widget.Toast -import com.google.android.material.appbar.MaterialToolbar import androidx.core.view.GravityCompat import androidx.drawerlayout.widget.DrawerLayout +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager +import com.airbnb.mvrx.Mavericks import com.airbnb.mvrx.viewModel +import com.google.android.material.appbar.MaterialToolbar +import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R -import im.vector.app.core.di.ScreenComponent import im.vector.app.core.extensions.hideKeyboard import im.vector.app.core.extensions.replaceFragment import im.vector.app.core.platform.ToolbarConfigurable import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.databinding.ActivityRoomDetailBinding import im.vector.app.features.home.room.breadcrumbs.BreadcrumbsFragment +import im.vector.app.features.matrixto.MatrixToBottomSheet +import im.vector.app.features.navigation.Navigator import im.vector.app.features.room.RequireActiveMembershipAction import im.vector.app.features.room.RequireActiveMembershipViewEvents import im.vector.app.features.room.RequireActiveMembershipViewModel -import im.vector.app.features.room.RequireActiveMembershipViewState -import im.vector.app.features.widgets.permissions.RoomWidgetPermissionViewModel -import im.vector.app.features.widgets.permissions.RoomWidgetPermissionViewState - -import javax.inject.Inject +@AndroidEntryPoint class RoomDetailActivity : VectorBaseActivity(), ToolbarConfigurable, - RequireActiveMembershipViewModel.Factory, - RoomWidgetPermissionViewModel.Factory { + MatrixToBottomSheet.InteractionListener { override fun getBinding(): ActivityRoomDetailBinding { return ActivityRoomDetailBinding.inflate(layoutInflater) } + private val fragmentLifecycleCallbacks = object : FragmentManager.FragmentLifecycleCallbacks() { + + override fun onFragmentResumed(fm: FragmentManager, f: Fragment) { + if (f is MatrixToBottomSheet) { + f.interactionListener = this@RoomDetailActivity + } + super.onFragmentResumed(fm, f) + } + + override fun onFragmentPaused(fm: FragmentManager, f: Fragment) { + if (f is MatrixToBottomSheet) { + f.interactionListener = null + } + super.onFragmentPaused(fm, f) + } + } + override fun getCoordinatorLayout() = views.coordinatorLayout private lateinit var sharedActionViewModel: RoomDetailSharedActionViewModel private val requireActiveMembershipViewModel: RequireActiveMembershipViewModel by viewModel() - @Inject - lateinit var requireActiveMembershipViewModelFactory: RequireActiveMembershipViewModel.Factory - - override fun create(initialState: RequireActiveMembershipViewState): RequireActiveMembershipViewModel { - // Due to shortcut, we cannot use MvRx args. Pass the first roomId here - return requireActiveMembershipViewModelFactory.create(initialState.copy(roomId = currentRoomId ?: "")) - } - - @Inject - lateinit var permissionsViewModelFactory: RoomWidgetPermissionViewModel.Factory - override fun create(initialState: RoomWidgetPermissionViewState): RoomWidgetPermissionViewModel { - return permissionsViewModelFactory.create(initialState) - } - - override fun injectWith(injector: ScreenComponent) { - injector.inject(this) - } - // Simple filter var currentRoomId: String? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, false) waitingView = views.waitingView.waitingView val roomDetailArgs: RoomDetailArgs? = if (intent?.action == ACTION_ROOM_DETAILS_FROM_SHORTCUT) { RoomDetailArgs(roomId = intent?.extras?.getString(EXTRA_ROOM_ID)!!) @@ -86,6 +86,7 @@ class RoomDetailActivity : intent?.extras?.getParcelable(EXTRA_ROOM_DETAIL_ARGS) } if (roomDetailArgs == null) return + intent.putExtra(Mavericks.KEY_ARG, roomDetailArgs) currentRoomId = roomDetailArgs.roomId if (isFirstCreation()) { @@ -130,6 +131,7 @@ class RoomDetailActivity : } override fun onDestroy() { + supportFragmentManager.unregisterFragmentLifecycleCallbacks(fragmentLifecycleCallbacks) views.drawerLayout.removeDrawerListener(drawerListener) super.onDestroy() } @@ -182,4 +184,12 @@ class RoomDetailActivity : } } } + + override fun mxToBottomSheetNavigateToRoom(roomId: String) { + navigator.openRoom(this, roomId) + } + + override fun mxToBottomSheetSwitchToSpace(spaceId: String) { + navigator.switchToSpace(this, spaceId, Navigator.PostSwitchSpaceAction.None) + } } 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 287ff70dde..75edee5d55 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 @@ -52,6 +52,7 @@ import androidx.core.view.forEach import androidx.core.view.isInvisible import androidx.core.view.isVisible import androidx.fragment.app.setFragmentResultListener +import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager @@ -61,7 +62,7 @@ import com.airbnb.epoxy.EpoxyModel import com.airbnb.epoxy.OnModelBuildFinishedListener import com.airbnb.epoxy.addGlidePreloader import com.airbnb.epoxy.glidePreloader -import com.airbnb.mvrx.MvRx +import com.airbnb.mvrx.Mavericks import com.airbnb.mvrx.args import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState @@ -86,6 +87,7 @@ import im.vector.app.core.hardware.vibrate import im.vector.app.core.intent.getFilenameFromUri import im.vector.app.core.intent.getMimeTypeFromUri import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.core.platform.lifecycleAwareLazy import im.vector.app.core.platform.showOptimizedSnackbar import im.vector.app.core.resources.ColorProvider import im.vector.app.core.ui.views.CurrentCallsView @@ -132,7 +134,12 @@ import im.vector.app.features.command.Command import im.vector.app.features.crypto.keysbackup.restore.KeysBackupRestoreActivity import im.vector.app.features.crypto.verification.VerificationBottomSheet import im.vector.app.features.home.AvatarRenderer +import im.vector.app.features.home.room.detail.composer.SendMode +import im.vector.app.features.home.room.detail.composer.TextComposerAction import im.vector.app.features.home.room.detail.composer.TextComposerView +import im.vector.app.features.home.room.detail.composer.TextComposerViewEvents +import im.vector.app.features.home.room.detail.composer.TextComposerViewModel +import im.vector.app.features.home.room.detail.composer.TextComposerViewState import im.vector.app.features.home.room.detail.composer.VoiceMessageRecorderView import im.vector.app.features.home.room.detail.readreceipts.DisplayReadReceiptsBottomSheet import im.vector.app.features.home.room.detail.timeline.TimelineEventController @@ -153,6 +160,7 @@ import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData import im.vector.app.features.home.room.detail.timeline.reactions.ViewReactionsBottomSheet import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever import im.vector.app.features.home.room.detail.upgrade.MigrateRoomBottomSheet +import im.vector.app.features.home.room.detail.views.RoomDetailLazyLoadedViews import im.vector.app.features.home.room.detail.widget.RoomWidgetsBottomSheet import im.vector.app.features.html.EventHtmlRenderer import im.vector.app.features.html.PillImageSpan @@ -177,15 +185,12 @@ import im.vector.app.features.widgets.WidgetActivity import im.vector.app.features.widgets.WidgetArgs import im.vector.app.features.widgets.WidgetKind import im.vector.app.features.widgets.permissions.RoomWidgetPermissionBottomSheet -import io.reactivex.android.schedulers.AndroidSchedulers -import io.reactivex.schedulers.Schedulers import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize import nl.dionsegijn.konfetti.models.Shape import nl.dionsegijn.konfetti.models.Size import org.billcarsonfr.jsonviewer.JSonViewerDialog import org.commonmark.parser.Parser -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 import org.matrix.android.sdk.api.session.events.model.toModel @@ -285,6 +290,7 @@ class RoomDetailFragment @Inject constructor( autoCompleterFactory.create(roomDetailArgs.roomId) } private val roomDetailViewModel: RoomDetailViewModel by fragmentViewModel() + private val textComposerViewModel: TextComposerViewModel by fragmentViewModel() private val debouncer = Debouncer(createUIHandler()) private lateinit var scrollOnNewMessageCallback: ScrollOnNewMessageCallback @@ -312,7 +318,10 @@ class RoomDetailFragment @Inject constructor( private var lockSendButton = false private val currentCallsViewPresenter = CurrentCallsViewPresenter() - private lateinit var emojiPopup: EmojiPopup + private val lazyLoadedViews = RoomDetailLazyLoadedViews() + private val emojiPopup: EmojiPopup by lifecycleAwareLazy { + createEmojiPopup() + } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -340,16 +349,15 @@ class RoomDetailFragment @Inject constructor( onTapToReturnToCall = ::onTapToReturnToCall ) keyboardStateUtils = KeyboardStateUtils(requireActivity()) + lazyLoadedViews.bind(views) setupToolbar(views.roomToolbar) setupRecyclerView() setupComposer() - setupInviteView() setupNotificationView() setupJumpToReadMarkerView() setupActiveCallView() setupJumpToBottomView() - setupEmojiPopup() - setupFailedMessagesWarningView() + setupEmojiButton() setupRemoveJitsiWidgetView() setupVoiceMessageView() @@ -371,13 +379,13 @@ class RoomDetailFragment @Inject constructor( invalidateOptionsMenu() } - roomDetailViewModel.selectSubscribe(RoomDetailViewState::canShowJumpToReadMarker, RoomDetailViewState::unreadState) { _, _ -> + roomDetailViewModel.onEach(RoomDetailViewState::canShowJumpToReadMarker, RoomDetailViewState::unreadState) { _, _ -> updateJumpToReadMarkerViewVisibility() } - roomDetailViewModel.selectSubscribe(RoomDetailViewState::sendMode, RoomDetailViewState::canSendMessage) { mode, canSend -> + textComposerViewModel.onEach(TextComposerViewState::sendMode, TextComposerViewState::canSendMessage) { mode, canSend -> if (!canSend) { - return@selectSubscribe + return@onEach } when (mode) { is SendMode.REGULAR -> renderRegularMode(mode.text) @@ -387,8 +395,28 @@ class RoomDetailFragment @Inject constructor( } } - roomDetailViewModel.selectSubscribe(RoomDetailViewState::syncState) { syncState -> - views.syncStateView.render(syncState) + roomDetailViewModel.onEach( + RoomDetailViewState::syncState, + RoomDetailViewState::incrementalSyncStatus, + RoomDetailViewState::pushCounter + ) { syncState, incrementalSyncStatus, pushCounter -> + views.syncStateView.render( + syncState, + incrementalSyncStatus, + pushCounter, + vectorPreferences.developerShowDebugInfo() + ) + } + + textComposerViewModel.observeViewEvents { + when (it) { + is TextComposerViewEvents.JoinRoomCommandSuccess -> handleJoinedToAnotherRoom(it) + is TextComposerViewEvents.SendMessageResult -> renderSendMessageResult(it) + is TextComposerViewEvents.ShowMessage -> showSnackWithMessage(it.message) + is TextComposerViewEvents.ShowRoomUpgradeDialog -> handleShowRoomUpgradeDialog(it) + is TextComposerViewEvents.AnimateSendButtonVisibility -> handleSendButtonVisibilityChanged(it) + is TextComposerViewEvents.OpenRoomMemberProfile -> openRoomMemberProfile(it.userId) + }.exhaustive } roomDetailViewModel.observeViewEvents { @@ -405,8 +433,6 @@ class RoomDetailFragment @Inject constructor( is RoomDetailViewEvents.ShowMessage -> showSnackWithMessage(it.message) is RoomDetailViewEvents.NavigateToEvent -> navigateToEvent(it) is RoomDetailViewEvents.DownloadFileState -> handleDownloadFileState(it) - is RoomDetailViewEvents.JoinRoomCommandSuccess -> handleJoinedToAnotherRoom(it) - is RoomDetailViewEvents.SendMessageResult -> renderSendMessageResult(it) is RoomDetailViewEvents.ShowE2EErrorMessage -> displayE2eError(it.withHeldCode) RoomDetailViewEvents.DisplayPromptForIntegrationManager -> displayPromptForIntegrationManager() is RoomDetailViewEvents.OpenStickerPicker -> openStickerPicker(it) @@ -431,7 +457,6 @@ class RoomDetailFragment @Inject constructor( RoomDetailViewEvents.StopChatEffects -> handleStopChatEffects() is RoomDetailViewEvents.DisplayAndAcceptCall -> acceptIncomingCall(it) RoomDetailViewEvents.RoomReplacementStarted -> handleRoomReplacement() - is RoomDetailViewEvents.ShowRoomUpgradeDialog -> handleShowRoomUpgradeDialog(it) }.exhaustive } @@ -441,6 +466,20 @@ class RoomDetailFragment @Inject constructor( } } + private fun handleSendButtonVisibilityChanged(event: TextComposerViewEvents.AnimateSendButtonVisibility) { + if (event.isVisible) { + views.voiceMessageRecorderView.isVisible = false + views.composerLayout.views.sendButton.alpha = 0f + views.composerLayout.views.sendButton.isVisible = true + views.composerLayout.views.sendButton.animate().alpha(1f).setDuration(150).start() + } else { + views.composerLayout.views.sendButton.isInvisible = true + views.voiceMessageRecorderView.alpha = 0f + views.voiceMessageRecorderView.isVisible = true + views.voiceMessageRecorderView.animate().alpha(1f).setDuration(150).start() + } + } + private fun setupRemoveJitsiWidgetView() { views.removeJitsiWidgetView.onCompleteSliding = { withState(roomDetailViewModel) { @@ -482,7 +521,7 @@ class RoomDetailFragment @Inject constructor( JoinReplacementRoomBottomSheet().show(childFragmentManager, tag) } - private fun handleShowRoomUpgradeDialog(roomDetailViewEvents: RoomDetailViewEvents.ShowRoomUpgradeDialog) { + private fun handleShowRoomUpgradeDialog(roomDetailViewEvents: TextComposerViewEvents.ShowRoomUpgradeDialog) { val tag = MigrateRoomBottomSheet::javaClass.name MigrateRoomBottomSheet.newInstance(roomDetailArgs.roomId, roomDetailViewEvents.newVersion) .show(parentFragmentManager, tag) @@ -584,8 +623,14 @@ class RoomDetailFragment @Inject constructor( ) } - private fun setupEmojiPopup() { - emojiPopup = EmojiPopup + private fun setupEmojiButton() { + views.composerLayout.views.composerEmojiButton.debouncedClicks { + emojiPopup.toggle() + } + } + + private fun createEmojiPopup(): EmojiPopup { + return EmojiPopup .Builder .fromRootView(views.rootConstraintLayout) .setKeyboardAnimationStyle(R.style.emoji_fade_animation_style) @@ -595,21 +640,37 @@ class RoomDetailFragment @Inject constructor( setImageResource(R.drawable.ic_keyboard) } } - .setOnEmojiPopupDismissListener { + .setOnEmojiPopupDismissListenerLifecycleAware { views.composerLayout.views.composerEmojiButton.apply { contentDescription = getString(R.string.a11y_open_emoji_picker) setImageResource(R.drawable.ic_insert_emoji) } } .build(views.composerLayout.views.composerEditText) + } - views.composerLayout.views.composerEmojiButton.debouncedClicks { - emojiPopup.toggle() + /** + * Ensure dismiss actions only trigger when the fragment is in the started state + * EmojiPopup by default dismisses onViewDetachedFromWindow, this can cause race conditions with onDestroyView + */ + private fun EmojiPopup.Builder.setOnEmojiPopupDismissListenerLifecycleAware(action: () -> Unit): EmojiPopup.Builder { + return setOnEmojiPopupDismissListener { + if (lifecycle.currentState == Lifecycle.State.STARTED) { + action() + } } } - private fun setupFailedMessagesWarningView() { - views.failedMessagesWarningView.callback = object : FailedMessagesWarningView.Callback { + private val permissionVoiceMessageLauncher = registerForPermissionsResult { allGranted, deniedPermanently -> + if (allGranted) { + // In this case, let the user start again the gesture + } else if (deniedPermanently) { + vectorBaseActivity.onPermissionDeniedSnackbar(R.string.denied_permission_voice_message) + } + } + + private fun createFailedMessagesWarningCallback(): FailedMessagesWarningView.Callback { + return object : FailedMessagesWarningView.Callback { override fun onDeleteAllClicked() { MaterialAlertDialogBuilder(requireContext()) .setTitle(R.string.event_status_delete_all_failed_dialog_title) @@ -627,22 +688,14 @@ class RoomDetailFragment @Inject constructor( } } - private val permissionVoiceMessageLauncher = registerForPermissionsResult { allGranted, deniedPermanently -> - if (allGranted) { - // In this case, let the user start again the gesture - } else if (deniedPermanently) { - vectorBaseActivity.onPermissionDeniedSnackbar(R.string.denied_permission_voice_message) - } - } - private fun setupVoiceMessageView() { views.voiceMessageRecorderView.voiceMessagePlaybackTracker = voiceMessagePlaybackTracker views.voiceMessageRecorderView.callback = object : VoiceMessageRecorderView.Callback { override fun onVoiceRecordingStarted(): Boolean { return if (checkPermissions(PERMISSIONS_FOR_VOICE_MESSAGE, requireActivity(), permissionVoiceMessageLauncher)) { - views.composerLayout.isInvisible = true roomDetailViewModel.handle(RoomDetailAction.StartRecordingVoiceMessage) + textComposerViewModel.handle(TextComposerAction.OnVoiceRecordingStateChanged(true)) vibrate(requireContext()) true } else { @@ -652,8 +705,8 @@ class RoomDetailFragment @Inject constructor( } override fun onVoiceRecordingEnded(isCancelled: Boolean) { - views.composerLayout.isInvisible = false roomDetailViewModel.handle(RoomDetailAction.EndRecordingVoiceMessage(isCancelled)) + textComposerViewModel.handle(TextComposerAction.OnVoiceRecordingStateChanged(false)) } override fun onVoiceRecordingPlaybackModeOn() { @@ -738,8 +791,8 @@ class RoomDetailFragment @Inject constructor( .show() } - private fun handleJoinedToAnotherRoom(action: RoomDetailViewEvents.JoinRoomCommandSuccess) { - updateComposerText("") + private fun handleJoinedToAnotherRoom(action: TextComposerViewEvents.JoinRoomCommandSuccess) { + views.composerLayout.setTextIfDifferent("") lockSendButton = false navigator.openRoom(vectorBaseActivity, action.roomId) } @@ -747,7 +800,7 @@ class RoomDetailFragment @Inject constructor( private fun handleShareData() { when (val sharedData = roomDetailArgs.sharedData) { is SharedData.Text -> { - roomDetailViewModel.handle(RoomDetailAction.EnterRegularMode(sharedData.text, fromSharing = true)) + textComposerViewModel.handle(TextComposerAction.EnterRegularMode(sharedData.text, fromSharing = true)) } is SharedData.Attachments -> { // open share edition @@ -767,6 +820,7 @@ class RoomDetailFragment @Inject constructor( } override fun onDestroyView() { + lazyLoadedViews.unBind() timelineEventController.callback = null timelineEventController.removeModelBuildListener(modelBuildListener) currentCallsViewPresenter.unBind() @@ -774,8 +828,6 @@ class RoomDetailFragment @Inject constructor( autoCompleter.clear() debouncer.cancelAll() views.timelineRecyclerView.cleanup() - emojiPopup.dismiss() - super.onDestroyView() } @@ -966,10 +1018,7 @@ class RoomDetailFragment @Inject constructor( private fun renderRegularMode(text: String) { autoCompleter.exitSpecialMode() views.composerLayout.collapse() - - views.voiceMessageRecorderView.isVisible = text.isBlank() - - updateComposerText(text) + views.composerLayout.setTextIfDifferent(text) views.composerLayout.views.sendButton.contentDescription = getString(R.string.send) } @@ -1008,7 +1057,7 @@ class RoomDetailFragment @Inject constructor( false } - updateComposerText(defaultContent) + views.composerLayout.setTextIfDifferent(defaultContent) views.composerLayout.views.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), iconRes)) views.composerLayout.views.sendButton.contentDescription = getString(descriptionRes) @@ -1020,21 +1069,11 @@ class RoomDetailFragment @Inject constructor( // need to do it here also when not using quick reply focusComposerAndShowKeyboard() views.composerLayout.views.composerRelatedMessageImage.isVisible = isImageVisible - views.voiceMessageRecorderView.isVisible = false } } focusComposerAndShowKeyboard() } - private fun updateComposerText(text: String) { - // Do not update if this is the same text to avoid the cursor to move - if (text != views.composerLayout.text.toString()) { - // Ignore update to avoid saving a draft - views.composerLayout.views.composerEditText.setText(text) - views.composerLayout.views.composerEditText.setSelection(views.composerLayout.text?.length ?: 0) - } - } - override fun onResume() { super.onResume() notificationDrawerManager.setCurrentRoom(roomDetailArgs.roomId) @@ -1063,10 +1102,10 @@ class RoomDetailFragment @Inject constructor( notificationDrawerManager.setCurrentRoom(null) - roomDetailViewModel.handle(RoomDetailAction.SaveDraft(views.composerLayout.text.toString())) + textComposerViewModel.handle(TextComposerAction.SaveDraft(views.composerLayout.text.toString())) // We should improve the UX to support going into playback mode when paused and delete the media when the view is destroyed. - roomDetailViewModel.handle(RoomDetailAction.EndAllVoiceActions) + roomDetailViewModel.handle(RoomDetailAction.EndAllVoiceActions(deleteRecord = false)) views.voiceMessageRecorderView.initVoiceRecordingViews() } @@ -1182,12 +1221,12 @@ class RoomDetailFragment @Inject constructor( override fun performQuickReplyOnHolder(model: EpoxyModel<*>) { (model as? AbsMessageItem)?.attributes?.informationData?.let { val eventId = it.eventId - roomDetailViewModel.handle(RoomDetailAction.EnterReplyMode(eventId, views.composerLayout.text.toString())) + textComposerViewModel.handle(TextComposerAction.EnterReplyMode(eventId, views.composerLayout.text.toString())) } } override fun canSwipeModel(model: EpoxyModel<*>): Boolean { - val canSendMessage = withState(roomDetailViewModel) { + val canSendMessage = withState(textComposerViewModel) { it.canSendMessage } if (!canSendMessage) { @@ -1265,10 +1304,10 @@ class RoomDetailFragment @Inject constructor( true } // Add external keyboard functionality (to send messages) - else if (null != keyEvent - && !keyEvent.isShiftPressed - && keyEvent.keyCode == KeyEvent.KEYCODE_ENTER - && resources.configuration.keyboard != Configuration.KEYBOARD_NOKEYS) { + else if (null != keyEvent && + !keyEvent.isShiftPressed && + keyEvent.keyCode == KeyEvent.KEYCODE_ENTER && + resources.configuration.keyboard != Configuration.KEYBOARD_NOKEYS) { sendTextMessage(v.text) true } else false @@ -1289,22 +1328,15 @@ class RoomDetailFragment @Inject constructor( } override fun onCloseRelatedMessage() { - roomDetailViewModel.handle(RoomDetailAction.EnterRegularMode(views.composerLayout.text.toString(), false)) + textComposerViewModel.handle(TextComposerAction.EnterRegularMode(views.composerLayout.text.toString(), false)) } override fun onRichContentSelected(contentUri: Uri): Boolean { return sendUri(contentUri) } - override fun onTextBlankStateChanged(isBlank: Boolean) { - if (!views.composerLayout.views.sendButton.isVisible) { - // Animate alpha to prevent overlapping with the animation of the send button - views.voiceMessageRecorderView.alpha = 0f - views.voiceMessageRecorderView.isVisible = true - views.voiceMessageRecorderView.animate().alpha(1f).setDuration(300).start() - } else { - views.voiceMessageRecorderView.isVisible = false - } + override fun onTextChanged(text: CharSequence) { + textComposerViewModel.handle(TextComposerAction.OnTextChanged(text)) } } } @@ -1318,7 +1350,7 @@ class RoomDetailFragment @Inject constructor( // We collapse ASAP, if not there will be a slight annoying delay views.composerLayout.collapse(true) lockSendButton = true - roomDetailViewModel.handle(RoomDetailAction.SendMessage(text, vectorPreferences.isMarkdownEnabled())) + textComposerViewModel.handle(TextComposerAction.SendMessage(text, vectorPreferences.isMarkdownEnabled())) emojiPopup.dismiss() } } @@ -1330,7 +1362,7 @@ class RoomDetailFragment @Inject constructor( .map { it.isNotEmpty() } .subscribe { Timber.d("Typing: User is typing: $it") - roomDetailViewModel.handle(RoomDetailAction.UserIsTyping(it)) + textComposerViewModel.handle(TextComposerAction.UserIsTyping(it)) } .disposeOnDestroyView() @@ -1350,51 +1382,56 @@ class RoomDetailFragment @Inject constructor( return isHandled } - private fun setupInviteView() { - views.inviteView.callback = this - } - - override fun invalidate() = withState(roomDetailViewModel) { state -> + override fun invalidate() = withState(roomDetailViewModel, textComposerViewModel) { mainState, textComposerState -> invalidateOptionsMenu() - val summary = state.asyncRoomSummary() - renderToolbar(summary, state.typingMessage) - views.removeJitsiWidgetView.render(state) - views.failedMessagesWarningView.render(state.hasFailedSending) - val inviter = state.asyncInviter() + val summary = mainState.asyncRoomSummary() + renderToolbar(summary, mainState.formattedTypingUsers) + views.removeJitsiWidgetView.render(mainState) + if (mainState.hasFailedSending) { + lazyLoadedViews.failedMessagesWarningView(inflateIfNeeded = true, createFailedMessagesWarningCallback())?.isVisible = true + } else { + lazyLoadedViews.failedMessagesWarningView(inflateIfNeeded = false)?.isVisible = false + } + val inviter = mainState.asyncInviter() if (summary?.membership == Membership.JOIN) { views.jumpToBottomView.count = summary.notificationCount views.jumpToBottomView.drawBadge = summary.hasUnreadMessages - timelineEventController.update(state) - views.inviteView.isVisible = false - if (state.tombstoneEvent == null) { - if (state.canSendMessage) { - if (!views.voiceMessageRecorderView.isActive()) { - views.composerLayout.isVisible = true - views.voiceMessageRecorderView.isVisible = views.composerLayout.text?.isBlank().orFalse() - views.composerLayout.setRoomEncrypted(summary.isEncrypted) - views.notificationAreaView.render(NotificationAreaView.State.Hidden) - views.composerLayout.alwaysShowSendButton = false - } + timelineEventController.update(mainState) + lazyLoadedViews.inviteView(false)?.isVisible = false + if (mainState.tombstoneEvent == null) { + views.composerLayout.isInvisible = !textComposerState.isComposerVisible + views.voiceMessageRecorderView.isVisible = textComposerState.isVoiceMessageRecorderVisible + views.composerLayout.views.sendButton.isInvisible = !textComposerState.isSendButtonVisible + views.composerLayout.setRoomEncrypted(summary.isEncrypted) + // views.composerLayout.alwaysShowSendButton = false + if (textComposerState.canSendMessage) { + views.notificationAreaView.render(NotificationAreaView.State.Hidden) } else { - views.composerLayout.isVisible = false - views.voiceMessageRecorderView.isVisible = false views.notificationAreaView.render(NotificationAreaView.State.NoPermissionToPost) } } else { - views.composerLayout.isVisible = false - views.voiceMessageRecorderView.isVisible = false - views.notificationAreaView.render(NotificationAreaView.State.Tombstone(state.tombstoneEvent)) + views.hideComposerViews() + views.notificationAreaView.render(NotificationAreaView.State.Tombstone(mainState.tombstoneEvent)) } } else if (summary?.membership == Membership.INVITE && inviter != null) { - views.inviteView.isVisible = true - views.inviteView.render(inviter, VectorInviteView.Mode.LARGE, state.changeMembershipState) - // Intercept click event - views.inviteView.setOnClickListener { } - } else if (state.asyncInviter.complete) { + views.hideComposerViews() + lazyLoadedViews.inviteView(true)?.apply { + callback = this@RoomDetailFragment + isVisible = true + render(inviter, VectorInviteView.Mode.LARGE, mainState.changeMembershipState) + setOnClickListener { } + } + Unit + } else if (mainState.asyncInviter.complete) { vectorBaseActivity.finish() } } + private fun FragmentRoomDetailBinding.hideComposerViews() { + composerLayout.isVisible = false + voiceMessageRecorderView.isVisible = false + } + private fun renderToolbar(roomSummary: RoomSummary?, typingMessage: String?) { if (roomSummary == null) { views.roomToolbarContentView.isClickable = false @@ -1402,9 +1439,10 @@ class RoomDetailFragment @Inject constructor( views.roomToolbarContentView.isClickable = roomSummary.membership == Membership.JOIN views.roomToolbarTitleView.text = roomSummary.displayName avatarRenderer.render(roomSummary.toMatrixItem(), views.roomToolbarAvatarImageView) - renderSubTitle(typingMessage, roomSummary.topic) views.roomToolbarDecorationImageView.render(roomSummary.roomEncryptionTrustLevel) + views.roomToolbarPresenceImageView.render(roomSummary.isDirect, roomSummary.directUserPresence) + views.roomToolbarPublicImageView.isVisible = roomSummary.isPublic && !roomSummary.isDirect } } @@ -1423,24 +1461,27 @@ class RoomDetailFragment @Inject constructor( } } - private fun renderSendMessageResult(sendMessageResult: RoomDetailViewEvents.SendMessageResult) { + private fun renderSendMessageResult(sendMessageResult: TextComposerViewEvents.SendMessageResult) { when (sendMessageResult) { - is RoomDetailViewEvents.SlashCommandHandled -> { - sendMessageResult.messageRes?.let { showSnackWithMessage(getString(it)) } + is TextComposerViewEvents.SlashCommandLoading -> { + showLoading(null) } - is RoomDetailViewEvents.SlashCommandError -> { + is TextComposerViewEvents.SlashCommandError -> { displayCommandError(getString(R.string.command_problem_with_parameters, sendMessageResult.command.command)) } - is RoomDetailViewEvents.SlashCommandUnknown -> { + is TextComposerViewEvents.SlashCommandUnknown -> { displayCommandError(getString(R.string.unrecognized_command, sendMessageResult.command)) } - is RoomDetailViewEvents.SlashCommandResultOk -> { - updateComposerText("") + is TextComposerViewEvents.SlashCommandResultOk -> { + dismissLoadingDialog() + views.composerLayout.setTextIfDifferent("") + sendMessageResult.messageRes?.let { showSnackWithMessage(getString(it)) } } - is RoomDetailViewEvents.SlashCommandResultError -> { + is TextComposerViewEvents.SlashCommandResultError -> { + dismissLoadingDialog() displayCommandError(errorFormatter.toHumanReadable(sendMessageResult.throwable)) } - is RoomDetailViewEvents.SlashCommandNotImplemented -> { + is TextComposerViewEvents.SlashCommandNotImplemented -> { displayCommandError(getString(R.string.not_implemented)) } } // .exhaustive @@ -1563,7 +1604,7 @@ class RoomDetailFragment @Inject constructor( val otherUserId = data.otherUserId ?: return VerificationBottomSheet().apply { arguments = Bundle().apply { - putParcelable(MvRx.KEY_ARG, VerificationBottomSheet.VerificationArgs( + putParcelable(Mavericks.KEY_ARG, VerificationBottomSheet.VerificationArgs( otherUserId, data.transactionId, roomId = roomDetailArgs.roomId)) } }.show(parentFragmentManager, "REQ") @@ -1571,57 +1612,54 @@ class RoomDetailFragment @Inject constructor( } } -// TimelineEventController.Callback ************************************************************ + // TimelineEventController.Callback ************************************************************ override fun onUrlClicked(url: String, title: String): Boolean { - permalinkHandler - .launch(requireActivity(), url, object : NavigationInterceptor { - override fun navToRoom(roomId: String?, eventId: String?, deepLink: Uri?): Boolean { - // Same room? - if (roomId == roomDetailArgs.roomId) { - // Navigation to same room - if (eventId == null) { - showSnackWithMessage(getString(R.string.navigate_to_room_when_already_in_the_room)) - } else { - // Highlight and scroll to this event - roomDetailViewModel.handle(RoomDetailAction.NavigateToEvent(eventId, true)) + viewLifecycleOwner.lifecycleScope.launch { + val isManaged = permalinkHandler + .launch(requireActivity(), url, object : NavigationInterceptor { + override fun navToRoom(roomId: String?, eventId: String?, deepLink: Uri?): Boolean { + // Same room? + if (roomId == roomDetailArgs.roomId) { + // Navigation to same room + if (eventId == null) { + showSnackWithMessage(getString(R.string.navigate_to_room_when_already_in_the_room)) + } else { + // Highlight and scroll to this event + roomDetailViewModel.handle(RoomDetailAction.NavigateToEvent(eventId, true)) + } + return true } + // Not handled + return false + } + + override fun navToMemberProfile(userId: String, deepLink: Uri): Boolean { + openRoomMemberProfile(userId) return true } - // Not handled - return false - } - - override fun navToMemberProfile(userId: String, deepLink: Uri): Boolean { - openRoomMemberProfile(userId) - return true - } - }) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { managed -> - if (!managed) { - if (title.isValidUrl() && url.isValidUrl() && URL(title).host != URL(url).host) { - MaterialAlertDialogBuilder(requireActivity(), R.style.ThemeOverlay_Vector_MaterialAlertDialog_NegativeDestructive) - .setTitle(R.string.external_link_confirmation_title) - .setMessage( - getString(R.string.external_link_confirmation_message, title, url) - .toSpannable() - .colorizeMatchingText(url, colorProvider.getColorFromAttribute(R.attr.vctr_content_tertiary)) - .colorizeMatchingText(title, colorProvider.getColorFromAttribute(R.attr.vctr_content_tertiary)) - ) - .setPositiveButton(R.string._continue) { _, _ -> - openUrlInExternalBrowser(requireContext(), url) - } - .setNegativeButton(R.string.cancel, null) - .show() - } else { - // Open in external browser, in a new Tab - openUrlInExternalBrowser(requireContext(), url) - } - } + }) + if (!isManaged) { + if (title.isValidUrl() && url.isValidUrl() && URL(title).host != URL(url).host) { + MaterialAlertDialogBuilder(requireActivity(), R.style.ThemeOverlay_Vector_MaterialAlertDialog_NegativeDestructive) + .setTitle(R.string.external_link_confirmation_title) + .setMessage( + getString(R.string.external_link_confirmation_message, title, url) + .toSpannable() + .colorizeMatchingText(url, colorProvider.getColorFromAttribute(R.attr.vctr_content_tertiary)) + .colorizeMatchingText(title, colorProvider.getColorFromAttribute(R.attr.vctr_content_tertiary)) + ) + .setPositiveButton(R.string._continue) { _, _ -> + openUrlInExternalBrowser(requireContext(), url) + } + .setNegativeButton(R.string.cancel, null) + .show() + } else { + // Open in external browser, in a new Tab + openUrlInExternalBrowser(requireContext(), url) } - .disposeOnDestroyView() + } + } // In fact it is always managed return true } @@ -1713,7 +1751,7 @@ class RoomDetailFragment @Inject constructor( override fun onEventLongClicked(informationData: MessageInformationData, messageContent: Any?, view: View): Boolean { view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) - val roomId = roomDetailViewModel.timeline.getTimelineEventWithId(informationData.eventId)?.roomId ?: return false + val roomId = roomDetailArgs.roomId this.view?.hideKeyboard() MessageActionsBottomSheet @@ -1780,15 +1818,15 @@ class RoomDetailFragment @Inject constructor( } override fun onRoomCreateLinkClicked(url: String) { - permalinkHandler - .launch(requireContext(), url, object : NavigationInterceptor { - override fun navToRoom(roomId: String?, eventId: String?, deepLink: Uri?): Boolean { - requireActivity().finish() - return false - } - }) - .subscribe() - .disposeOnDestroyView() + viewLifecycleOwner.lifecycleScope.launchWhenResumed { + permalinkHandler + .launch(requireContext(), url, object : NavigationInterceptor { + override fun navToRoom(roomId: String?, eventId: String?, deepLink: Uri?): Boolean { + requireActivity().finish() + return false + } + }) + } } override fun onReadReceiptsClicked(readReceipts: List) { @@ -1846,8 +1884,8 @@ class RoomDetailFragment @Inject constructor( } private fun onSaveActionClicked(action: EventSharedAction.Save) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q - && !checkPermissions(PERMISSIONS_FOR_WRITING_FILES, requireActivity(), saveActionActivityResultLauncher)) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q && + !checkPermissions(PERMISSIONS_FOR_WRITING_FILES, requireActivity(), saveActionActivityResultLauncher)) { sharedActionViewModel.pendingAction = action return } @@ -1919,17 +1957,17 @@ class RoomDetailFragment @Inject constructor( } is EventSharedAction.Edit -> { if (!views.voiceMessageRecorderView.isActive()) { - roomDetailViewModel.handle(RoomDetailAction.EnterEditMode(action.eventId, views.composerLayout.text.toString())) + textComposerViewModel.handle(TextComposerAction.EnterEditMode(action.eventId, views.composerLayout.text.toString())) } else { requireActivity().toast(R.string.error_voice_message_cannot_reply_or_edit) } } is EventSharedAction.Quote -> { - roomDetailViewModel.handle(RoomDetailAction.EnterQuoteMode(action.eventId, views.composerLayout.text.toString())) + textComposerViewModel.handle(TextComposerAction.EnterQuoteMode(action.eventId, views.composerLayout.text.toString())) } is EventSharedAction.Reply -> { if (!views.voiceMessageRecorderView.isActive()) { - roomDetailViewModel.handle(RoomDetailAction.EnterReplyMode(action.eventId, views.composerLayout.text.toString())) + textComposerViewModel.handle(TextComposerAction.EnterReplyMode(action.eventId, views.composerLayout.text.toString())) } else { requireActivity().toast(R.string.error_voice_message_cannot_reply_or_edit) } @@ -1999,8 +2037,8 @@ class RoomDetailFragment @Inject constructor( private fun insertUserDisplayNameInTextEditor(userId: String) { val startToCompose = views.composerLayout.text.isNullOrBlank() - if (startToCompose - && userId == session.myUserId) { + if (startToCompose && + userId == session.myUserId) { // Empty composer, current user: start an emote views.composerLayout.views.composerEditText.setText(Command.EMOTE.command + " ") views.composerLayout.views.composerEditText.setSelection(Command.EMOTE.length) @@ -2141,7 +2179,7 @@ class RoomDetailFragment @Inject constructor( override fun onContactAttachmentReady(contactAttachment: ContactAttachment) { super.onContactAttachmentReady(contactAttachment) val formattedContact = contactAttachment.toHumanReadable() - roomDetailViewModel.handle(RoomDetailAction.SendMessage(formattedContact, false)) + textComposerViewModel.handle(TextComposerAction.SendMessage(formattedContact, false)) } private fun onViewWidgetsClicked() { 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 2802ee2f83..2e7f2bfd63 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 @@ -18,10 +18,8 @@ package im.vector.app.features.home.room.detail import android.net.Uri import android.view.View -import androidx.annotation.StringRes import im.vector.app.core.platform.VectorViewEvents import im.vector.app.features.call.webrtc.WebRtcCall -import im.vector.app.features.command.Command import org.matrix.android.sdk.api.session.widgets.model.Widget import org.matrix.android.sdk.api.util.MatrixItem import org.matrix.android.sdk.internal.crypto.model.event.WithHeldCode @@ -66,9 +64,7 @@ sealed class RoomDetailViewEvents : VectorViewEvents { val mimeType: String? ) : RoomDetailViewEvents() - abstract class SendMessageResult : RoomDetailViewEvents() - - data class DisplayAndAcceptCall(val call: WebRtcCall): RoomDetailViewEvents() + data class DisplayAndAcceptCall(val call: WebRtcCall) : RoomDetailViewEvents() object DisplayPromptForIntegrationManager : RoomDetailViewEvents() @@ -82,19 +78,7 @@ sealed class RoomDetailViewEvents : VectorViewEvents { val domain: String, val grantedEvents: RoomDetailViewEvents) : RoomDetailViewEvents() - object MessageSent : SendMessageResult() - data class JoinRoomCommandSuccess(val roomId: String) : SendMessageResult() - class SlashCommandError(val command: Command) : SendMessageResult() - class SlashCommandUnknown(val command: String) : SendMessageResult() - data class SlashCommandHandled(@StringRes val messageRes: Int? = null) : SendMessageResult() - object SlashCommandResultOk : SendMessageResult() - class SlashCommandResultError(val throwable: Throwable) : SendMessageResult() - - // TODO Remove - object SlashCommandNotImplemented : SendMessageResult() - data class StartChatEffect(val type: ChatEffect) : RoomDetailViewEvents() object StopChatEffects : RoomDetailViewEvents() object RoomReplacementStarted : RoomDetailViewEvents() - data class ShowRoomUpgradeDialog(val newVersion: String, val isPublic: Boolean): 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 5ea5e81240..03bde7d4cc 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 @@ -18,17 +18,16 @@ package im.vector.app.features.home.room.detail import android.net.Uri import androidx.annotation.IdRes -import androidx.lifecycle.viewModelScope +import androidx.lifecycle.asFlow import com.airbnb.mvrx.Async import com.airbnb.mvrx.Fail import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.Loading -import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.ViewModelContext import com.jakewharton.rxrelay2.BehaviorRelay -import com.jakewharton.rxrelay2.PublishRelay import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -38,36 +37,37 @@ 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.ConferenceEvent +import im.vector.app.features.call.conference.JitsiActiveConferenceHolder import im.vector.app.features.call.conference.JitsiService import im.vector.app.features.call.lookup.CallProtocolsChecker import im.vector.app.features.call.webrtc.WebRtcCallManager -import im.vector.app.features.command.CommandParser -import im.vector.app.features.command.ParsedCommand import im.vector.app.features.createdirect.DirectRoomHelper import im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrategy import im.vector.app.features.crypto.verification.SupportedVerificationMethodsProvider 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.url.PreviewUrlRetriever import im.vector.app.features.home.room.typing.TypingHelper -import im.vector.app.features.powerlevel.PowerLevelsObservableFactory +import im.vector.app.features.powerlevel.PowerLevelsFlowFactory import im.vector.app.features.session.coroutineScope +import im.vector.app.features.settings.VectorDataStore import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.voice.VoicePlayerHelper -import io.reactivex.Observable import io.reactivex.rxkotlin.subscribeBy -import io.reactivex.schedulers.Schedulers import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import org.commonmark.parser.Parser -import org.commonmark.renderer.html.HtmlRenderer -import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixPatterns import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.query.QueryStringValue @@ -80,30 +80,23 @@ import org.matrix.android.sdk.api.session.events.model.isTextMessage import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.file.FileService +import org.matrix.android.sdk.api.session.initsync.SyncStatusService import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState import org.matrix.android.sdk.api.session.room.members.roomMemberQueryParams import org.matrix.android.sdk.api.session.room.model.Membership -import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary -import org.matrix.android.sdk.api.session.room.model.message.MessageType -import org.matrix.android.sdk.api.session.room.model.message.OptionItem import org.matrix.android.sdk.api.session.room.model.message.getFileUrl import org.matrix.android.sdk.api.session.room.model.tombstone.RoomTombstoneContent import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper import org.matrix.android.sdk.api.session.room.read.ReadService -import org.matrix.android.sdk.api.session.room.send.UserDraft import org.matrix.android.sdk.api.session.room.timeline.Timeline import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent -import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent -import org.matrix.android.sdk.api.session.room.timeline.getRelationContent -import org.matrix.android.sdk.api.session.room.timeline.getTextEditableContent -import org.matrix.android.sdk.api.session.space.CreateSpaceParams import org.matrix.android.sdk.api.session.widgets.model.WidgetType import org.matrix.android.sdk.api.util.toOptional +import org.matrix.android.sdk.flow.flow +import org.matrix.android.sdk.flow.unwrap import org.matrix.android.sdk.internal.crypto.model.event.WithHeldCode -import org.matrix.android.sdk.rx.rx -import org.matrix.android.sdk.rx.unwrap import timber.log.Timber import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicBoolean @@ -111,8 +104,8 @@ import java.util.concurrent.atomic.AtomicBoolean class RoomDetailViewModel @AssistedInject constructor( @Assisted private val initialState: RoomDetailViewState, private val vectorPreferences: VectorPreferences, + private val vectorDataStore: VectorDataStore, private val stringProvider: StringProvider, - private val rainbowGenerator: RainbowGenerator, private val session: Session, private val supportedVerificationMethodsProvider: SupportedVerificationMethodsProvider, private val stickerPickerActionHandler: StickerPickerActionHandler, @@ -132,7 +125,7 @@ class RoomDetailViewModel @AssistedInject constructor( private val eventId = initialState.eventId private val invisibleEventsObservable = BehaviorRelay.create() private val visibleEventsObservable = BehaviorRelay.create() - private var timelineEvents = PublishRelay.create>() + private var timelineEvents = MutableSharedFlow>(0) val timeline = timelineFactory.createTimeline(viewModelScope, room, eventId) // Same lifecycle than the ViewModel (survive to screen rotation) @@ -154,14 +147,16 @@ class RoomDetailViewModel @AssistedInject constructor( fun create(initialState: RoomDetailViewState): RoomDetailViewModel } - companion object : MvRxViewModelFactory { + /** + * Can't use the hiltMaverick here because some dependencies are injected here and in fragment but they don't share the graph. + */ + companion object : MavericksViewModelFactory { const val PAGINATION_COUNT = 50 @JvmStatic - override fun create(viewModelContext: ViewModelContext, state: RoomDetailViewState): RoomDetailViewModel? { + override fun create(viewModelContext: ViewModelContext, state: RoomDetailViewState): RoomDetailViewModel { val fragment: RoomDetailFragment = (viewModelContext as FragmentViewModelContext).fragment() - return fragment.roomDetailViewModelFactory.create(state) } } @@ -174,8 +169,8 @@ class RoomDetailViewModel @AssistedInject constructor( observeSummaryState() getUnreadState() observeSyncState() + observeDataStore() observeEventDisplayedActions() - loadDraftIfAny() observeUnreadState() observeMyRoomMember() observeActiveRoomWidgets() @@ -198,6 +193,18 @@ class RoomDetailViewModel @AssistedInject constructor( } } + private fun observeDataStore() { + viewModelScope.launch { + vectorDataStore.pushCounterFlow.collect { nbOfPush -> + setState { + copy( + pushCounter = nbOfPush + ) + } + } + } + } + private fun prepareForEncryption() { // check if there is not already a call made, or if there has been an error if (prepareToEncrypt.shouldLoad) { @@ -215,26 +222,23 @@ class RoomDetailViewModel @AssistedInject constructor( } private fun observePowerLevel() { - PowerLevelsObservableFactory(room).createObservable() - .subscribe { - val canSendMessage = PowerLevelsHelper(it).isUserAllowedToSend(session.myUserId, false, EventType.MESSAGE) + PowerLevelsFlowFactory(room).createFlow() + .onEach { val canInvite = PowerLevelsHelper(it).isUserAbleToInvite(session.myUserId) val isAllowedToManageWidgets = session.widgetService().hasPermissionsToHandleWidgets(room.roomId) val isAllowedToStartWebRTCCall = PowerLevelsHelper(it).isUserAllowedToSend(session.myUserId, false, EventType.CALL_INVITE) setState { copy( - canSendMessage = canSendMessage, canInvite = canInvite, isAllowedToManageWidgets = isAllowedToManageWidgets, isAllowedToStartWebRTCCall = isAllowedToStartWebRTCCall ) } - } - .disposeOnClear() + }.launchIn(viewModelScope) } private fun observeActiveRoomWidgets() { - session.rx() + session.flow() .liveRoomWidgets( roomId = initialState.roomId, widgetId = QueryStringValue.NoCondition @@ -246,7 +250,7 @@ class RoomDetailViewModel @AssistedInject constructor( copy(activeRoomWidgets = widgets) } - asyncSubscribe(RoomDetailViewState::activeRoomWidgets) { widgets -> + onAsync(RoomDetailViewState::activeRoomWidgets) { widgets -> setState { val jitsiWidget = widgets.firstOrNull { it.type == WidgetType.Jitsi } val jitsiConfId = jitsiWidget?.let { @@ -267,7 +271,7 @@ class RoomDetailViewModel @AssistedInject constructor( val queryParams = roomMemberQueryParams { this.userId = QueryStringValue.Equals(session.myUserId, QueryStringValue.Case.SENSITIVE) } - room.rx() + room.flow() .liveRoomMembers(queryParams) .map { it.firstOrNull().toOptional() @@ -282,10 +286,7 @@ class RoomDetailViewModel @AssistedInject constructor( override fun handle(action: RoomDetailAction) { when (action) { - is RoomDetailAction.UserIsTyping -> handleUserIsTyping(action) is RoomDetailAction.ComposerFocusChange -> handleComposerFocusChange(action) - is RoomDetailAction.SaveDraft -> handleSaveDraft(action) - is RoomDetailAction.SendMessage -> handleSendMessage(action) is RoomDetailAction.SendMedia -> handleSendMedia(action) is RoomDetailAction.SendSticker -> handleSendSticker(action) is RoomDetailAction.TimelineEventTurnsVisible -> handleEventVisible(action) @@ -297,10 +298,6 @@ class RoomDetailViewModel @AssistedInject constructor( is RoomDetailAction.RedactAction -> handleRedactEvent(action) is RoomDetailAction.UndoReaction -> handleUndoReact(action) is RoomDetailAction.UpdateQuickReactAction -> handleUpdateQuickReaction(action) - is RoomDetailAction.EnterRegularMode -> handleEnterRegularMode(action) - is RoomDetailAction.EnterEditMode -> handleEditAction(action) - is RoomDetailAction.EnterQuoteMode -> handleQuoteAction(action) - is RoomDetailAction.EnterReplyMode -> handleReplyAction(action) is RoomDetailAction.DownloadOrOpen -> handleOpenOrDownloadFile(action) is RoomDetailAction.NavigateToEvent -> handleNavigateToEvent(action) is RoomDetailAction.JoinAndOpenReplacementRoom -> handleJoinAndOpenReplacementRoom() @@ -350,7 +347,7 @@ class RoomDetailViewModel @AssistedInject constructor( is RoomDetailAction.PlayOrPauseVoicePlayback -> handlePlayOrPauseVoicePlayback(action) RoomDetailAction.PauseRecordingVoiceMessage -> handlePauseRecordingVoiceMessage() RoomDetailAction.PlayOrPauseRecordingPlayback -> handlePlayOrPauseRecordingPlayback() - RoomDetailAction.EndAllVoiceActions -> handleEndAllVoiceActions() + is RoomDetailAction.EndAllVoiceActions -> handleEndAllVoiceActions(action.deleteRecord) is RoomDetailAction.RoomUpgradeSuccess -> { setState { copy(joinUpgradedRoomAsync = Success(action.replacementRoomId)) @@ -532,11 +529,11 @@ class RoomDetailViewModel @AssistedInject constructor( val widget = action.widget val domain = action.widget.widgetContent.data["domain"] as? String ?: "" val isAllowed = action.userJustAccepted || if (widget.type == WidgetType.Jitsi) { - widget.senderInfo?.userId == session.myUserId - || session.integrationManagerService().isNativeWidgetDomainAllowed( - action.widget.type.preferred, - domain - ) + widget.senderInfo?.userId == session.myUserId || + session.integrationManagerService().isNativeWidgetDomainAllowed( + action.widget.type.preferred, + domain + ) } else false if (isAllowed) { @@ -572,70 +569,6 @@ class RoomDetailViewModel @AssistedInject constructor( return room.getRoomMember(userId) } - /** - * Convert a send mode to a draft and save the draft - */ - private fun handleSaveDraft(action: RoomDetailAction.SaveDraft) = withState { - session.coroutineScope.launch { - when { - it.sendMode is SendMode.REGULAR && !it.sendMode.fromSharing -> { - setState { copy(sendMode = it.sendMode.copy(action.draft)) } - room.saveDraft(UserDraft.REGULAR(action.draft)) - } - it.sendMode is SendMode.REPLY -> { - setState { copy(sendMode = it.sendMode.copy(text = action.draft)) } - room.saveDraft(UserDraft.REPLY(it.sendMode.timelineEvent.root.eventId!!, action.draft)) - } - it.sendMode is SendMode.QUOTE -> { - setState { copy(sendMode = it.sendMode.copy(text = action.draft)) } - room.saveDraft(UserDraft.QUOTE(it.sendMode.timelineEvent.root.eventId!!, action.draft)) - } - it.sendMode is SendMode.EDIT -> { - setState { copy(sendMode = it.sendMode.copy(text = action.draft)) } - room.saveDraft(UserDraft.EDIT(it.sendMode.timelineEvent.root.eventId!!, action.draft)) - } - } - } - } - - private fun loadDraftIfAny() { - val currentDraft = room.getDraft() - setState { - copy( - // Create a sendMode from a draft and retrieve the TimelineEvent - sendMode = when (currentDraft) { - is UserDraft.REGULAR -> SendMode.REGULAR(currentDraft.text, false) - is UserDraft.QUOTE -> { - room.getTimeLineEvent(currentDraft.linkedEventId)?.let { timelineEvent -> - SendMode.QUOTE(timelineEvent, currentDraft.text) - } - } - is UserDraft.REPLY -> { - room.getTimeLineEvent(currentDraft.linkedEventId)?.let { timelineEvent -> - SendMode.REPLY(timelineEvent, currentDraft.text) - } - } - is UserDraft.EDIT -> { - room.getTimeLineEvent(currentDraft.linkedEventId)?.let { timelineEvent -> - SendMode.EDIT(timelineEvent, currentDraft.text) - } - } - else -> null - } ?: SendMode.REGULAR("", fromSharing = false) - ) - } - } - - private fun handleUserIsTyping(action: RoomDetailAction.UserIsTyping) { - if (vectorPreferences.sendTypingNotifs()) { - if (action.isTyping) { - room.userIsTyping() - } else { - room.userStopsTyping() - } - } - } - private fun handleComposerFocusChange(action: RoomDetailAction.ComposerFocusChange) { // Ensure outbound session keys if (OutboundSessionKeySharingStrategy.WhenTyping == BuildConfig.outboundSessionKeySharingStrategy && room.isEncrypted()) { @@ -720,8 +653,8 @@ class RoomDetailViewModel @AssistedInject constructor( voiceMessageHelper.startOrPauseRecordingPlayback() } - private fun handleEndAllVoiceActions() { - voiceMessageHelper.stopAllVoiceActions() + private fun handleEndAllVoiceActions(deleteRecord: Boolean) { + voiceMessageHelper.stopAllVoiceActions(deleteRecord) } private fun handlePauseRecordingVoiceMessage() { @@ -748,417 +681,7 @@ class RoomDetailViewModel @AssistedInject constructor( } } -// PRIVATE METHODS ***************************************************************************** - - private fun handleSendMessage(action: RoomDetailAction.SendMessage) { - withState { state -> - when (state.sendMode) { - is SendMode.REGULAR -> { - when (val slashCommandResult = CommandParser.parseSplashCommand(action.text)) { - is ParsedCommand.ErrorNotACommand -> { - // Send the text message to the room - room.sendTextMessage(action.text, autoMarkdown = action.autoMarkdown) - _viewEvents.post(RoomDetailViewEvents.MessageSent) - popDraft() - } - is ParsedCommand.ErrorSyntax -> { - _viewEvents.post(RoomDetailViewEvents.SlashCommandError(slashCommandResult.command)) - } - is ParsedCommand.ErrorEmptySlashCommand -> { - _viewEvents.post(RoomDetailViewEvents.SlashCommandUnknown("/")) - } - is ParsedCommand.ErrorUnknownSlashCommand -> { - _viewEvents.post(RoomDetailViewEvents.SlashCommandUnknown(slashCommandResult.slashCommand)) - } - is ParsedCommand.SendPlainText -> { - // Send the text message to the room, without markdown - room.sendTextMessage(slashCommandResult.message, autoMarkdown = false) - _viewEvents.post(RoomDetailViewEvents.MessageSent) - popDraft() - } - is ParsedCommand.Invite -> { - handleInviteSlashCommand(slashCommandResult) - popDraft() - } - is ParsedCommand.Invite3Pid -> { - handleInvite3pidSlashCommand(slashCommandResult) - popDraft() - } - is ParsedCommand.SetUserPowerLevel -> { - handleSetUserPowerLevel(slashCommandResult) - popDraft() - } - is ParsedCommand.ClearScalarToken -> { - // TODO - _viewEvents.post(RoomDetailViewEvents.SlashCommandNotImplemented) - } - is ParsedCommand.SetMarkdown -> { - vectorPreferences.setMarkdownEnabled(slashCommandResult.enable) - _viewEvents.post(RoomDetailViewEvents.SlashCommandHandled( - if (slashCommandResult.enable) R.string.markdown_has_been_enabled else R.string.markdown_has_been_disabled)) - popDraft() - } - is ParsedCommand.UnbanUser -> { - handleUnbanSlashCommand(slashCommandResult) - popDraft() - } - is ParsedCommand.BanUser -> { - handleBanSlashCommand(slashCommandResult) - popDraft() - } - is ParsedCommand.KickUser -> { - handleKickSlashCommand(slashCommandResult) - popDraft() - } - is ParsedCommand.JoinRoom -> { - handleJoinToAnotherRoomSlashCommand(slashCommandResult) - popDraft() - } - is ParsedCommand.PartRoom -> { - // TODO - _viewEvents.post(RoomDetailViewEvents.SlashCommandNotImplemented) - } - is ParsedCommand.SendEmote -> { - room.sendTextMessage(slashCommandResult.message, msgType = MessageType.MSGTYPE_EMOTE, autoMarkdown = action.autoMarkdown) - _viewEvents.post(RoomDetailViewEvents.SlashCommandHandled()) - popDraft() - } - is ParsedCommand.SendRainbow -> { - slashCommandResult.message.toString().let { - room.sendFormattedTextMessage(it, rainbowGenerator.generate(it)) - } - _viewEvents.post(RoomDetailViewEvents.SlashCommandHandled()) - popDraft() - } - is ParsedCommand.SendRainbowEmote -> { - slashCommandResult.message.toString().let { - room.sendFormattedTextMessage(it, rainbowGenerator.generate(it), MessageType.MSGTYPE_EMOTE) - } - _viewEvents.post(RoomDetailViewEvents.SlashCommandHandled()) - popDraft() - } - is ParsedCommand.SendSpoiler -> { - room.sendFormattedTextMessage( - "[${stringProvider.getString(R.string.spoiler)}](${slashCommandResult.message})", - "${slashCommandResult.message}" - ) - _viewEvents.post(RoomDetailViewEvents.SlashCommandHandled()) - popDraft() - } - is ParsedCommand.SendShrug -> { - val sequence = buildString { - append("¯\\_(ツ)_/¯") - if (slashCommandResult.message.isNotEmpty()) { - append(" ") - append(slashCommandResult.message) - } - } - room.sendTextMessage(sequence) - _viewEvents.post(RoomDetailViewEvents.SlashCommandHandled()) - popDraft() - } - is ParsedCommand.SendChatEffect -> { - sendChatEffect(slashCommandResult) - _viewEvents.post(RoomDetailViewEvents.SlashCommandHandled()) - popDraft() - } - is ParsedCommand.SendPoll -> { - room.sendPoll(slashCommandResult.question, slashCommandResult.options.mapIndexed { index, s -> OptionItem(s, "$index. $s") }) - _viewEvents.post(RoomDetailViewEvents.SlashCommandHandled()) - popDraft() - } - is ParsedCommand.ChangeTopic -> { - handleChangeTopicSlashCommand(slashCommandResult) - popDraft() - } - is ParsedCommand.ChangeDisplayName -> { - handleChangeDisplayNameSlashCommand(slashCommandResult) - popDraft() - } - is ParsedCommand.DiscardSession -> { - if (room.isEncrypted()) { - session.cryptoService().discardOutboundSession(room.roomId) - _viewEvents.post(RoomDetailViewEvents.SlashCommandHandled()) - popDraft() - } else { - _viewEvents.post(RoomDetailViewEvents.SlashCommandHandled()) - _viewEvents.post( - RoomDetailViewEvents - .ShowMessage(stringProvider.getString(R.string.command_description_discard_session_not_handled)) - ) - } - } - is ParsedCommand.CreateSpace -> { - viewModelScope.launch(Dispatchers.IO) { - try { - val params = CreateSpaceParams().apply { - name = slashCommandResult.name - invitedUserIds.addAll(slashCommandResult.invitees) - } - val spaceId = session.spaceService().createSpace(params) - session.spaceService().getSpace(spaceId) - ?.addChildren( - state.roomId, - null, - null, - true - ) - } catch (failure: Throwable) { - _viewEvents.post(RoomDetailViewEvents.SlashCommandResultError(failure)) - } - } - _viewEvents.post(RoomDetailViewEvents.SlashCommandHandled()) - popDraft() - } - is ParsedCommand.AddToSpace -> { - viewModelScope.launch(Dispatchers.IO) { - try { - session.spaceService().getSpace(slashCommandResult.spaceId) - ?.addChildren( - room.roomId, - null, - null, - false - ) - } catch (failure: Throwable) { - _viewEvents.post(RoomDetailViewEvents.SlashCommandResultError(failure)) - } - } - _viewEvents.post(RoomDetailViewEvents.SlashCommandHandled()) - popDraft() - } - is ParsedCommand.JoinSpace -> { - viewModelScope.launch(Dispatchers.IO) { - try { - session.spaceService().joinSpace(slashCommandResult.spaceIdOrAlias) - } catch (failure: Throwable) { - _viewEvents.post(RoomDetailViewEvents.SlashCommandResultError(failure)) - } - } - _viewEvents.post(RoomDetailViewEvents.SlashCommandHandled()) - popDraft() - } - is ParsedCommand.LeaveRoom -> { - viewModelScope.launch(Dispatchers.IO) { - try { - session.getRoom(slashCommandResult.roomId)?.leave(null) - } catch (failure: Throwable) { - _viewEvents.post(RoomDetailViewEvents.SlashCommandResultError(failure)) - } - } - _viewEvents.post(RoomDetailViewEvents.SlashCommandHandled()) - popDraft() - } - is ParsedCommand.UpgradeRoom -> { - _viewEvents.post( - RoomDetailViewEvents.ShowRoomUpgradeDialog( - slashCommandResult.newVersion, - room.roomSummary()?.isPublic ?: false - ) - ) - _viewEvents.post(RoomDetailViewEvents.SlashCommandHandled()) - popDraft() - } - }.exhaustive - } - is SendMode.EDIT -> { - // is original event a reply? - val inReplyTo = state.sendMode.timelineEvent.getRelationContent()?.inReplyTo?.eventId - if (inReplyTo != null) { - // TODO check if same content? - room.getTimeLineEvent(inReplyTo)?.let { - room.editReply(state.sendMode.timelineEvent, it, action.text.toString()) - } - } else { - val messageContent = state.sendMode.timelineEvent.getLastMessageContent() - val existingBody = messageContent?.body ?: "" - if (existingBody != action.text) { - room.editTextMessage(state.sendMode.timelineEvent, - messageContent?.msgType ?: MessageType.MSGTYPE_TEXT, - action.text, - action.autoMarkdown) - } else { - Timber.w("Same message content, do not send edition") - } - } - _viewEvents.post(RoomDetailViewEvents.MessageSent) - popDraft() - } - is SendMode.QUOTE -> { - val messageContent = state.sendMode.timelineEvent.getLastMessageContent() - val textMsg = messageContent?.body - - val finalText = legacyRiotQuoteText(textMsg, action.text.toString()) - - // TODO check for pills? - - // TODO Refactor this, just temporary for quotes - val parser = Parser.builder().build() - val document = parser.parse(finalText) - val renderer = HtmlRenderer.builder().build() - val htmlText = renderer.render(document) - if (finalText == htmlText) { - room.sendTextMessage(finalText) - } else { - room.sendFormattedTextMessage(finalText, htmlText) - } - _viewEvents.post(RoomDetailViewEvents.MessageSent) - popDraft() - } - is SendMode.REPLY -> { - state.sendMode.timelineEvent.let { - room.replyToMessage(it, action.text.toString(), action.autoMarkdown) - _viewEvents.post(RoomDetailViewEvents.MessageSent) - popDraft() - } - } - }.exhaustive - } - } - - private fun sendChatEffect(sendChatEffect: ParsedCommand.SendChatEffect) { - // If message is blank, convert to an emote, with default message - if (sendChatEffect.message.isBlank()) { - val defaultMessage = stringProvider.getString(when (sendChatEffect.chatEffect) { - ChatEffect.CONFETTI -> R.string.default_message_emote_confetti - ChatEffect.SNOWFALL -> R.string.default_message_emote_snow - }) - room.sendTextMessage(defaultMessage, MessageType.MSGTYPE_EMOTE) - } else { - room.sendTextMessage(sendChatEffect.message, sendChatEffect.chatEffect.toMessageType()) - } - } - - private fun popDraft() = withState { - if (it.sendMode is SendMode.REGULAR && it.sendMode.fromSharing) { - // If we were sharing, we want to get back our last value from draft - loadDraftIfAny() - } else { - // Otherwise we clear the composer and remove the draft from db - setState { copy(sendMode = SendMode.REGULAR("", false)) } - viewModelScope.launch { - room.deleteDraft() - } - } - } - - private fun handleJoinToAnotherRoomSlashCommand(command: ParsedCommand.JoinRoom) { - viewModelScope.launch { - try { - session.joinRoom(command.roomAlias, command.reason, emptyList()) - } catch (failure: Throwable) { - _viewEvents.post(RoomDetailViewEvents.SlashCommandResultError(failure)) - return@launch - } - session.getRoomSummary(command.roomAlias) - ?.roomId - ?.let { - _viewEvents.post(RoomDetailViewEvents.JoinRoomCommandSuccess(it)) - } - } - } - - private fun legacyRiotQuoteText(quotedText: String?, myText: String): String { - val messageParagraphs = quotedText?.split("\n\n".toRegex())?.dropLastWhile { it.isEmpty() }?.toTypedArray() - return buildString { - if (messageParagraphs != null) { - for (i in messageParagraphs.indices) { - if (messageParagraphs[i].isNotBlank()) { - append("> ") - append(messageParagraphs[i]) - } - - if (i != messageParagraphs.lastIndex) { - append("\n\n") - } - } - } - append("\n\n") - append(myText) - } - } - - private fun handleChangeTopicSlashCommand(changeTopic: ParsedCommand.ChangeTopic) { - launchSlashCommandFlowSuspendable { - room.updateTopic(changeTopic.topic) - } - } - - private fun handleInviteSlashCommand(invite: ParsedCommand.Invite) { - launchSlashCommandFlowSuspendable { - room.invite(invite.userId, invite.reason) - } - } - - private fun handleInvite3pidSlashCommand(invite: ParsedCommand.Invite3Pid) { - launchSlashCommandFlowSuspendable { - room.invite3pid(invite.threePid) - } - } - - private fun handleSetUserPowerLevel(setUserPowerLevel: ParsedCommand.SetUserPowerLevel) { - val newPowerLevelsContent = room.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS) - ?.content - ?.toModel() - ?.setUserPowerLevel(setUserPowerLevel.userId, setUserPowerLevel.powerLevel) - ?.toContent() - ?: return - - launchSlashCommandFlowSuspendable { - room.sendStateEvent(EventType.STATE_ROOM_POWER_LEVELS, null, newPowerLevelsContent) - } - } - - private fun handleChangeDisplayNameSlashCommand(changeDisplayName: ParsedCommand.ChangeDisplayName) { - launchSlashCommandFlowSuspendable { - session.setDisplayName(session.myUserId, changeDisplayName.displayName) - } - } - - private fun handleKickSlashCommand(kick: ParsedCommand.KickUser) { - launchSlashCommandFlowSuspendable { - room.kick(kick.userId, kick.reason) - } - } - - private fun handleBanSlashCommand(ban: ParsedCommand.BanUser) { - launchSlashCommandFlowSuspendable { - room.ban(ban.userId, ban.reason) - } - } - - private fun handleUnbanSlashCommand(unban: ParsedCommand.UnbanUser) { - launchSlashCommandFlowSuspendable { - room.unban(unban.userId, unban.reason) - } - } - - private fun launchSlashCommandFlow(lambda: (MatrixCallback) -> Unit) { - _viewEvents.post(RoomDetailViewEvents.SlashCommandHandled()) - val matrixCallback = object : MatrixCallback { - override fun onSuccess(data: Unit) { - _viewEvents.post(RoomDetailViewEvents.SlashCommandResultOk) - } - - override fun onFailure(failure: Throwable) { - _viewEvents.post(RoomDetailViewEvents.SlashCommandResultError(failure)) - } - } - lambda.invoke(matrixCallback) - } - - private fun launchSlashCommandFlowSuspendable(block: suspend () -> Unit) { - _viewEvents.post(RoomDetailViewEvents.SlashCommandHandled()) - viewModelScope.launch { - val event = try { - block() - RoomDetailViewEvents.SlashCommandResultOk - } catch (failure: Exception) { - RoomDetailViewEvents.SlashCommandResultError(failure) - } - _viewEvents.post(event) - } - } + // PRIVATE METHODS ***************************************************************************** private fun handleSendReaction(action: RoomDetailAction.SendReaction) { room.sendReaction(action.targetEventId, action.reaction) @@ -1228,32 +751,10 @@ class RoomDetailViewModel @AssistedInject constructor( } } - private fun handleEditAction(action: RoomDetailAction.EnterEditMode) { - room.getTimeLineEvent(action.eventId)?.let { timelineEvent -> - setState { copy(sendMode = SendMode.EDIT(timelineEvent, timelineEvent.getTextEditableContent() ?: "")) } - } - } - - private fun handleQuoteAction(action: RoomDetailAction.EnterQuoteMode) { - room.getTimeLineEvent(action.eventId)?.let { timelineEvent -> - setState { copy(sendMode = SendMode.QUOTE(timelineEvent, action.text)) } - } - } - - private fun handleReplyAction(action: RoomDetailAction.EnterReplyMode) { - room.getTimeLineEvent(action.eventId)?.let { timelineEvent -> - setState { copy(sendMode = SendMode.REPLY(timelineEvent, action.text)) } - } - } - - private fun handleEnterRegularMode(action: RoomDetailAction.EnterRegularMode) = setState { - copy(sendMode = SendMode.REGULAR(action.text, action.fromSharing)) - } - private fun handleOpenOrDownloadFile(action: RoomDetailAction.DownloadOrOpen) { val mxcUrl = action.messageFileContent.getFileUrl() ?: return - val isLocalSendingFile = action.senderId == session.myUserId - && mxcUrl.startsWith("content://") + val isLocalSendingFile = action.senderId == session.myUserId && + mxcUrl.startsWith("content://") if (isLocalSendingFile) { tryOrNull { Uri.parse(mxcUrl) }?.let { _viewEvents.post(RoomDetailViewEvents.OpenFile( @@ -1485,18 +986,22 @@ class RoomDetailViewModel @AssistedInject constructor( } private fun observeSyncState() { - session.rx() + session.flow() .liveSyncState() - .subscribe { syncState -> - setState { - copy(syncState = syncState) - } + .setOnEach { syncState -> + copy(syncState = syncState) + } + + session.getSyncStatusLive() + .asFlow() + .filterIsInstance() + .setOnEach { + copy(incrementalSyncStatus = it) } - .disposeOnClear() } private fun observeRoomSummary() { - room.rx().liveRoomSummary() + room.flow().liveRoomSummary() .unwrap() .execute { async -> copy( @@ -1506,14 +1011,12 @@ class RoomDetailViewModel @AssistedInject constructor( } private fun getUnreadState() { - Observable - .combineLatest, RoomSummary, UnreadState>( - timelineEvents.observeOn(Schedulers.computation()), - room.rx().liveRoomSummary().unwrap(), - { timelineEvents, roomSummary -> - computeUnreadState(timelineEvents, roomSummary) - } - ) + combine( + timelineEvents, + room.flow().liveRoomSummary().unwrap() + ) { timelineEvents, roomSummary -> + computeUnreadState(timelineEvents, roomSummary) + } // We don't want live update of unread so we skip when we already had a HasUnread or HasNoUnread .distinctUntilChanged { previous, current -> when { @@ -1522,10 +1025,9 @@ class RoomDetailViewModel @AssistedInject constructor( else -> false } } - .subscribe { - setState { copy(unreadState = it) } + .setOnEach { + copy(unreadState = it) } - .disposeOnClear() } private fun computeUnreadState(events: List, roomSummary: RoomSummary): UnreadState { @@ -1549,7 +1051,7 @@ class RoomDetailViewModel @AssistedInject constructor( } private fun observeUnreadState() { - selectSubscribe(RoomDetailViewState::unreadState) { + onEach(RoomDetailViewState::unreadState) { Timber.v("Unread state: $it") if (it is UnreadState.HasNoUnread) { startTrackingUnreadMessages() @@ -1558,24 +1060,23 @@ class RoomDetailViewModel @AssistedInject constructor( } private fun observeMembershipChanges() { - session.rx() + session.flow() .liveRoomChangeMembershipState() .map { it[initialState.roomId] ?: ChangeMembershipState.Unknown } .distinctUntilChanged() - .subscribe { - setState { copy(changeMembershipState = it) } + .setOnEach { + copy(changeMembershipState = it) } - .disposeOnClear() } private fun observeSummaryState() { - asyncSubscribe(RoomDetailViewState::asyncRoomSummary) { summary -> + onAsync(RoomDetailViewState::asyncRoomSummary) { summary -> setState { val typingMessage = typingHelper.getTypingMessage(summary.typingUsers) copy( - typingMessage = typingMessage, + formattedTypingUsers = typingMessage, hasFailedSending = summary.hasFailedSending ) } @@ -1593,8 +1094,10 @@ class RoomDetailViewModel @AssistedInject constructor( } override fun onTimelineUpdated(snapshot: List) { - timelineEvents.accept(snapshot) - + viewModelScope.launch { + // tryEmit doesn't work with SharedFlow without cache + timelineEvents.emit(snapshot) + } // PreviewUrl if (vectorPreferences.showUrlPreviews()) { withState { state -> 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 1c75429d11..042a415b47 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 @@ -17,39 +17,18 @@ package im.vector.app.features.home.room.detail import com.airbnb.mvrx.Async -import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.Uninitialized import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.initsync.SyncStatusService import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary -import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.sync.SyncState import org.matrix.android.sdk.api.session.widgets.model.Widget import org.matrix.android.sdk.api.session.widgets.model.WidgetType -/** - * Describes the current send mode: - * REGULAR: sends the text as a regular message - * QUOTE: User is currently quoting a message - * EDIT: User is currently editing an existing message - * - * Depending on the state the bottom toolbar will change (icons/preview/actions...) - */ -sealed class SendMode(open val text: String) { - data class REGULAR( - override val text: String, - val fromSharing: Boolean, - // This is necessary for forcing refresh on selectSubscribe - private val ts: Long = System.currentTimeMillis() - ) : SendMode(text) - - data class QUOTE(val timelineEvent: TimelineEvent, override val text: String) : SendMode(text) - data class EDIT(val timelineEvent: TimelineEvent, override val text: String) : SendMode(text) - data class REPLY(val timelineEvent: TimelineEvent, override val text: String) : SendMode(text) -} - sealed class UnreadState { object Unknown : UnreadState() object HasNoUnread : UnreadState() @@ -72,22 +51,22 @@ data class RoomDetailViewState( val asyncInviter: Async = Uninitialized, val asyncRoomSummary: Async = Uninitialized, val activeRoomWidgets: Async> = Uninitialized, - val typingMessage: String? = null, - val sendMode: SendMode = SendMode.REGULAR("", false), + val formattedTypingUsers: String? = null, val tombstoneEvent: Event? = null, val joinUpgradedRoomAsync: Async = Uninitialized, val syncState: SyncState = SyncState.Idle, + val incrementalSyncStatus: SyncStatusService.Status.IncrementalSyncStatus = SyncStatusService.Status.IncrementalSyncIdle, + val pushCounter: Int = 0, val highlightedEventId: String? = null, val unreadState: UnreadState = UnreadState.Unknown, val canShowJumpToReadMarker: Boolean = true, val changeMembershipState: ChangeMembershipState = ChangeMembershipState.Unknown, - val canSendMessage: Boolean = true, val canInvite: Boolean = true, val isAllowedToManageWidgets: Boolean = false, val isAllowedToStartWebRTCCall: Boolean = true, val hasFailedSending: Boolean = false, val jitsiState: JitsiState = JitsiState() -) : MvRxState { +) : MavericksState { constructor(args: RoomDetailArgs) : this( roomId = args.roomId, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/ComposerEditText.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/ComposerEditText.kt index 79ff7be441..232323789c 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/ComposerEditText.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/ComposerEditText.kt @@ -32,16 +32,15 @@ import im.vector.app.core.platform.SimpleTextWatcher import im.vector.app.features.html.PillImageSpan import timber.log.Timber -class ComposerEditText @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = android.R.attr.editTextStyle) - : EmojiEditText(context, attrs, defStyleAttr) { +class ComposerEditText @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = android.R.attr.editTextStyle) : + EmojiEditText(context, attrs, defStyleAttr) { interface Callback { fun onRichContentSelected(contentUri: Uri): Boolean - fun onTextBlankStateChanged(isBlank: Boolean) + fun onTextChanged(text: CharSequence) } var callback: Callback? = null - private var isBlankText = true override fun onCreateInputConnection(editorInfo: EditorInfo): InputConnection? { val ic = super.onCreateInputConnection(editorInfo) ?: return null @@ -95,11 +94,7 @@ class ComposerEditText @JvmOverloads constructor(context: Context, attrs: Attrib } spanToRemove = null } - // Report blank status of EditText to be able to arrange other elements of the composer - if (s.isBlank() != isBlankText) { - isBlankText = !isBlankText - callback?.onTextBlankStateChanged(isBlankText) - } + callback?.onTextChanged(s.toString()) } } ) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerAction.kt new file mode 100644 index 0000000000..7725400187 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerAction.kt @@ -0,0 +1,31 @@ +/* + * 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.composer + +import im.vector.app.core.platform.VectorViewModelAction + +sealed class TextComposerAction : VectorViewModelAction { + data class SaveDraft(val draft: String) : TextComposerAction() + data class SendMessage(val text: CharSequence, val autoMarkdown: Boolean) : TextComposerAction() + data class EnterEditMode(val eventId: String, val text: String) : TextComposerAction() + data class EnterQuoteMode(val eventId: String, val text: String) : TextComposerAction() + data class EnterReplyMode(val eventId: String, val text: String) : TextComposerAction() + data class EnterRegularMode(val text: String, val fromSharing: Boolean) : TextComposerAction() + data class UserIsTyping(val isTyping: Boolean) : TextComposerAction() + data class OnTextChanged(val text: CharSequence) : TextComposerAction() + data class OnVoiceRecordingStateChanged(val isRecording: Boolean) : TextComposerAction() +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerView.kt index eb935f9e75..34b3c1777c 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerView.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerView.kt @@ -24,17 +24,14 @@ import android.view.ViewGroup import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet import androidx.core.text.toSpannable -import androidx.core.view.isInvisible -import androidx.core.view.isVisible -import androidx.transition.AutoTransition import androidx.transition.ChangeBounds import androidx.transition.Fade import androidx.transition.Transition import androidx.transition.TransitionManager import androidx.transition.TransitionSet import im.vector.app.R +import im.vector.app.core.extensions.setTextIfDifferent import im.vector.app.databinding.ComposerLayoutBinding -import org.matrix.android.sdk.api.extensions.orFalse /** * Encapsulate the timeline composer UX. @@ -61,13 +58,6 @@ class TextComposerView @JvmOverloads constructor( val text: Editable? get() = views.composerEditText.text - var alwaysShowSendButton = false - set(value) { - field = value - val shouldShowSendButton = currentConstraintSetId == R.layout.composer_layout_constraint_set_expanded || text?.isNotBlank().orFalse() || value - views.sendButton.isInvisible = !shouldShowSendButton - } - init { inflate(context, R.layout.composer_layout, this) views = ComposerLayoutBinding.bind(this) @@ -79,17 +69,8 @@ class TextComposerView @JvmOverloads constructor( return callback?.onRichContentSelected(contentUri) ?: false } - override fun onTextBlankStateChanged(isBlank: Boolean) { - val shouldShowSendButton = currentConstraintSetId == R.layout.composer_layout_constraint_set_expanded || !isBlank || alwaysShowSendButton - TransitionManager.endTransitions(this@TextComposerView) - if (views.sendButton.isVisible != shouldShowSendButton) { - TransitionManager.beginDelayedTransition( - this@TextComposerView, - AutoTransition().also { it.duration = 150 } - ) - views.sendButton.isInvisible = !shouldShowSendButton - } - callback?.onTextBlankStateChanged(isBlank) + override fun onTextChanged(text: CharSequence) { + callback?.onTextChanged(text) } } views.composerRelatedMessageCloseButton.setOnClickListener { @@ -114,9 +95,6 @@ class TextComposerView @JvmOverloads constructor( } currentConstraintSetId = R.layout.composer_layout_constraint_set_compact applyNewConstraintSet(animate, transitionComplete) - - val shouldShowSendButton = !views.composerEditText.text.isNullOrEmpty() || alwaysShowSendButton - views.sendButton.isInvisible = !shouldShowSendButton } fun expand(animate: Boolean = true, transitionComplete: (() -> Unit)? = null) { @@ -126,10 +104,14 @@ class TextComposerView @JvmOverloads constructor( } currentConstraintSetId = R.layout.composer_layout_constraint_set_expanded applyNewConstraintSet(animate, transitionComplete) - views.sendButton.isInvisible = false + } + + fun setTextIfDifferent(text: CharSequence?): Boolean { + return views.composerEditText.setTextIfDifferent(text) } private fun applyNewConstraintSet(animate: Boolean, transitionComplete: (() -> Unit)?) { + // val wasSendButtonInvisible = views.sendButton.isInvisible if (animate) { configureAndBeginTransition(transitionComplete) } @@ -137,6 +119,8 @@ class TextComposerView @JvmOverloads constructor( it.clone(context, currentConstraintSetId) it.applyTo(this) } + // Might be updated by view state just after, but avoid blinks + // views.sendButton.isInvisible = wasSendButtonInvisible } private fun configureAndBeginTransition(transitionComplete: (() -> Unit)? = null) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewEvents.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewEvents.kt new file mode 100644 index 0000000000..ff4a09ad71 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewEvents.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.home.room.detail.composer + +import androidx.annotation.StringRes +import im.vector.app.core.platform.VectorViewEvents +import im.vector.app.features.command.Command + +sealed class TextComposerViewEvents : VectorViewEvents { + + data class AnimateSendButtonVisibility(val isVisible: Boolean) : TextComposerViewEvents() + + data class ShowMessage(val message: String) : TextComposerViewEvents() + + abstract class SendMessageResult : TextComposerViewEvents() + + object MessageSent : SendMessageResult() + data class JoinRoomCommandSuccess(val roomId: String) : SendMessageResult() + class SlashCommandError(val command: Command) : SendMessageResult() + class SlashCommandUnknown(val command: String) : SendMessageResult() + object SlashCommandLoading : SendMessageResult() + data class SlashCommandResultOk(@StringRes val messageRes: Int? = null) : SendMessageResult() + class SlashCommandResultError(val throwable: Throwable) : SendMessageResult() + + data class OpenRoomMemberProfile(val userId: String) : TextComposerViewEvents() + + // TODO Remove + object SlashCommandNotImplemented : SendMessageResult() + + data class ShowRoomUpgradeDialog(val newVersion: String, val isPublic: Boolean) : TextComposerViewEvents() +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewModel.kt new file mode 100644 index 0000000000..e80f25de2f --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewModel.kt @@ -0,0 +1,717 @@ +/* + * 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.composer + +import com.airbnb.mvrx.MavericksViewModelFactory +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import im.vector.app.R +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory +import im.vector.app.core.extensions.exhaustive +import im.vector.app.core.platform.VectorViewModel +import im.vector.app.core.resources.StringProvider +import im.vector.app.features.command.CommandParser +import im.vector.app.features.command.ParsedCommand +import im.vector.app.features.home.room.detail.ChatEffect +import im.vector.app.features.home.room.detail.composer.rainbow.RainbowGenerator +import im.vector.app.features.home.room.detail.toMessageType +import im.vector.app.features.powerlevel.PowerLevelsFlowFactory +import im.vector.app.features.session.coroutineScope +import im.vector.app.features.settings.VectorPreferences +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.commonmark.parser.Parser +import org.commonmark.renderer.html.HtmlRenderer +import org.matrix.android.sdk.api.query.QueryStringValue +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent +import org.matrix.android.sdk.api.session.room.model.RoomAvatarContent +import org.matrix.android.sdk.api.session.room.model.RoomMemberContent +import org.matrix.android.sdk.api.session.room.model.message.MessageType +import org.matrix.android.sdk.api.session.room.model.message.OptionItem +import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper +import org.matrix.android.sdk.api.session.room.send.UserDraft +import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent +import org.matrix.android.sdk.api.session.room.timeline.getRelationContent +import org.matrix.android.sdk.api.session.room.timeline.getTextEditableContent +import org.matrix.android.sdk.api.session.space.CreateSpaceParams +import timber.log.Timber + +class TextComposerViewModel @AssistedInject constructor( + @Assisted initialState: TextComposerViewState, + private val session: Session, + private val stringProvider: StringProvider, + private val vectorPreferences: VectorPreferences, + private val rainbowGenerator: RainbowGenerator +) : VectorViewModel(initialState) { + + private val room = session.getRoom(initialState.roomId)!! + + // Keep it out of state to avoid invalidate being called + private var currentComposerText: CharSequence = "" + + init { + loadDraftIfAny() + observePowerLevel() + subscribeToStateInternal() + } + + override fun handle(action: TextComposerAction) { + Timber.v("Handle action: $action") + when (action) { + is TextComposerAction.EnterEditMode -> handleEnterEditMode(action) + is TextComposerAction.EnterQuoteMode -> handleEnterQuoteMode(action) + is TextComposerAction.EnterRegularMode -> handleEnterRegularMode(action) + is TextComposerAction.EnterReplyMode -> handleEnterReplyMode(action) + is TextComposerAction.SaveDraft -> handleSaveDraft(action) + is TextComposerAction.SendMessage -> handleSendMessage(action) + is TextComposerAction.UserIsTyping -> handleUserIsTyping(action) + is TextComposerAction.OnTextChanged -> handleOnTextChanged(action) + is TextComposerAction.OnVoiceRecordingStateChanged -> handleOnVoiceRecordingStateChanged(action) + } + } + + private fun handleOnVoiceRecordingStateChanged(action: TextComposerAction.OnVoiceRecordingStateChanged) = setState { + copy(isVoiceRecording = action.isRecording) + } + + private fun handleOnTextChanged(action: TextComposerAction.OnTextChanged) { + setState { + // Makes sure currentComposerText is upToDate when accessing further setState + currentComposerText = action.text + this + } + updateIsSendButtonVisibility(true) + } + + private fun subscribeToStateInternal() { + selectSubscribe(TextComposerViewState::sendMode, TextComposerViewState::canSendMessage, TextComposerViewState::isVoiceRecording) { _, _, _ -> + updateIsSendButtonVisibility(false) + } + } + + private fun updateIsSendButtonVisibility(triggerAnimation: Boolean) = setState { + val isSendButtonVisible = isComposerVisible && (sendMode !is SendMode.REGULAR || currentComposerText.isNotBlank()) + if (this.isSendButtonVisible != isSendButtonVisible && triggerAnimation) { + _viewEvents.post(TextComposerViewEvents.AnimateSendButtonVisibility(isSendButtonVisible)) + } + copy(isSendButtonVisible = isSendButtonVisible) + } + + private fun handleEnterRegularMode(action: TextComposerAction.EnterRegularMode) = setState { + copy(sendMode = SendMode.REGULAR(action.text, action.fromSharing)) + } + + private fun handleEnterEditMode(action: TextComposerAction.EnterEditMode) { + room.getTimeLineEvent(action.eventId)?.let { timelineEvent -> + setState { copy(sendMode = SendMode.EDIT(timelineEvent, timelineEvent.getTextEditableContent() ?: "")) } + } + } + + private fun observePowerLevel() { + PowerLevelsFlowFactory(room).createFlow() + .setOnEach { + val canSendMessage = PowerLevelsHelper(it).isUserAllowedToSend(session.myUserId, false, EventType.MESSAGE) + copy(canSendMessage = canSendMessage) + } + } + + private fun handleEnterQuoteMode(action: TextComposerAction.EnterQuoteMode) { + room.getTimeLineEvent(action.eventId)?.let { timelineEvent -> + setState { copy(sendMode = SendMode.QUOTE(timelineEvent, action.text)) } + } + } + + private fun handleEnterReplyMode(action: TextComposerAction.EnterReplyMode) { + room.getTimeLineEvent(action.eventId)?.let { timelineEvent -> + setState { copy(sendMode = SendMode.REPLY(timelineEvent, action.text)) } + } + } + + private fun handleSendMessage(action: TextComposerAction.SendMessage) { + withState { state -> + when (state.sendMode) { + is SendMode.REGULAR -> { + when (val slashCommandResult = CommandParser.parseSplashCommand(action.text)) { + is ParsedCommand.ErrorNotACommand -> { + // Send the text message to the room + room.sendTextMessage(action.text, autoMarkdown = action.autoMarkdown) + _viewEvents.post(TextComposerViewEvents.MessageSent) + popDraft() + } + is ParsedCommand.ErrorSyntax -> { + _viewEvents.post(TextComposerViewEvents.SlashCommandError(slashCommandResult.command)) + } + is ParsedCommand.ErrorEmptySlashCommand -> { + _viewEvents.post(TextComposerViewEvents.SlashCommandUnknown("/")) + } + is ParsedCommand.ErrorUnknownSlashCommand -> { + _viewEvents.post(TextComposerViewEvents.SlashCommandUnknown(slashCommandResult.slashCommand)) + } + is ParsedCommand.SendPlainText -> { + // Send the text message to the room, without markdown + room.sendTextMessage(slashCommandResult.message, autoMarkdown = false) + _viewEvents.post(TextComposerViewEvents.MessageSent) + popDraft() + } + is ParsedCommand.ChangeRoomName -> { + handleChangeRoomNameSlashCommand(slashCommandResult) + } + is ParsedCommand.Invite -> { + handleInviteSlashCommand(slashCommandResult) + } + is ParsedCommand.Invite3Pid -> { + handleInvite3pidSlashCommand(slashCommandResult) + } + is ParsedCommand.SetUserPowerLevel -> { + handleSetUserPowerLevel(slashCommandResult) + } + is ParsedCommand.ClearScalarToken -> { + // TODO + _viewEvents.post(TextComposerViewEvents.SlashCommandNotImplemented) + } + is ParsedCommand.SetMarkdown -> { + vectorPreferences.setMarkdownEnabled(slashCommandResult.enable) + _viewEvents.post(TextComposerViewEvents.SlashCommandResultOk( + if (slashCommandResult.enable) R.string.markdown_has_been_enabled else R.string.markdown_has_been_disabled)) + popDraft() + } + is ParsedCommand.BanUser -> { + handleBanSlashCommand(slashCommandResult) + } + is ParsedCommand.UnbanUser -> { + handleUnbanSlashCommand(slashCommandResult) + } + is ParsedCommand.IgnoreUser -> { + handleIgnoreSlashCommand(slashCommandResult) + } + is ParsedCommand.UnignoreUser -> { + handleUnignoreSlashCommand(slashCommandResult) + } + is ParsedCommand.KickUser -> { + handleKickSlashCommand(slashCommandResult) + } + is ParsedCommand.JoinRoom -> { + handleJoinToAnotherRoomSlashCommand(slashCommandResult) + popDraft() + } + is ParsedCommand.PartRoom -> { + handlePartSlashCommand(slashCommandResult) + } + is ParsedCommand.SendEmote -> { + room.sendTextMessage(slashCommandResult.message, msgType = MessageType.MSGTYPE_EMOTE, autoMarkdown = action.autoMarkdown) + _viewEvents.post(TextComposerViewEvents.SlashCommandResultOk()) + popDraft() + } + is ParsedCommand.SendRainbow -> { + slashCommandResult.message.toString().let { + room.sendFormattedTextMessage(it, rainbowGenerator.generate(it)) + } + _viewEvents.post(TextComposerViewEvents.SlashCommandResultOk()) + popDraft() + } + is ParsedCommand.SendRainbowEmote -> { + slashCommandResult.message.toString().let { + room.sendFormattedTextMessage(it, rainbowGenerator.generate(it), MessageType.MSGTYPE_EMOTE) + } + _viewEvents.post(TextComposerViewEvents.SlashCommandResultOk()) + popDraft() + } + is ParsedCommand.SendSpoiler -> { + room.sendFormattedTextMessage( + "[${stringProvider.getString(R.string.spoiler)}](${slashCommandResult.message})", + "${slashCommandResult.message}" + ) + _viewEvents.post(TextComposerViewEvents.SlashCommandResultOk()) + popDraft() + } + is ParsedCommand.SendShrug -> { + sendPrefixedMessage("¯\\_(ツ)_/¯", slashCommandResult.message) + _viewEvents.post(TextComposerViewEvents.SlashCommandResultOk()) + popDraft() + } + is ParsedCommand.SendLenny -> { + sendPrefixedMessage("( ͡° ͜ʖ ͡°)", slashCommandResult.message) + _viewEvents.post(TextComposerViewEvents.SlashCommandResultOk()) + popDraft() + } + is ParsedCommand.SendChatEffect -> { + sendChatEffect(slashCommandResult) + _viewEvents.post(TextComposerViewEvents.SlashCommandResultOk()) + popDraft() + } + is ParsedCommand.SendPoll -> { + room.sendPoll(slashCommandResult.question, slashCommandResult.options.mapIndexed { index, s -> OptionItem(s, "$index. $s") }) + _viewEvents.post(TextComposerViewEvents.SlashCommandResultOk()) + popDraft() + } + is ParsedCommand.ChangeTopic -> { + handleChangeTopicSlashCommand(slashCommandResult) + } + is ParsedCommand.ChangeDisplayName -> { + handleChangeDisplayNameSlashCommand(slashCommandResult) + } + is ParsedCommand.ChangeDisplayNameForRoom -> { + handleChangeDisplayNameForRoomSlashCommand(slashCommandResult) + } + is ParsedCommand.ChangeRoomAvatar -> { + handleChangeRoomAvatarSlashCommand(slashCommandResult) + } + is ParsedCommand.ChangeAvatarForRoom -> { + handleChangeAvatarForRoomSlashCommand(slashCommandResult) + } + is ParsedCommand.ShowUser -> { + _viewEvents.post(TextComposerViewEvents.SlashCommandResultOk()) + handleWhoisSlashCommand(slashCommandResult) + popDraft() + } + is ParsedCommand.DiscardSession -> { + if (room.isEncrypted()) { + session.cryptoService().discardOutboundSession(room.roomId) + _viewEvents.post(TextComposerViewEvents.SlashCommandResultOk()) + popDraft() + } else { + _viewEvents.post(TextComposerViewEvents.SlashCommandResultOk()) + _viewEvents.post( + TextComposerViewEvents + .ShowMessage(stringProvider.getString(R.string.command_description_discard_session_not_handled)) + ) + } + } + is ParsedCommand.CreateSpace -> { + _viewEvents.post(TextComposerViewEvents.SlashCommandLoading) + viewModelScope.launch(Dispatchers.IO) { + try { + val params = CreateSpaceParams().apply { + name = slashCommandResult.name + invitedUserIds.addAll(slashCommandResult.invitees) + } + val spaceId = session.spaceService().createSpace(params) + session.spaceService().getSpace(spaceId) + ?.addChildren( + state.roomId, + null, + null, + true + ) + popDraft() + _viewEvents.post(TextComposerViewEvents.SlashCommandResultOk()) + } catch (failure: Throwable) { + _viewEvents.post(TextComposerViewEvents.SlashCommandResultError(failure)) + } + } + Unit + } + is ParsedCommand.AddToSpace -> { + _viewEvents.post(TextComposerViewEvents.SlashCommandLoading) + viewModelScope.launch(Dispatchers.IO) { + try { + session.spaceService().getSpace(slashCommandResult.spaceId) + ?.addChildren( + room.roomId, + null, + null, + false + ) + popDraft() + _viewEvents.post(TextComposerViewEvents.SlashCommandResultOk()) + } catch (failure: Throwable) { + _viewEvents.post(TextComposerViewEvents.SlashCommandResultError(failure)) + } + } + Unit + } + is ParsedCommand.JoinSpace -> { + _viewEvents.post(TextComposerViewEvents.SlashCommandLoading) + viewModelScope.launch(Dispatchers.IO) { + try { + session.spaceService().joinSpace(slashCommandResult.spaceIdOrAlias) + popDraft() + _viewEvents.post(TextComposerViewEvents.SlashCommandResultOk()) + } catch (failure: Throwable) { + _viewEvents.post(TextComposerViewEvents.SlashCommandResultError(failure)) + } + } + Unit + } + is ParsedCommand.LeaveRoom -> { + viewModelScope.launch(Dispatchers.IO) { + try { + session.getRoom(slashCommandResult.roomId)?.leave(null) + popDraft() + _viewEvents.post(TextComposerViewEvents.SlashCommandResultOk()) + } catch (failure: Throwable) { + _viewEvents.post(TextComposerViewEvents.SlashCommandResultError(failure)) + } + } + Unit + } + is ParsedCommand.UpgradeRoom -> { + _viewEvents.post( + TextComposerViewEvents.ShowRoomUpgradeDialog( + slashCommandResult.newVersion, + room.roomSummary()?.isPublic ?: false + ) + ) + _viewEvents.post(TextComposerViewEvents.SlashCommandResultOk()) + popDraft() + } + }.exhaustive + } + is SendMode.EDIT -> { + // is original event a reply? + val inReplyTo = state.sendMode.timelineEvent.getRelationContent()?.inReplyTo?.eventId + if (inReplyTo != null) { + // TODO check if same content? + room.getTimeLineEvent(inReplyTo)?.let { + room.editReply(state.sendMode.timelineEvent, it, action.text.toString()) + } + } else { + val messageContent = state.sendMode.timelineEvent.getLastMessageContent() + val existingBody = messageContent?.body ?: "" + if (existingBody != action.text) { + room.editTextMessage(state.sendMode.timelineEvent, + messageContent?.msgType ?: MessageType.MSGTYPE_TEXT, + action.text, + action.autoMarkdown) + } else { + Timber.w("Same message content, do not send edition") + } + } + _viewEvents.post(TextComposerViewEvents.MessageSent) + popDraft() + } + is SendMode.QUOTE -> { + val messageContent = state.sendMode.timelineEvent.getLastMessageContent() + val textMsg = messageContent?.body + + val finalText = legacyRiotQuoteText(textMsg, action.text.toString()) + + // TODO check for pills? + + // TODO Refactor this, just temporary for quotes + val parser = Parser.builder().build() + val document = parser.parse(finalText) + val renderer = HtmlRenderer.builder().build() + val htmlText = renderer.render(document) + if (finalText == htmlText) { + room.sendTextMessage(finalText) + } else { + room.sendFormattedTextMessage(finalText, htmlText) + } + _viewEvents.post(TextComposerViewEvents.MessageSent) + popDraft() + } + is SendMode.REPLY -> { + state.sendMode.timelineEvent.let { + room.replyToMessage(it, action.text.toString(), action.autoMarkdown) + _viewEvents.post(TextComposerViewEvents.MessageSent) + popDraft() + } + } + }.exhaustive + } + } + + private fun popDraft() = withState { + if (it.sendMode is SendMode.REGULAR && it.sendMode.fromSharing) { + // If we were sharing, we want to get back our last value from draft + loadDraftIfAny() + } else { + // Otherwise we clear the composer and remove the draft from db + setState { copy(sendMode = SendMode.REGULAR("", false)) } + viewModelScope.launch { + room.deleteDraft() + } + } + } + + private fun loadDraftIfAny() { + val currentDraft = room.getDraft() + setState { + copy( + // Create a sendMode from a draft and retrieve the TimelineEvent + sendMode = when (currentDraft) { + is UserDraft.REGULAR -> SendMode.REGULAR(currentDraft.text, false) + is UserDraft.QUOTE -> { + room.getTimeLineEvent(currentDraft.linkedEventId)?.let { timelineEvent -> + SendMode.QUOTE(timelineEvent, currentDraft.text) + } + } + is UserDraft.REPLY -> { + room.getTimeLineEvent(currentDraft.linkedEventId)?.let { timelineEvent -> + SendMode.REPLY(timelineEvent, currentDraft.text) + } + } + is UserDraft.EDIT -> { + room.getTimeLineEvent(currentDraft.linkedEventId)?.let { timelineEvent -> + SendMode.EDIT(timelineEvent, currentDraft.text) + } + } + else -> null + } ?: SendMode.REGULAR("", fromSharing = false) + ) + } + } + + private fun handleUserIsTyping(action: TextComposerAction.UserIsTyping) { + if (vectorPreferences.sendTypingNotifs()) { + if (action.isTyping) { + room.userIsTyping() + } else { + room.userStopsTyping() + } + } + } + + private fun sendChatEffect(sendChatEffect: ParsedCommand.SendChatEffect) { + // If message is blank, convert to an emote, with default message + if (sendChatEffect.message.isBlank()) { + val defaultMessage = stringProvider.getString(when (sendChatEffect.chatEffect) { + ChatEffect.CONFETTI -> R.string.default_message_emote_confetti + ChatEffect.SNOWFALL -> R.string.default_message_emote_snow + }) + room.sendTextMessage(defaultMessage, MessageType.MSGTYPE_EMOTE) + } else { + room.sendTextMessage(sendChatEffect.message, sendChatEffect.chatEffect.toMessageType()) + } + } + + private fun handleJoinToAnotherRoomSlashCommand(command: ParsedCommand.JoinRoom) { + viewModelScope.launch { + try { + session.joinRoom(command.roomAlias, command.reason, emptyList()) + } catch (failure: Throwable) { + _viewEvents.post(TextComposerViewEvents.SlashCommandResultError(failure)) + return@launch + } + session.getRoomSummary(command.roomAlias) + ?.roomId + ?.let { + _viewEvents.post(TextComposerViewEvents.JoinRoomCommandSuccess(it)) + } + } + } + + private fun legacyRiotQuoteText(quotedText: String?, myText: String): String { + val messageParagraphs = quotedText?.split("\n\n".toRegex())?.dropLastWhile { it.isEmpty() }?.toTypedArray() + return buildString { + if (messageParagraphs != null) { + for (i in messageParagraphs.indices) { + if (messageParagraphs[i].isNotBlank()) { + append("> ") + append(messageParagraphs[i]) + } + + if (i != messageParagraphs.lastIndex) { + append("\n\n") + } + } + } + append("\n\n") + append(myText) + } + } + + private fun handleChangeTopicSlashCommand(changeTopic: ParsedCommand.ChangeTopic) { + launchSlashCommandFlowSuspendable { + room.updateTopic(changeTopic.topic) + } + } + + private fun handleInviteSlashCommand(invite: ParsedCommand.Invite) { + launchSlashCommandFlowSuspendable { + room.invite(invite.userId, invite.reason) + } + } + + private fun handleInvite3pidSlashCommand(invite: ParsedCommand.Invite3Pid) { + launchSlashCommandFlowSuspendable { + room.invite3pid(invite.threePid) + } + } + + private fun handleSetUserPowerLevel(setUserPowerLevel: ParsedCommand.SetUserPowerLevel) { + val newPowerLevelsContent = room.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS) + ?.content + ?.toModel() + ?.setUserPowerLevel(setUserPowerLevel.userId, setUserPowerLevel.powerLevel) + ?.toContent() + ?: return + + launchSlashCommandFlowSuspendable { + room.sendStateEvent(EventType.STATE_ROOM_POWER_LEVELS, null, newPowerLevelsContent) + } + } + + private fun handleChangeDisplayNameSlashCommand(changeDisplayName: ParsedCommand.ChangeDisplayName) { + launchSlashCommandFlowSuspendable { + session.setDisplayName(session.myUserId, changeDisplayName.displayName) + } + } + + private fun handlePartSlashCommand(command: ParsedCommand.PartRoom) { + launchSlashCommandFlowSuspendable { + if (command.roomAlias == null) { + // Leave the current room + room + } else { + session.getRoomSummary(roomIdOrAlias = command.roomAlias) + ?.roomId + ?.let { session.getRoom(it) } + } + ?.leave(reason = null) + } + } + + private fun handleKickSlashCommand(kick: ParsedCommand.KickUser) { + launchSlashCommandFlowSuspendable { + room.kick(kick.userId, kick.reason) + } + } + + private fun handleBanSlashCommand(ban: ParsedCommand.BanUser) { + launchSlashCommandFlowSuspendable { + room.ban(ban.userId, ban.reason) + } + } + + private fun handleUnbanSlashCommand(unban: ParsedCommand.UnbanUser) { + launchSlashCommandFlowSuspendable { + room.unban(unban.userId, unban.reason) + } + } + + private fun handleChangeRoomNameSlashCommand(changeRoomName: ParsedCommand.ChangeRoomName) { + launchSlashCommandFlowSuspendable { + room.updateName(changeRoomName.name) + } + } + + private fun getMyRoomMemberContent(): RoomMemberContent? { + return room.getStateEvent(EventType.STATE_ROOM_MEMBER, QueryStringValue.Equals(session.myUserId)) + ?.content + ?.toModel() + } + + private fun handleChangeDisplayNameForRoomSlashCommand(changeDisplayName: ParsedCommand.ChangeDisplayNameForRoom) { + launchSlashCommandFlowSuspendable { + getMyRoomMemberContent() + ?.copy(displayName = changeDisplayName.displayName) + ?.toContent() + ?.let { + room.sendStateEvent(EventType.STATE_ROOM_MEMBER, session.myUserId, it) + } + } + } + + private fun handleChangeRoomAvatarSlashCommand(changeAvatar: ParsedCommand.ChangeRoomAvatar) { + launchSlashCommandFlowSuspendable { + room.sendStateEvent(EventType.STATE_ROOM_AVATAR, null, RoomAvatarContent(changeAvatar.url).toContent()) + } + } + + private fun handleChangeAvatarForRoomSlashCommand(changeAvatar: ParsedCommand.ChangeAvatarForRoom) { + launchSlashCommandFlowSuspendable { + getMyRoomMemberContent() + ?.copy(avatarUrl = changeAvatar.url) + ?.toContent() + ?.let { + room.sendStateEvent(EventType.STATE_ROOM_MEMBER, session.myUserId, it) + } + } + } + + private fun handleIgnoreSlashCommand(ignore: ParsedCommand.IgnoreUser) { + launchSlashCommandFlowSuspendable { + session.ignoreUserIds(listOf(ignore.userId)) + } + } + + private fun handleUnignoreSlashCommand(unignore: ParsedCommand.UnignoreUser) { + launchSlashCommandFlowSuspendable { + session.unIgnoreUserIds(listOf(unignore.userId)) + } + } + + private fun handleWhoisSlashCommand(whois: ParsedCommand.ShowUser) { + _viewEvents.post(TextComposerViewEvents.OpenRoomMemberProfile(whois.userId)) + } + + private fun sendPrefixedMessage(prefix: String, message: CharSequence) { + val sequence = buildString { + append(prefix) + if (message.isNotEmpty()) { + append(" ") + append(message) + } + } + room.sendTextMessage(sequence) + } + + /** + * Convert a send mode to a draft and save the draft + */ + private fun handleSaveDraft(action: TextComposerAction.SaveDraft) = withState { + session.coroutineScope.launch { + when { + it.sendMode is SendMode.REGULAR && !it.sendMode.fromSharing -> { + setState { copy(sendMode = it.sendMode.copy(action.draft)) } + room.saveDraft(UserDraft.REGULAR(action.draft)) + } + it.sendMode is SendMode.REPLY -> { + setState { copy(sendMode = it.sendMode.copy(text = action.draft)) } + room.saveDraft(UserDraft.REPLY(it.sendMode.timelineEvent.root.eventId!!, action.draft)) + } + it.sendMode is SendMode.QUOTE -> { + setState { copy(sendMode = it.sendMode.copy(text = action.draft)) } + room.saveDraft(UserDraft.QUOTE(it.sendMode.timelineEvent.root.eventId!!, action.draft)) + } + it.sendMode is SendMode.EDIT -> { + setState { copy(sendMode = it.sendMode.copy(text = action.draft)) } + room.saveDraft(UserDraft.EDIT(it.sendMode.timelineEvent.root.eventId!!, action.draft)) + } + } + } + } + + private fun launchSlashCommandFlowSuspendable(block: suspend () -> Unit) { + _viewEvents.post(TextComposerViewEvents.SlashCommandLoading) + viewModelScope.launch { + val event = try { + block() + popDraft() + TextComposerViewEvents.SlashCommandResultOk() + } catch (failure: Throwable) { + TextComposerViewEvents.SlashCommandResultError(failure) + } + _viewEvents.post(event) + } + } + + @AssistedFactory + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: TextComposerViewState): TextComposerViewModel + } + + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewState.kt new file mode 100644 index 0000000000..199fb1b82d --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewState.kt @@ -0,0 +1,56 @@ +/* + * 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.composer + +import com.airbnb.mvrx.MavericksState +import im.vector.app.features.home.room.detail.RoomDetailArgs +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent + +/** + * Describes the current send mode: + * REGULAR: sends the text as a regular message + * QUOTE: User is currently quoting a message + * EDIT: User is currently editing an existing message + * + * Depending on the state the bottom toolbar will change (icons/preview/actions...) + */ +sealed class SendMode(open val text: String) { + data class REGULAR( + override val text: String, + val fromSharing: Boolean, + // This is necessary for forcing refresh on selectSubscribe + private val ts: Long = System.currentTimeMillis() + ) : SendMode(text) + + data class QUOTE(val timelineEvent: TimelineEvent, override val text: String) : SendMode(text) + data class EDIT(val timelineEvent: TimelineEvent, override val text: String) : SendMode(text) + data class REPLY(val timelineEvent: TimelineEvent, override val text: String) : SendMode(text) +} + +data class TextComposerViewState( + val roomId: String, + val canSendMessage: Boolean = true, + val isVoiceRecording: Boolean = false, + val isSendButtonVisible: Boolean = false, + val sendMode: SendMode = SendMode.REGULAR("", false) +) : MavericksState { + + val isComposerVisible = canSendMessage && !isVoiceRecording + val isVoiceMessageRecorderVisible = canSendMessage && !isSendButtonVisible + + constructor(args: RoomDetailArgs) : this(roomId = args.roomId) +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageHelper.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageHelper.kt index 0225ef2853..adcd6a3008 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageHelper.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageHelper.kt @@ -60,6 +60,7 @@ class VoiceMessageHelper @Inject constructor( try { voiceRecorder.startRecord() } catch (failure: Throwable) { + Timber.e(failure, "Unable to start recording") throw VoiceFailure.UnableToRecord(failure) } startRecordingAmplitudes() @@ -146,6 +147,7 @@ class VoiceMessageHelper @Inject constructor( } } } catch (failure: Throwable) { + Timber.e(failure, "Unable to start playback") throw VoiceFailure.UnableToPlay(failure) } startPlaybackTicker(id) @@ -215,10 +217,12 @@ class VoiceMessageHelper @Inject constructor( playbackTicker = null } - fun stopAllVoiceActions() { + fun stopAllVoiceActions(deleteRecord: Boolean = true) { stopRecording() stopPlayback() - deleteRecording() + if (deleteRecord) { + deleteRecording() + } playbackTracker.clear() } } 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 5f4f53cf1e..f7b8cead37 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageRecorderView.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageRecorderView.kt @@ -27,6 +27,9 @@ import androidx.core.view.isVisible import androidx.core.view.updateLayoutParams import im.vector.app.BuildConfig import im.vector.app.R +import im.vector.app.core.extensions.setAttributeBackground +import im.vector.app.core.extensions.setAttributeTintedBackground +import im.vector.app.core.extensions.setAttributeTintedImageResource import im.vector.app.core.hardware.vibrate import im.vector.app.core.utils.CountUpTimer import im.vector.app.core.utils.DimensionConverter @@ -40,7 +43,7 @@ import kotlin.math.floor /** * Encapsulates the voice message recording view and animations. */ -class VoiceMessageRecorderView: ConstraintLayout, VoiceMessagePlaybackTracker.Listener { +class VoiceMessageRecorderView : ConstraintLayout, VoiceMessagePlaybackTracker.Listener { interface Callback { // Return true if the recording is started @@ -217,7 +220,7 @@ class VoiceMessageRecorderView: ConstraintLayout, VoiceMessagePlaybackTracker.Li views.voiceMessageLockArrow.translationY = 0F } RecordingState.LOCKING -> { - views.voiceMessageLockImage.setImageResource(R.drawable.ic_voice_message_locked) + views.voiceMessageLockImage.setAttributeTintedImageResource(R.drawable.ic_voice_message_locked, R.attr.colorPrimary) val translationAmount = -distanceY.coerceIn(0F, distanceToLock) views.voiceMessageMicButton.translationY = translationAmount views.voiceMessageLockArrow.translationY = translationAmount @@ -257,8 +260,8 @@ class VoiceMessageRecorderView: ConstraintLayout, VoiceMessagePlaybackTracker.Li val previousRecordingState = recordingState if (recordingState == RecordingState.STARTED) { // Determine if cancelling or locking for the first move action. - if (((currentX < firstX && rtlXMultiplier == 1) || (currentX > firstX && rtlXMultiplier == -1)) - && distanceX > distanceY && distanceX > lastDistanceX) { + if (((currentX < firstX && rtlXMultiplier == 1) || (currentX > firstX && rtlXMultiplier == -1)) && + distanceX > distanceY && distanceX > lastDistanceX) { recordingState = RecordingState.CANCELLING } else if (currentY < firstY && distanceY > distanceX && distanceY > lastDistanceY) { recordingState = RecordingState.LOCKING @@ -366,6 +369,7 @@ class VoiceMessageRecorderView: ConstraintLayout, VoiceMessagePlaybackTracker.Li private fun showRecordingViews() { views.voiceMessageMicButton.setImageResource(R.drawable.ic_voice_mic_recording) + views.voiceMessageMicButton.setAttributeTintedBackground(R.drawable.circle_with_halo, R.attr.colorPrimary) views.voiceMessageMicButton.updateLayoutParams { setMargins(0, 0, 0, 0) } @@ -443,6 +447,7 @@ class VoiceMessageRecorderView: ConstraintLayout, VoiceMessagePlaybackTracker.Li private fun resetMicButtonUi() { views.voiceMessageMicButton.isVisible = true views.voiceMessageMicButton.setImageResource(R.drawable.ic_voice_mic) + views.voiceMessageMicButton.setAttributeBackground(android.R.attr.selectableItemBackgroundBorderless) views.voiceMessageMicButton.updateLayoutParams { if (rtlXMultiplier == -1) { // RTL @@ -499,12 +504,14 @@ class VoiceMessageRecorderView: ConstraintLayout, VoiceMessagePlaybackTracker.Li views.voiceMessagePlaybackTimerIndicator.isVisible = true views.voicePlaybackControlButton.isVisible = false views.voiceMessageSendButton.isVisible = true + views.voicePlaybackWaveform.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_YES renderToast(context.getString(R.string.voice_message_tap_to_stop_toast)) } private fun showPlaybackViews() { views.voiceMessagePlaybackTimerIndicator.isVisible = false views.voicePlaybackControlButton.isVisible = true + views.voicePlaybackWaveform.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO callback?.onVoiceRecordingPlaybackModeOn() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/readreceipts/DisplayReadReceiptItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/readreceipts/DisplayReadReceiptItem.kt index c097b9adb5..7a91dae183 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/readreceipts/DisplayReadReceiptItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/readreceipts/DisplayReadReceiptItem.kt @@ -26,6 +26,7 @@ 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.onClick +import im.vector.app.features.displayname.getBestName import im.vector.app.features.home.AvatarRenderer import org.matrix.android.sdk.api.util.MatrixItem diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/readreceipts/DisplayReadReceiptsBottomSheet.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/readreceipts/DisplayReadReceiptsBottomSheet.kt index 95b2333b43..6c315a4e76 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/readreceipts/DisplayReadReceiptsBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/readreceipts/DisplayReadReceiptsBottomSheet.kt @@ -21,10 +21,10 @@ import android.os.Parcelable import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import com.airbnb.mvrx.MvRx +import com.airbnb.mvrx.Mavericks import com.airbnb.mvrx.args +import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R -import im.vector.app.core.di.ScreenComponent import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.configureWith import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment @@ -43,6 +43,7 @@ data class DisplayReadReceiptArgs( /** * Bottom sheet displaying list of read receipts for a given event ordered by descending timestamp */ +@AndroidEntryPoint class DisplayReadReceiptsBottomSheet : VectorBaseBottomSheetDialogFragment(), DisplayReadReceiptsController.Listener { @@ -53,10 +54,6 @@ class DisplayReadReceiptsBottomSheet : private lateinit var sharedActionViewModel: MessageSharedActionViewModel - override fun injectWith(injector: ScreenComponent) { - injector.inject(this) - } - override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): BottomSheetGenericListWithTitleBinding { return BottomSheetGenericListWithTitleBinding.inflate(inflater, container, false) } @@ -88,7 +85,7 @@ class DisplayReadReceiptsBottomSheet : val parcelableArgs = DisplayReadReceiptArgs( readReceipts ) - args.putParcelable(MvRx.KEY_ARG, parcelableArgs) + args.putParcelable(Mavericks.KEY_ARG, parcelableArgs) return DisplayReadReceiptsBottomSheet().apply { arguments = args } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/readreceipts/DisplayReadReceiptsController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/readreceipts/DisplayReadReceiptsController.kt index f5348bd398..bf88218fa6 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/readreceipts/DisplayReadReceiptsController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/readreceipts/DisplayReadReceiptsController.kt @@ -30,8 +30,8 @@ import javax.inject.Inject */ class DisplayReadReceiptsController @Inject constructor(private val dateFormatter: VectorDateFormatter, private val session: Session, - private val avatarRender: AvatarRenderer) - : TypedEpoxyController>() { + private val avatarRender: AvatarRenderer) : + TypedEpoxyController>() { var listener: Listener? = null diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchActivity.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchActivity.kt index d9a2f98e32..eea62b0907 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchActivity.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchActivity.kt @@ -20,13 +20,14 @@ import android.content.Context import android.content.Intent import android.os.Bundle import androidx.appcompat.widget.SearchView -import com.airbnb.mvrx.MvRx +import com.airbnb.mvrx.Mavericks +import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R -import im.vector.app.core.di.ScreenComponent import im.vector.app.core.extensions.addFragment import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.databinding.ActivitySearchBinding +@AndroidEntryPoint class SearchActivity : VectorBaseActivity() { private val searchFragment: SearchFragment? @@ -38,10 +39,6 @@ class SearchActivity : VectorBaseActivity() { override fun getCoordinatorLayout() = views.coordinatorLayout - override fun injectWith(injector: ScreenComponent) { - injector.inject(this) - } - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) configureToolbar(views.searchToolbar) @@ -49,7 +46,7 @@ class SearchActivity : VectorBaseActivity() { override fun initUiAndData() { if (isFirstCreation()) { - val fragmentArgs: SearchArgs = intent?.extras?.getParcelable(MvRx.KEY_ARG) ?: return + val fragmentArgs: SearchArgs = intent?.extras?.getParcelable(Mavericks.KEY_ARG) ?: return addFragment(R.id.searchFragmentContainer, SearchFragment::class.java, fragmentArgs, FRAGMENT_TAG) } views.searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { @@ -73,7 +70,7 @@ class SearchActivity : VectorBaseActivity() { return Intent(context, SearchActivity::class.java).apply { // If we do that we will have the same room two times on the stack. Let's allow infinite stack for the moment. // flags = Intent.FLAG_ACTIVITY_REORDER_TO_FRONT - putExtra(MvRx.KEY_ARG, args) + putExtra(Mavericks.KEY_ARG, args) } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchFragment.kt index 9f34cdd679..4a285da5f2 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchFragment.kt @@ -47,7 +47,6 @@ data class SearchArgs( ) : Parcelable class SearchFragment @Inject constructor( - val viewModelFactory: SearchViewModel.Factory, private val controller: SearchResultController ) : VectorBaseFragment(), StateView.EventCallback, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultController.kt index 1debf32104..edef92ee2d 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultController.kt @@ -102,9 +102,9 @@ class SearchResultController @Inject constructor( data.searchResult.forEach { eventAndSender -> val event = eventAndSender.event - @Suppress("UNCHECKED_CAST") // Take new content first - val text = ((event.content?.get("m.new_content") as? Content) ?: event.content)?.get("body") as? String ?: return@forEach + @Suppress("UNCHECKED_CAST") +val text = ((event.content?.get("m.new_content") as? Content) ?: event.content)?.get("body") as? String ?: return@forEach val spannable = setHighLightedText(text, data.highlights) ?: return@forEach val eventDate = Calendar.getInstance().apply { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultItem.kt index 14ea94ffd5..c0f71ed6cc 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultItem.kt @@ -26,6 +26,7 @@ import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.epoxy.onClick import im.vector.app.core.extensions.setTextOrHide +import im.vector.app.features.displayname.getBestName import im.vector.app.features.home.AvatarRenderer import org.matrix.android.sdk.api.util.MatrixItem diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchViewModel.kt index 26111e4f2b..a360b91085 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchViewModel.kt @@ -16,16 +16,15 @@ package im.vector.app.features.home.room.detail.search -import androidx.lifecycle.viewModelScope import com.airbnb.mvrx.Fail -import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.Loading -import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.Success -import com.airbnb.mvrx.ViewModelContext import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import kotlinx.coroutines.CancellationException @@ -46,18 +45,11 @@ class SearchViewModel @AssistedInject constructor( private var nextBatch: String? = null @AssistedFactory - interface Factory { - fun create(initialState: SearchViewState): SearchViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: SearchViewState): SearchViewModel } - companion object : MvRxViewModelFactory { - - @JvmStatic - override fun create(viewModelContext: ViewModelContext, state: SearchViewState): SearchViewModel? { - val fragment: SearchFragment = (viewModelContext as FragmentViewModelContext).fragment() - return fragment.viewModelFactory.create(state) - } - } + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() override fun handle(action: SearchAction) { when (action) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchViewState.kt index 41fecbb5e2..4b272b6c4c 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchViewState.kt @@ -17,7 +17,7 @@ package im.vector.app.features.home.room.detail.search import com.airbnb.mvrx.Async -import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.Uninitialized import org.matrix.android.sdk.api.session.search.EventAndSender @@ -32,7 +32,7 @@ data class SearchViewState( val roomId: String = "", // Current pagination request val asyncSearchRequest: Async = Uninitialized -) : MvRxState { +) : MavericksState { constructor(args: SearchArgs) : this(roomId = args.roomId) } 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 377e5e06df..e5c1cd5f73 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 @@ -269,6 +269,10 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec } override fun buildModels() { + // Don't build anything if membership is not joined + if (partialState.roomSummary?.membership != Membership.JOIN) { + return + } val timestamp = System.currentTimeMillis() val showingForwardLoader = LoadingItem_() @@ -496,8 +500,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec if (vectorPreferences.labShowCompleteHistoryInEncryptedRoom()) { return } - if (event.root.type == EventType.STATE_ROOM_MEMBER - && event.root.stateKey == session.myUserId) { + if (event.root.type == EventType.STATE_ROOM_MEMBER && + event.root.stateKey == session.myUserId) { val content = event.root.content.toModel() if (content?.membership == Membership.INVITE) { hasReachedInvite = true diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionState.kt index caee485aba..c1c145040e 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionState.kt @@ -17,7 +17,7 @@ package im.vector.app.features.home.room.detail.timeline.action import com.airbnb.mvrx.Async -import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.Uninitialized import im.vector.app.core.extensions.canReact import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData @@ -50,7 +50,7 @@ data class MessageActionState( val actions: List = emptyList(), val expendedReportContentMenu: Boolean = false, val actionPermissions: ActionPermissions = ActionPermissions() -) : MvRxState { +) : MavericksState { constructor(args: TimelineEventFragmentArgs) : this(roomId = args.roomId, eventId = args.eventId, informationData = args.informationData) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt index cb64c8b5ad..5e0db19d9e 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt @@ -21,23 +21,22 @@ import android.view.View import android.view.ViewGroup import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState -import im.vector.app.core.di.ScreenComponent +import dagger.hilt.android.AndroidEntryPoint import im.vector.app.core.extensions.cleanup 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.home.room.detail.timeline.item.MessageInformationData - import javax.inject.Inject /** * Bottom sheet fragment that shows a message preview with list of contextual actions */ +@AndroidEntryPoint class MessageActionsBottomSheet : VectorBaseBottomSheetDialogFragment(), MessageActionsEpoxyController.MessageActionsEpoxyControllerListener { - @Inject lateinit var messageActionViewModelFactory: MessageActionsViewModel.Factory @Inject lateinit var messageActionsEpoxyController: MessageActionsEpoxyController private val viewModel: MessageActionsViewModel by fragmentViewModel(MessageActionsViewModel::class) @@ -46,10 +45,6 @@ class MessageActionsBottomSheet : private lateinit var sharedActionViewModel: MessageSharedActionViewModel - override fun injectWith(injector: ScreenComponent) { - injector.inject(this) - } - override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): BottomSheetGenericListBinding { return BottomSheetGenericListBinding.inflate(inflater, container, false) } 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 6e6c7c1dbe..30c231131e 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 @@ -15,15 +15,14 @@ */ package im.vector.app.features.home.room.detail.timeline.action -import com.airbnb.mvrx.FragmentViewModelContext -import com.airbnb.mvrx.MvRxViewModelFactory -import com.airbnb.mvrx.ViewModelContext -import com.jakewharton.rxrelay2.BehaviorRelay +import com.airbnb.mvrx.MavericksViewModelFactory import dagger.Lazy import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import im.vector.app.R +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.extensions.canReact import im.vector.app.core.platform.EmptyViewEvents @@ -33,9 +32,14 @@ import im.vector.app.features.home.room.detail.timeline.format.NoticeEventFormat import im.vector.app.features.html.EventHtmlRenderer import im.vector.app.features.html.PillsPostProcessor import im.vector.app.features.html.VectorHtmlCompressor -import im.vector.app.features.powerlevel.PowerLevelsObservableFactory +import im.vector.app.features.powerlevel.PowerLevelsFlowFactory import im.vector.app.features.reactions.data.EmojiDataSource import im.vector.app.features.settings.VectorPreferences +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach 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 @@ -54,9 +58,8 @@ import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent import org.matrix.android.sdk.api.session.room.timeline.hasBeenEdited -import org.matrix.android.sdk.rx.rx -import org.matrix.android.sdk.rx.unwrap -import java.util.ArrayList +import org.matrix.android.sdk.flow.flow +import org.matrix.android.sdk.flow.unwrap /** * Information related to an event and used to display preview in contextual bottom sheet. @@ -79,20 +82,14 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted pillsPostProcessorFactory.create(initialState.roomId) } - private val eventIdObservable = BehaviorRelay.createDefault(initialState.eventId) + private val eventIdFlow = MutableStateFlow(initialState.eventId) @AssistedFactory - interface Factory { - fun create(initialState: MessageActionState): MessageActionsViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: MessageActionState): MessageActionsViewModel } - companion object : MvRxViewModelFactory { - @JvmStatic - override fun create(viewModelContext: ViewModelContext, state: MessageActionState): MessageActionsViewModel? { - val fragment: MessageActionsBottomSheet = (viewModelContext as FragmentViewModelContext).fragment() - return fragment.messageActionViewModelFactory.create(state) - } - } + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() init { observeEvent() @@ -119,8 +116,8 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted if (room == null) { return } - PowerLevelsObservableFactory(room).createObservable() - .subscribe { + PowerLevelsFlowFactory(room).createFlow() + .onEach { val powerLevelsHelper = PowerLevelsHelper(it) val canReact = powerLevelsHelper.isUserAllowedToSend(session.myUserId, false, EventType.REACTION) val canRedact = powerLevelsHelper.isUserAbleToRedact(session.myUserId) @@ -129,13 +126,12 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted setState { copy(actionPermissions = permissions) } - } - .disposeOnClear() + }.launchIn(viewModelScope) } private fun observeEvent() { if (room == null) return - room.rx() + room.flow() .liveTimelineEvent(initialState.eventId) .unwrap() .execute { @@ -145,9 +141,9 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted private fun observeReactions() { if (room == null) return - eventIdObservable - .switchMap { eventId -> - room.rx() + eventIdFlow + .flatMapLatest { eventId -> + room.flow() .liveAnnotationSummary(eventId) .map { annotations -> EmojiDataSource.quickEmojis.map { emoji -> @@ -161,9 +157,9 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted } private fun observeTimelineEventState() { - selectSubscribe(MessageActionState::timelineEvent, MessageActionState::actionPermissions) { timelineEvent, permissions -> - val nonNullTimelineEvent = timelineEvent() ?: return@selectSubscribe - eventIdObservable.accept(nonNullTimelineEvent.eventId) + onEach(MessageActionState::timelineEvent, MessageActionState::actionPermissions) { timelineEvent, permissions -> + val nonNullTimelineEvent = timelineEvent() ?: return@onEach + eventIdFlow.tryEmit(nonNullTimelineEvent.eventId) setState { copy( eventId = nonNullTimelineEvent.eventId, @@ -341,14 +337,14 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted add(EventSharedAction.AddReaction(eventId)) } - if (canQuote(timelineEvent, messageContent, actionPermissions)) { - add(EventSharedAction.Quote(eventId)) - } - if (canViewReactions(timelineEvent)) { add(EventSharedAction.ViewReactions(informationData)) } + if (canQuote(timelineEvent, messageContent, actionPermissions)) { + add(EventSharedAction.Quote(eventId)) + } + if (timelineEvent.hasBeenEdited()) { add(EventSharedAction.ViewEditHistory(informationData)) } @@ -365,14 +361,14 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted if (vectorPreferences.developerMode()) { if (timelineEvent.isEncrypted() && timelineEvent.root.mCryptoError != null) { val keysBackupService = session.cryptoService().keysBackupService() - if (keysBackupService.state == KeysBackupState.NotTrusted - || (keysBackupService.state == KeysBackupState.ReadyToBackUp - && keysBackupService.canRestoreKeys()) + if (keysBackupService.state == KeysBackupState.NotTrusted || + (keysBackupService.state == KeysBackupState.ReadyToBackUp && + keysBackupService.canRestoreKeys()) ) { add(EventSharedAction.UseKeyBackup) } - if (session.cryptoService().getCryptoDeviceInfo(session.myUserId).size > 1 - || timelineEvent.senderInfo.userId != session.myUserId) { + if (session.cryptoService().getCryptoDeviceInfo(session.myUserId).size > 1 || + timelineEvent.senderInfo.userId != session.myUserId) { add(EventSharedAction.ReRequestKey(timelineEvent.eventId)) } } @@ -435,9 +431,9 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted } private fun canRetry(event: TimelineEvent, actionPermissions: ActionPermissions): Boolean { - return event.root.sendState.hasFailed() - && actionPermissions.canSendMessage - && (event.root.isAttachmentMessage() || event.root.isTextMessage()) + return event.root.sendState.hasFailed() && + actionPermissions.canSendMessage && + (event.root.isAttachmentMessage() || event.root.isTextMessage()) } private fun canViewReactions(event: TimelineEvent): Boolean { @@ -453,8 +449,8 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted // TODO if user is admin or moderator val messageContent = event.root.getClearContent().toModel() return event.root.senderId == myUserId && ( - messageContent?.msgType == MessageType.MSGTYPE_TEXT - || messageContent?.msgType == MessageType.MSGTYPE_EMOTE + messageContent?.msgType == MessageType.MSGTYPE_TEXT || + messageContent?.msgType == MessageType.MSGTYPE_EMOTE ) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/edithistory/ViewEditHistoryBottomSheet.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/edithistory/ViewEditHistoryBottomSheet.kt index 7be4be4b57..63140edd8b 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/edithistory/ViewEditHistoryBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/edithistory/ViewEditHistoryBottomSheet.kt @@ -19,35 +19,30 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import com.airbnb.mvrx.MvRx +import com.airbnb.mvrx.Mavericks import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState +import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R -import im.vector.app.core.di.ScreenComponent import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.configureWith import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment import im.vector.app.databinding.BottomSheetGenericListWithTitleBinding import im.vector.app.features.home.room.detail.timeline.action.TimelineEventFragmentArgs import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData - import javax.inject.Inject /** * Bottom sheet displaying list of edits for a given event ordered by timestamp */ -class ViewEditHistoryBottomSheet: +@AndroidEntryPoint +class ViewEditHistoryBottomSheet : VectorBaseBottomSheetDialogFragment() { private val viewModel: ViewEditHistoryViewModel by fragmentViewModel(ViewEditHistoryViewModel::class) - @Inject lateinit var viewEditHistoryViewModelFactory: ViewEditHistoryViewModel.Factory @Inject lateinit var epoxyController: ViewEditHistoryEpoxyController - override fun injectWith(injector: ScreenComponent) { - injector.inject(this) - } - override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): BottomSheetGenericListWithTitleBinding { return BottomSheetGenericListWithTitleBinding.inflate(inflater, container, false) } @@ -79,7 +74,7 @@ class ViewEditHistoryBottomSheet: roomId, informationData ) - args.putParcelable(MvRx.KEY_ARG, parcelableArgs) + args.putParcelable(Mavericks.KEY_ARG, parcelableArgs) return ViewEditHistoryBottomSheet().apply { arguments = args } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/edithistory/ViewEditHistoryViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/edithistory/ViewEditHistoryViewModel.kt index 5732326f2e..9abc67e41f 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/edithistory/ViewEditHistoryViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/edithistory/ViewEditHistoryViewModel.kt @@ -15,16 +15,15 @@ */ package im.vector.app.features.home.room.detail.timeline.edithistory -import androidx.lifecycle.viewModelScope import com.airbnb.mvrx.Fail -import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.Loading -import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.Success -import com.airbnb.mvrx.ViewModelContext import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.platform.EmptyAction import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel @@ -47,18 +46,11 @@ class ViewEditHistoryViewModel @AssistedInject constructor( ?: throw IllegalStateException("Shouldn't use this ViewModel without a room") @AssistedFactory - interface Factory { - fun create(initialState: ViewEditHistoryViewState): ViewEditHistoryViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: ViewEditHistoryViewState): ViewEditHistoryViewModel } - companion object : MvRxViewModelFactory { - - @JvmStatic - override fun create(viewModelContext: ViewModelContext, state: ViewEditHistoryViewState): ViewEditHistoryViewModel? { - val fragment: ViewEditHistoryBottomSheet = (viewModelContext as FragmentViewModelContext).fragment() - return fragment.viewEditHistoryViewModelFactory.create(state) - } - } + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() init { loadHistory() diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/edithistory/ViewEditHistoryViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/edithistory/ViewEditHistoryViewState.kt index 62f08eef7f..61a400d875 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/edithistory/ViewEditHistoryViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/edithistory/ViewEditHistoryViewState.kt @@ -17,7 +17,7 @@ package im.vector.app.features.home.room.detail.timeline.edithistory import com.airbnb.mvrx.Async -import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.Uninitialized import im.vector.app.features.home.room.detail.timeline.action.TimelineEventFragmentArgs import org.matrix.android.sdk.api.session.events.model.Event @@ -26,8 +26,8 @@ data class ViewEditHistoryViewState( val eventId: String, val roomId: String, val isOriginalAReply: Boolean = false, - val editList: Async> = Uninitialized) - : MvRxState { + val editList: Async> = Uninitialized) : + MavericksState { constructor(args: TimelineEventFragmentArgs) : this(roomId = args.roomId, eventId = args.eventId) } 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 25b5dc34d6..e378969b4a 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 @@ -63,10 +63,9 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde currentPosition: Int, eventIdToHighlight: String?, callback: TimelineEventController.Callback?, - requestModelBuild: () -> Unit) - : BasedMergedItem<*>? { - return if (nextEvent?.root?.getClearType() == EventType.STATE_ROOM_CREATE - && event.isRoomConfiguration(nextEvent.root.getClearContent()?.toModel()?.creator)) { + requestModelBuild: () -> Unit): BasedMergedItem<*>? { + return if (nextEvent?.root?.getClearType() == EventType.STATE_ROOM_CREATE && + event.isRoomConfiguration(nextEvent.root.getClearContent()?.toModel()?.creator)) { // It's the first item before room.create // Collapse all room configuration events buildRoomCreationMergedSummary(currentPosition, items, partialState, event, eventIdToHighlight, requestModelBuild, callback) 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 287cd014e9..98deaaf9c3 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 @@ -69,6 +69,7 @@ import im.vector.app.features.media.ImageContentRenderer import im.vector.app.features.media.VideoContentRenderer import me.gujun.android.span.span import org.commonmark.node.Document +import org.matrix.android.sdk.api.MatrixUrls.isMxcUrl import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.events.model.RelationType import org.matrix.android.sdk.api.session.events.model.toModel @@ -142,8 +143,8 @@ class MessageItemFactory @Inject constructor( val malformedText = stringProvider.getString(R.string.malformed_message) return defaultItemFactory.create(malformedText, informationData, highlight, callback) } - if (messageContent.relatesTo?.type == RelationType.REPLACE - || event.isEncrypted() && event.root.content.toModel()?.relatesTo?.type == RelationType.REPLACE + if (messageContent.relatesTo?.type == RelationType.REPLACE || + event.isEncrypted() && event.root.content.toModel()?.relatesTo?.type == RelationType.REPLACE ) { // This is an edit event, we should display it when debugging as a notice event return noticeItemFactory.create(params) @@ -213,7 +214,7 @@ class MessageItemFactory @Inject constructor( if (informationData.sentByMe && !informationData.sendState.isSent()) { it } else { - it.takeIf { it.startsWith("mxc://") } + it.takeIf { it.isMxcUrl() } } } ?: "" return MessageFileItem_() @@ -244,7 +245,7 @@ class MessageItemFactory @Inject constructor( if (informationData.sentByMe && !informationData.sendState.isSent()) { it } else { - it.takeIf { it.startsWith("mxc://") } + it.takeIf { it.isMxcUrl() } } } ?: "" 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 5a9af975ed..b5831b33b8 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 @@ -46,8 +46,8 @@ class DisplayableEventFormatter @Inject constructor( return noticeEventFormatter.formatRedactedEvent(timelineEvent.root) } - if (timelineEvent.root.isEncrypted() - && timelineEvent.root.mxDecryptionResult == null) { + if (timelineEvent.root.isEncrypted() && + timelineEvent.root.mxDecryptionResult == null) { return stringProvider.getString(R.string.encrypted_message) } 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 c80a92d568..4ca1557208 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 @@ -363,8 +363,8 @@ class NoticeEventFormatter @Inject constructor( 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 + val isMembershipEvent = prevEventContent?.membership != eventContent?.membership || + eventContent?.membership == Membership.LEAVE return if (isMembershipEvent) { buildMembershipNotice(event, senderName, eventContent, prevEventContent, isDm) } else { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/ContentDownloadStateTrackerBinder.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/ContentDownloadStateTrackerBinder.kt index abaaaf2152..caf0131144 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/ContentDownloadStateTrackerBinder.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/ContentDownloadStateTrackerBinder.kt @@ -19,16 +19,16 @@ package im.vector.app.features.home.room.detail.timeline.helper import android.graphics.drawable.Drawable import androidx.vectordrawable.graphics.drawable.Animatable2Compat import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat +import dagger.hilt.android.scopes.ActivityScoped import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder -import im.vector.app.core.di.ScreenScope import im.vector.app.core.error.ErrorFormatter import im.vector.app.features.home.room.detail.timeline.MessageColorProvider import im.vector.app.features.home.room.detail.timeline.item.MessageFileItem import org.matrix.android.sdk.api.session.file.ContentDownloadStateTracker import javax.inject.Inject -@ScreenScope +@ActivityScoped class ContentDownloadStateTrackerBinder @Inject constructor(private val activeSessionHolder: ActiveSessionHolder, private val messageColorProvider: MessageColorProvider, private val errorFormatter: ErrorFormatter) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/ContentUploadStateTrackerBinder.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/ContentUploadStateTrackerBinder.kt index 75570a67a0..0909cbe8de 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/ContentUploadStateTrackerBinder.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/ContentUploadStateTrackerBinder.kt @@ -22,9 +22,9 @@ import android.view.ViewGroup import android.widget.ProgressBar import android.widget.TextView import androidx.core.view.isVisible +import dagger.hilt.android.scopes.ActivityScoped import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder -import im.vector.app.core.di.ScreenScope import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.extensions.exhaustive import im.vector.app.core.utils.TextUtils @@ -33,7 +33,7 @@ import org.matrix.android.sdk.api.session.content.ContentUploadStateTracker import org.matrix.android.sdk.api.session.room.send.SendState import javax.inject.Inject -@ScreenScope +@ActivityScoped class ContentUploadStateTrackerBinder @Inject constructor(private val activeSessionHolder: ActiveSessionHolder, private val messageColorProvider: MessageColorProvider, private val errorFormatter: ErrorFormatter) { 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 da75a808d8..6385494fe1 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 @@ -65,13 +65,13 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses ?: false val showInformation = - addDaySeparator - || event.senderInfo.avatarUrl != nextDisplayableEvent?.senderInfo?.avatarUrl - || event.senderInfo.disambiguatedDisplayName != nextDisplayableEvent?.senderInfo?.disambiguatedDisplayName - || nextDisplayableEvent.root.getClearType() !in listOf(EventType.MESSAGE, EventType.STICKER, EventType.ENCRYPTED) - || isNextMessageReceivedMoreThanOneHourAgo - || isTileTypeMessage(nextDisplayableEvent) - || nextDisplayableEvent.isEdition() + addDaySeparator || + event.senderInfo.avatarUrl != nextDisplayableEvent?.senderInfo?.avatarUrl || + event.senderInfo.disambiguatedDisplayName != nextDisplayableEvent?.senderInfo?.disambiguatedDisplayName || + nextDisplayableEvent.root.getClearType() !in listOf(EventType.MESSAGE, EventType.STICKER, EventType.ENCRYPTED) || + isNextMessageReceivedMoreThanOneHourAgo || + isTileTypeMessage(nextDisplayableEvent) || + nextDisplayableEvent.isEdition() val time = dateFormatter.format(event.root.originServerTs, DateFormatKind.MESSAGE_SIMPLE) val roomSummary = params.partialState.roomSummary @@ -143,10 +143,10 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses private fun getE2EDecoration(roomSummary: RoomSummary?, event: TimelineEvent): E2EDecoration { return if ( - event.root.sendState == SendState.SYNCED - && roomSummary?.isEncrypted.orFalse() + event.root.sendState == SendState.SYNCED && + roomSummary?.isEncrypted.orFalse() && // is user verified - && session.cryptoService().crossSigningService().getUserCrossSigningKeys(event.root.senderId ?: "")?.isTrusted() == true) { + session.cryptoService().crossSigningService().getUserCrossSigningKeys(event.root.senderId ?: "")?.isTrusted() == true) { val ts = roomSummary?.encryptionEventTs ?: 0 val eventTs = event.root.originServerTs ?: 0 if (event.isEncrypted()) { 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 736da63ee2..7165921b35 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 @@ -65,8 +65,8 @@ class TimelineControllerInterceptorHelper(private val positionOfReadMarker: KMut } epoxyModel.getEventIds().forEach { eventId -> adapterPositionMapping[eventId] = index - appendReadMarker = appendReadMarker - || (epoxyModel.canAppendReadMarker() && eventId == firstUnreadEventId && atLeastOneVisibleItemsBeforeReadMarker) + appendReadMarker = appendReadMarker || + (epoxyModel.canAppendReadMarker() && eventId == firstUnreadEventId && atLeastOneVisibleItemsBeforeReadMarker) } } if (epoxyModel is DaySeparatorItem) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineMediaSizeProvider.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineMediaSizeProvider.kt index 52229151a0..9ec61e6054 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineMediaSizeProvider.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineMediaSizeProvider.kt @@ -17,11 +17,11 @@ package im.vector.app.features.home.room.detail.timeline.helper import androidx.recyclerview.widget.RecyclerView -import im.vector.app.core.di.ScreenScope +import dagger.hilt.android.scopes.ActivityScoped import javax.inject.Inject import kotlin.math.roundToInt -@ScreenScope +@ActivityScoped class TimelineMediaSizeProvider @Inject constructor() { var recyclerView: RecyclerView? = null diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineVisibilityStateChangedListeners.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineVisibilityStateChangedListeners.kt index 33e81aeb9b..2337a6ea15 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineVisibilityStateChangedListeners.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineVisibilityStateChangedListeners.kt @@ -21,8 +21,8 @@ import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.features.home.room.detail.timeline.TimelineEventController import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent -class ReadMarkerVisibilityStateChangedListener(private val callback: TimelineEventController.Callback?) - : VectorEpoxyModel.OnVisibilityStateChangedListener { +class ReadMarkerVisibilityStateChangedListener(private val callback: TimelineEventController.Callback?) : + VectorEpoxyModel.OnVisibilityStateChangedListener { override fun onVisibilityStateChanged(visibilityState: Int) { if (visibilityState == VisibilityState.VISIBLE) { @@ -32,8 +32,8 @@ class ReadMarkerVisibilityStateChangedListener(private val callback: TimelineEve } class TimelineEventVisibilityStateChangedListener(private val callback: TimelineEventController.Callback?, - private val event: TimelineEvent) - : VectorEpoxyModel.OnVisibilityStateChangedListener { + private val event: TimelineEvent) : + VectorEpoxyModel.OnVisibilityStateChangedListener { override fun onVisibilityStateChanged(visibilityState: Int) { if (visibilityState == VisibilityState.VISIBLE) { @@ -45,8 +45,8 @@ class TimelineEventVisibilityStateChangedListener(private val callback: Timeline } class MergedTimelineEventVisibilityStateChangedListener(private val callback: TimelineEventController.Callback?, - private val events: List) - : VectorEpoxyModel.OnVisibilityStateChangedListener { + private val events: List) : + VectorEpoxyModel.OnVisibilityStateChangedListener { override fun onVisibilityStateChanged(visibilityState: Int) { if (visibilityState == VisibilityState.VISIBLE) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/VoiceMessagePlaybackTracker.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/VoiceMessagePlaybackTracker.kt index 446d4161e3..2e8f6d9336 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/VoiceMessagePlaybackTracker.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/VoiceMessagePlaybackTracker.kt @@ -18,10 +18,10 @@ package im.vector.app.features.home.room.detail.timeline.helper import android.os.Handler import android.os.Looper -import im.vector.app.core.di.ScreenScope +import dagger.hilt.android.scopes.ActivityScoped import javax.inject.Inject -@ScreenScope +@ActivityScoped class VoiceMessagePlaybackTracker @Inject constructor() { private val mainHandler = Handler(Looper.getMainLooper()) 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 46392a494f..5f8ac822da 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 @@ -32,6 +32,7 @@ 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.features.displayname.getBestName 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 diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MergedRoomCreationItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MergedRoomCreationItem.kt index 2305cc3754..1e8e96426f 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MergedRoomCreationItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MergedRoomCreationItem.kt @@ -166,9 +166,9 @@ abstract class MergedRoomCreationItem : BasedMergedItem= 2)) - && roomItem?.avatarUrl.isNullOrBlank() + val shouldSetAvatar = attributes.canChangeAvatar && + (roomSummary?.isDirect == false || (isDirect && membersCount >= 2)) && + roomItem?.avatarUrl.isNullOrBlank() holder.roomAvatarImageView.isVisible = roomItem != null if (roomItem != null) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/reactions/ViewReactionsBottomSheet.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/reactions/ViewReactionsBottomSheet.kt index 9cc3be9ac1..8071ed8809 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/reactions/ViewReactionsBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/reactions/ViewReactionsBottomSheet.kt @@ -20,11 +20,11 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import com.airbnb.mvrx.MvRx +import com.airbnb.mvrx.Mavericks import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState +import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R -import im.vector.app.core.di.ScreenComponent import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.configureWith import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment @@ -33,27 +33,22 @@ import im.vector.app.features.home.room.detail.timeline.action.EventSharedAction import im.vector.app.features.home.room.detail.timeline.action.MessageSharedActionViewModel import im.vector.app.features.home.room.detail.timeline.action.TimelineEventFragmentArgs import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData - import javax.inject.Inject /** * Bottom sheet displaying list of reactions for a given event ordered by timestamp */ +@AndroidEntryPoint class ViewReactionsBottomSheet : VectorBaseBottomSheetDialogFragment(), ViewReactionsEpoxyController.Listener { private val viewModel: ViewReactionsViewModel by fragmentViewModel(ViewReactionsViewModel::class) - @Inject lateinit var viewReactionsViewModelFactory: ViewReactionsViewModel.Factory private lateinit var sharedActionViewModel: MessageSharedActionViewModel @Inject lateinit var epoxyController: ViewReactionsEpoxyController - override fun injectWith(injector: ScreenComponent) { - injector.inject(this) - } - override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): BottomSheetGenericListWithTitleBinding { return BottomSheetGenericListWithTitleBinding.inflate(inflater, container, false) } @@ -93,7 +88,7 @@ class ViewReactionsBottomSheet : roomId, informationData ) - args.putParcelable(MvRx.KEY_ARG, parcelableArgs) + args.putParcelable(Mavericks.KEY_ARG, parcelableArgs) return ViewReactionsBottomSheet().apply { arguments = args } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/reactions/ViewReactionsEpoxyController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/reactions/ViewReactionsEpoxyController.kt index c67821701a..0b3d381619 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/reactions/ViewReactionsEpoxyController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/reactions/ViewReactionsEpoxyController.kt @@ -32,8 +32,8 @@ import javax.inject.Inject */ class ViewReactionsEpoxyController @Inject constructor( private val stringProvider: StringProvider, - private val emojiCompatWrapper: EmojiCompatWrapper) - : TypedEpoxyController() { + private val emojiCompatWrapper: EmojiCompatWrapper) : + TypedEpoxyController() { var listener: Listener? = null diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/reactions/ViewReactionsViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/reactions/ViewReactionsViewModel.kt index 35d208e30e..1f4d67db03 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/reactions/ViewReactionsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/reactions/ViewReactionsViewModel.kt @@ -17,16 +17,16 @@ package im.vector.app.features.home.room.detail.timeline.reactions import com.airbnb.mvrx.Async -import com.airbnb.mvrx.FragmentViewModelContext -import com.airbnb.mvrx.MvRxState -import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.MavericksState +import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.Uninitialized -import com.airbnb.mvrx.ViewModelContext import dagger.assisted.Assisted -import dagger.assisted.AssistedInject import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import im.vector.app.core.date.DateFormatKind import im.vector.app.core.date.VectorDateFormatter +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.platform.EmptyAction import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel @@ -41,8 +41,8 @@ import org.matrix.android.sdk.rx.unwrap data class DisplayReactionsViewState( val eventId: String, val roomId: String, - val mapReactionKeyToMemberList: Async> = Uninitialized) - : MvRxState { + val mapReactionKeyToMemberList: Async> = Uninitialized) : + MavericksState { constructor(args: TimelineEventFragmentArgs) : this(roomId = args.roomId, eventId = args.eventId) } @@ -70,18 +70,11 @@ class ViewReactionsViewModel @AssistedInject constructor(@Assisted ?: throw IllegalStateException("Shouldn't use this ViewModel without a room") @AssistedFactory - interface Factory { - fun create(initialState: DisplayReactionsViewState): ViewReactionsViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: DisplayReactionsViewState): ViewReactionsViewModel } - companion object : MvRxViewModelFactory { - - @JvmStatic - override fun create(viewModelContext: ViewModelContext, state: DisplayReactionsViewState): ViewReactionsViewModel? { - val fragment: ViewReactionsBottomSheet = (viewModelContext as FragmentViewModelContext).fragment() - return fragment.viewReactionsViewModelFactory.create(state) - } - } + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() init { observeEventAnnotationSummaries() diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/url/PreviewUrlRetriever.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/url/PreviewUrlRetriever.kt index 4c018c8a99..c97fae055d 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/url/PreviewUrlRetriever.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/url/PreviewUrlRetriever.kt @@ -146,7 +146,7 @@ class PreviewUrlRetriever(session: Session, companion object { // One week in millis - private const val CACHE_VALIDITY: Long = 7 * 24 * 3_600 * 1_000 + private const val CACHE_VALIDITY = 604_800_000L // 7 * 24 * 3_600 * 1_000 private val blockedDomains = listOf( "https://matrix.to", diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/url/PreviewUrlView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/url/PreviewUrlView.kt index f5d09efd18..3e08ce5589 100755 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/url/PreviewUrlView.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/url/PreviewUrlView.kt @@ -26,7 +26,6 @@ import im.vector.app.core.extensions.setTextOrHide import im.vector.app.databinding.ViewUrlPreviewBinding import im.vector.app.features.home.room.detail.timeline.TimelineEventController import im.vector.app.features.media.ImageContentRenderer - import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.media.PreviewUrlData diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomBottomSheet.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomBottomSheet.kt index 2fa210a748..03a0e64d9b 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomBottomSheet.kt @@ -27,8 +27,8 @@ import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Success import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState +import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R -import im.vector.app.core.di.ScreenComponent import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.extensions.setTextOrHide import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment @@ -36,9 +36,9 @@ import im.vector.app.databinding.BottomSheetRoomUpgradeBinding import kotlinx.parcelize.Parcelize import javax.inject.Inject +@AndroidEntryPoint class MigrateRoomBottomSheet : - VectorBaseBottomSheetDialogFragment(), - MigrateRoomViewModel.Factory { + VectorBaseBottomSheetDialogFragment() { enum class MigrationReason { MANUAL, @@ -53,20 +53,12 @@ class MigrateRoomBottomSheet : val customDescription: CharSequence? = null ) : Parcelable - @Inject - lateinit var viewModelFactory: MigrateRoomViewModel.Factory - override val showExpanded = true - @Inject - lateinit var errorFormatter: ErrorFormatter + @Inject lateinit var errorFormatter: ErrorFormatter val viewModel: MigrateRoomViewModel by fragmentViewModel() - override fun injectWith(injector: ScreenComponent) { - injector.inject(this) - } - override fun invalidate() = withState(viewModel) { state -> views.headerText.setText(if (state.isPublic) R.string.upgrade_public_room else R.string.upgrade_private_room) @@ -152,10 +144,6 @@ class MigrateRoomBottomSheet : } } - override fun create(initialState: MigrateRoomViewState): MigrateRoomViewModel { - return viewModelFactory.create(initialState) - } - companion object { const val REQUEST_KEY = "MigrateRoomBottomSheetRequest" diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomViewModel.kt index 3dc5d0e3ba..98be65c167 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomViewModel.kt @@ -16,15 +16,14 @@ package im.vector.app.features.home.room.detail.upgrade -import com.airbnb.mvrx.ActivityViewModelContext -import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.Loading -import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.Success -import com.airbnb.mvrx.ViewModelContext import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel import im.vector.app.features.session.coroutineScope @@ -34,8 +33,8 @@ import org.matrix.android.sdk.api.session.Session class MigrateRoomViewModel @AssistedInject constructor( @Assisted initialState: MigrateRoomViewState, private val session: Session, - private val upgradeRoomViewModelTask: UpgradeRoomViewModelTask) - : VectorViewModel(initialState) { + private val upgradeRoomViewModelTask: UpgradeRoomViewModelTask) : + VectorViewModel(initialState) { init { val room = session.getRoom(initialState.roomId) @@ -51,20 +50,11 @@ class MigrateRoomViewModel @AssistedInject constructor( } @AssistedFactory - interface Factory { - fun create(initialState: MigrateRoomViewState): MigrateRoomViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: MigrateRoomViewState): MigrateRoomViewModel } - companion object : MvRxViewModelFactory { - - override fun create(viewModelContext: ViewModelContext, state: MigrateRoomViewState): MigrateRoomViewModel? { - 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") - } - } + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() override fun handle(action: MigrateRoomAction) { when (action) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomViewState.kt index e3ea98682b..6a155aa22a 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomViewState.kt @@ -17,7 +17,7 @@ package im.vector.app.features.home.room.detail.upgrade import com.airbnb.mvrx.Async -import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.Uninitialized data class MigrateRoomViewState( @@ -36,7 +36,7 @@ data class MigrateRoomViewState( val upgradingProgressIndeterminate: Boolean = true, val migrationReason: MigrateRoomBottomSheet.MigrationReason = MigrateRoomBottomSheet.MigrationReason.MANUAL, val autoMigrateMembersAndParents: Boolean = false -) : MvRxState { +) : MavericksState { constructor(args: MigrateRoomBottomSheet.Args) : this( roomId = args.roomId, newVersion = args.newVersion, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/views/RoomDetailLazyLoadedViews.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/views/RoomDetailLazyLoadedViews.kt new file mode 100644 index 0000000000..fafb49ad5c --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/views/RoomDetailLazyLoadedViews.kt @@ -0,0 +1,63 @@ +/* + * 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.views + +import android.view.View +import android.view.ViewStub +import im.vector.app.core.ui.views.FailedMessagesWarningView +import im.vector.app.databinding.FragmentRoomDetailBinding +import im.vector.app.features.invite.VectorInviteView +import kotlin.reflect.KMutableProperty0 + +/** + * This is an holder for lazy loading some views of the RoomDetail screen. + * It's using some ViewStub where it makes sense. + */ +class RoomDetailLazyLoadedViews { + + private var roomDetailBinding: FragmentRoomDetailBinding? = null + + private var failedMessagesWarningView: FailedMessagesWarningView? = null + private var inviteView: VectorInviteView? = null + + fun bind(roomDetailBinding: FragmentRoomDetailBinding) { + this.roomDetailBinding = roomDetailBinding + } + + fun unBind() { + roomDetailBinding = null + inviteView = null + failedMessagesWarningView = null + } + + fun failedMessagesWarningView(inflateIfNeeded: Boolean, callback: FailedMessagesWarningView.Callback? = null): FailedMessagesWarningView? { + return getOrInflate(inflateIfNeeded, roomDetailBinding?.failedMessagesWarningStub, this::failedMessagesWarningView)?.apply { + this.callback = callback + } + } + + fun inviteView(inflateIfNeeded: Boolean): VectorInviteView? { + return getOrInflate(inflateIfNeeded, roomDetailBinding?.inviteViewStub, this::inviteView) + } + + private inline fun getOrInflate(inflateIfNeeded: Boolean, stub: ViewStub?, reference: KMutableProperty0): T? { + if (!inflateIfNeeded || stub == null || stub.parent == null) return reference.get() + val inflatedView = stub.inflate() as T + reference.set(inflatedView) + return inflatedView + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/widget/RoomWidgetsBannerView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/widget/RoomWidgetsBannerView.kt index 4492361832..b934136dc2 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/widget/RoomWidgetsBannerView.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/widget/RoomWidgetsBannerView.kt @@ -22,7 +22,6 @@ import android.view.View import android.widget.RelativeLayout import im.vector.app.R import im.vector.app.databinding.ViewRoomWidgetsBannerBinding - import org.matrix.android.sdk.api.session.widgets.model.Widget class RoomWidgetsBannerView @JvmOverloads constructor( diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/widget/RoomWidgetsBottomSheet.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/widget/RoomWidgetsBottomSheet.kt index 4e77873522..aa6966254f 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/widget/RoomWidgetsBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/widget/RoomWidgetsBottomSheet.kt @@ -22,8 +22,8 @@ import android.view.View import android.view.ViewGroup import com.airbnb.mvrx.parentFragmentViewModel import com.airbnb.mvrx.withState +import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R -import im.vector.app.core.di.ScreenComponent import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.configureWith import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment @@ -33,13 +33,13 @@ import im.vector.app.features.home.room.detail.RoomDetailAction import im.vector.app.features.home.room.detail.RoomDetailViewModel import im.vector.app.features.home.room.detail.RoomDetailViewState import im.vector.app.features.navigation.Navigator - import org.matrix.android.sdk.api.session.widgets.model.Widget import javax.inject.Inject /** * Bottom sheet displaying active widgets in a room */ +@AndroidEntryPoint class RoomWidgetsBottomSheet : VectorBaseBottomSheetDialogFragment(), RoomWidgetsController.Listener { @@ -50,10 +50,6 @@ class RoomWidgetsBottomSheet : private val roomDetailViewModel: RoomDetailViewModel by parentFragmentViewModel() - override fun injectWith(injector: ScreenComponent) { - injector.inject(this) - } - override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): BottomSheetGenericListWithTitleBinding { return BottomSheetGenericListWithTitleBinding.inflate(inflater, container, false) } @@ -65,7 +61,7 @@ class RoomWidgetsBottomSheet : views.bottomSheetTitle.textSize = 20f views.bottomSheetTitle.setTextColor(colorProvider.getColorFromAttribute(R.attr.vctr_content_primary)) epoxyController.listener = this - roomDetailViewModel.asyncSubscribe(this, RoomDetailViewState::activeRoomWidgets) { + roomDetailViewModel.onAsync(RoomDetailViewState::activeRoomWidgets) { epoxyController.setData(it) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/widget/RoomWidgetsController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/widget/RoomWidgetsController.kt index 14f8b53970..2da3dab16a 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/widget/RoomWidgetsController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/widget/RoomWidgetsController.kt @@ -30,8 +30,8 @@ import javax.inject.Inject */ class RoomWidgetsController @Inject constructor( val stringProvider: StringProvider, - val colorProvider: ColorProvider) - : TypedEpoxyController>() { + val colorProvider: ColorProvider) : + TypedEpoxyController>() { var listener: Listener? = null diff --git a/vector/src/main/java/im/vector/app/features/home/room/filtered/FilteredRoomFooterItem.kt b/vector/src/main/java/im/vector/app/features/home/room/filtered/FilteredRoomFooterItem.kt index 5e45004579..4c163f2f56 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/filtered/FilteredRoomFooterItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/filtered/FilteredRoomFooterItem.kt @@ -17,6 +17,7 @@ package im.vector.app.features.home.room.filtered import android.widget.Button +import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R @@ -33,11 +34,21 @@ abstract class FilteredRoomFooterItem : VectorEpoxyModel() { private val roomListFragment: RoomListFragment? @@ -40,10 +41,6 @@ class FilteredRoomsActivity : VectorBaseActivity() override fun getCoordinatorLayout() = views.coordinatorLayout - override fun injectWith(injector: ScreenComponent) { - injector.inject(this) - } - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) configureToolbar(views.filteredRoomsToolbar) diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/CollapsableTypedEpoxyController.kt b/vector/src/main/java/im/vector/app/features/home/room/list/CollapsableTypedEpoxyController.kt index 19d718a3c7..15b3602766 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/CollapsableTypedEpoxyController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/CollapsableTypedEpoxyController.kt @@ -18,8 +18,8 @@ package im.vector.app.features.home.room.list import com.airbnb.epoxy.EpoxyController -abstract class CollapsableTypedEpoxyController - : EpoxyController(), CollapsableControllerExtension { +abstract class CollapsableTypedEpoxyController : + EpoxyController(), CollapsableControllerExtension { private var currentData: T? = null private var allowModelBuildRequests = false @@ -43,8 +43,8 @@ abstract class CollapsableTypedEpoxyController override fun requestModelBuild() { check(allowModelBuildRequests) { - ("You cannot call `requestModelBuild` directly. Call `setData` instead to trigger a " - + "model refresh with new data.") + ("You cannot call `requestModelBuild` directly. Call `setData` instead to trigger a " + + "model refresh with new data.") } super.requestModelBuild() } @@ -57,8 +57,8 @@ abstract class CollapsableTypedEpoxyController override fun requestDelayedModelBuild(delayMs: Int) { check(allowModelBuildRequests) { - ("You cannot call `requestModelBuild` directly. Call `setData` instead to trigger a " - + "model refresh with new data.") + ("You cannot call `requestModelBuild` directly. Call `setData` instead to trigger a " + + "model refresh with new data.") } super.requestDelayedModelBuild(delayMs) } @@ -69,8 +69,8 @@ abstract class CollapsableTypedEpoxyController override fun buildModels() { check(isBuildingModels()) { - ("You cannot call `buildModels` directly. Call `setData` instead to trigger a model " - + "refresh with new data.") + ("You cannot call `buildModels` directly. Call `setData` instead to trigger a model " + + "refresh with new data.") } if (collapsed) { buildModels(null) diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomInvitationItem.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomInvitationItem.kt index b0cc8fec6c..4413776636 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomInvitationItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomInvitationItem.kt @@ -28,6 +28,7 @@ import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.epoxy.onClick import im.vector.app.core.extensions.setTextOrHide import im.vector.app.core.platform.ButtonStateView +import im.vector.app.features.displayname.getBestName import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.invite.InviteButtonStateBinder import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFooterController.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFooterController.kt index 22cd0ae639..00d59f4c37 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFooterController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFooterController.kt @@ -24,6 +24,7 @@ import im.vector.app.core.resources.UserPreferencesProvider import im.vector.app.features.home.RoomListDisplayMode import im.vector.app.features.home.room.filtered.FilteredRoomFooterItem import im.vector.app.features.home.room.filtered.filteredRoomFooterItem +import im.vector.app.space import javax.inject.Inject class RoomListFooterController @Inject constructor( @@ -41,6 +42,7 @@ class RoomListFooterController @Inject constructor( id("filter_footer") listener(host.listener) currentFilter(data.roomFilter) + inSpace(data.currentRoomGrouping.invoke()?.space() != null) } } else -> { diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt index fdfe171439..1c173e12e8 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt @@ -64,7 +64,6 @@ data class RoomListParams( class RoomListFragment @Inject constructor( private val pagedControllerFactory: RoomSummaryPagedControllerFactory, - val roomListViewModelFactory: RoomListViewModel.Factory, private val notificationDrawerManager: NotificationDrawerManager, private val footerController: RoomListFooterController, private val userPreferencesProvider: UserPreferencesProvider @@ -123,7 +122,7 @@ class RoomListFragment @Inject constructor( .subscribe { handleQuickActions(it) } .disposeOnDestroyView() - roomListViewModel.selectSubscribe(viewLifecycleOwner, RoomListViewState::roomMembershipChanges) { ms -> + roomListViewModel.onEach(RoomListViewState::roomMembershipChanges) { ms -> // it's for invites local echo adapterInfosList.filter { it.section.notifyOfLocalEcho } .onEach { @@ -415,8 +414,8 @@ class RoomListFragment @Inject constructor( } private fun checkEmptyState() { - val shouldShowEmpty = adapterInfosList.all { it.sectionHeaderAdapter.roomsSectionData.isHidden } - && !adapterInfosList.any { it.sectionHeaderAdapter.roomsSectionData.isLoading } + val shouldShowEmpty = adapterInfosList.all { it.sectionHeaderAdapter.roomsSectionData.isHidden } && + !adapterInfosList.any { it.sectionHeaderAdapter.roomsSectionData.isLoading } if (shouldShowEmpty) { val emptyState = when (roomListParams.displayMode) { RoomListDisplayMode.NOTIFICATIONS -> { diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilder.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilder.kt index 019a6ceddf..2b3152f8cf 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilder.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilder.kt @@ -19,7 +19,7 @@ package im.vector.app.features.home.room.list import im.vector.app.features.home.RoomListDisplayMode interface RoomListSectionBuilder { - fun buildSections(mode: RoomListDisplayMode) : List + fun buildSections(mode: RoomListDisplayMode): List fun dispose() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt index 11f92284a6..b38f2565b6 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt @@ -17,22 +17,27 @@ package im.vector.app.features.home.room.list import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.viewModelScope import com.airbnb.mvrx.Async import com.airbnb.mvrx.Fail -import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.Loading -import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.Success -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.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider +import im.vector.app.features.displayname.getBestName import im.vector.app.features.invite.AutoAcceptInvites import im.vector.app.features.settings.VectorPreferences import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.query.QueryStringValue @@ -41,12 +46,12 @@ import org.matrix.android.sdk.api.session.room.UpdatableLivePageResult import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState import org.matrix.android.sdk.api.session.room.model.tag.RoomTag import org.matrix.android.sdk.api.session.room.state.isPublic -import org.matrix.android.sdk.rx.rx +import org.matrix.android.sdk.api.util.toMatrixItem +import org.matrix.android.sdk.flow.flow import timber.log.Timber -import javax.inject.Inject -class RoomListViewModel @Inject constructor( - initialState: RoomListViewState, +class RoomListViewModel @AssistedInject constructor( + @Assisted initialState: RoomListViewState, private val session: Session, private val stringProvider: StringProvider, private val appStateHandler: AppStateHandler, @@ -54,8 +59,9 @@ class RoomListViewModel @Inject constructor( private val autoAcceptInvites: AutoAcceptInvites ) : VectorViewModel(initialState) { - interface Factory { - fun create(initialState: RoomListViewState): RoomListViewModel + @AssistedFactory + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: RoomListViewState): RoomListViewModel } private var updatableQuery: UpdatableLivePageResult? = null @@ -72,11 +78,13 @@ class RoomListViewModel @Inject constructor( * If current space is null, will return orphan rooms only */ ORPHANS_IF_SPACE_NULL, + /** * Special case when we don't want to discriminate rooms when current space is null. * In this case return all. */ ALL_IF_SPACE_NULL, + /** Do not filter based on space*/ NONE } @@ -92,8 +100,8 @@ class RoomListViewModel @Inject constructor( ) } - session.rx().liveUser(session.myUserId) - .map { it.getOrNull()?.getBestName() } + session.flow().liveUser(session.myUserId) + .map { it.getOrNull()?.toMatrixItem()?.getBestName() } .distinctUntilChanged() .execute { copy( @@ -103,22 +111,14 @@ class RoomListViewModel @Inject constructor( } private fun observeMembershipChanges() { - session.rx() + session.flow() .liveRoomChangeMembershipState() - .subscribe { - setState { copy(roomMembershipChanges = it) } + .setOnEach { + copy(roomMembershipChanges = it) } - .disposeOnClear() } - companion object : MvRxViewModelFactory { - - @JvmStatic - override fun create(viewModelContext: ViewModelContext, state: RoomListViewState): RoomListViewModel? { - val fragment: RoomListFragment = (viewModelContext as FragmentViewModelContext).fragment() - return fragment.roomListViewModelFactory.create(state) - } - } + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() private val roomListSectionBuilder = if (appStateHandler.getCurrentRoomGroupingMethod() is RoomGroupingMethod.BySpace) { RoomListSectionBuilderSpace( @@ -191,7 +191,7 @@ class RoomListViewModel @Inject constructor( } updatableQuery?.updateQuery { it.copy( - displayName = QueryStringValue.Contains(action.filter, QueryStringValue.Case.INSENSITIVE) + displayName = QueryStringValue.Contains(action.filter, QueryStringValue.Case.NORMALIZED) ) } } @@ -320,7 +320,7 @@ class RoomListViewModel @Inject constructor( private fun String.otherTag(): String? { return when (this) { - RoomTag.ROOM_TAG_FAVOURITE -> RoomTag.ROOM_TAG_LOW_PRIORITY + RoomTag.ROOM_TAG_FAVOURITE -> RoomTag.ROOM_TAG_LOW_PRIORITY RoomTag.ROOM_TAG_LOW_PRIORITY -> RoomTag.ROOM_TAG_FAVOURITE else -> null } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModelFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModelFactory.kt deleted file mode 100644 index 6b269356c7..0000000000 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModelFactory.kt +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2019 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.list - -import im.vector.app.AppStateHandler -import im.vector.app.core.resources.StringProvider -import im.vector.app.features.invite.AutoAcceptInvites -import im.vector.app.features.settings.VectorPreferences -import org.matrix.android.sdk.api.session.Session -import javax.inject.Inject -import javax.inject.Provider - -class RoomListViewModelFactory @Inject constructor(private val session: Provider, - private val appStateHandler: AppStateHandler, - private val stringProvider: StringProvider, - private val vectorPreferences: VectorPreferences, - private val autoAcceptInvites: AutoAcceptInvites) - : RoomListViewModel.Factory { - - override fun create(initialState: RoomListViewState): RoomListViewModel { - return RoomListViewModel( - initialState = initialState, - session = session.get(), - stringProvider = stringProvider, - appStateHandler = appStateHandler, - vectorPreferences = vectorPreferences, - autoAcceptInvites = autoAcceptInvites - ) - } -} diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewState.kt index 68a8b9e515..46ff6c242b 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewState.kt @@ -17,7 +17,7 @@ package im.vector.app.features.home.room.list import com.airbnb.mvrx.Async -import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.Uninitialized import im.vector.app.RoomGroupingMethod import im.vector.app.features.home.RoomListDisplayMode @@ -31,7 +31,7 @@ data class RoomListViewState( val asyncSuggestedRooms: Async> = Uninitialized, val currentUserName: String? = null, val currentRoomGrouping: Async = Uninitialized -) : MvRxState { +) : MavericksState { constructor(args: RoomListParams) : this(displayMode = args.displayMode) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItem.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItem.kt index 6cfd04738a..fd0ccd17e9 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItem.kt @@ -32,10 +32,13 @@ import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.epoxy.onClick import im.vector.app.core.extensions.setTextOrHide +import im.vector.app.core.ui.views.PresenceStateImageView import im.vector.app.core.ui.views.ShieldImageView +import im.vector.app.features.displayname.getBestName import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.themes.ThemeUtils import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel +import org.matrix.android.sdk.api.session.presence.model.UserPresence import org.matrix.android.sdk.api.util.MatrixItem @EpoxyModelClass(layout = R.layout.item_room) @@ -52,6 +55,8 @@ abstract class RoomSummaryItem : VectorEpoxyModel() { @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) lateinit var lastFormattedEvent: CharSequence @EpoxyAttribute lateinit var lastEventTime: CharSequence @EpoxyAttribute var encryptionTrustLevel: RoomEncryptionTrustLevel? = null + @EpoxyAttribute var userPresence: UserPresence? = null + @EpoxyAttribute var showPresence: Boolean = false @EpoxyAttribute var izPublic: Boolean = false @EpoxyAttribute var unreadNotificationCount: Int = 0 @EpoxyAttribute var hasUnreadMessage: Boolean = false @@ -82,6 +87,7 @@ abstract class RoomSummaryItem : VectorEpoxyModel() { renderSelection(holder, showSelected) holder.typingView.setTextOrHide(typingMessage) holder.lastEventView.isInvisible = holder.typingView.isVisible + holder.roomAvatarPresenceImageView.render(showPresence, userPresence) } override fun unbind(holder: Holder) { @@ -116,6 +122,7 @@ abstract class RoomSummaryItem : VectorEpoxyModel() { val roomAvatarDecorationImageView by bind(R.id.roomAvatarDecorationImageView) val roomAvatarPublicDecorationImageView by bind(R.id.roomAvatarPublicDecorationImageView) val roomAvatarFailSendingImageView by bind(R.id.roomAvatarFailSendingImageView) + val roomAvatarPresenceImageView by bind(R.id.roomAvatarPresenceImageView) val rootView by bind(R.id.itemRoomLayout) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt index c06ab33a54..fdb7d3a323 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt @@ -124,6 +124,8 @@ class RoomSummaryItemFactory @Inject constructor(private val displayableEventFor // We do not display shield in the room list anymore // .encryptionTrustLevel(roomSummary.roomEncryptionTrustLevel) .izPublic(roomSummary.isPublic) + .showPresence(roomSummary.isDirect) + .userPresence(roomSummary.directUserPresence) .matrixItem(roomSummary.toMatrixItem()) .lastEventTime(latestEventTime) .typingMessage(typingMessage) diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/actions/RoomListQuickActionsBottomSheet.kt b/vector/src/main/java/im/vector/app/features/home/room/list/actions/RoomListQuickActionsBottomSheet.kt index 8c1bdc086f..014ce14c95 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/actions/RoomListQuickActionsBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/actions/RoomListQuickActionsBottomSheet.kt @@ -26,8 +26,8 @@ import com.airbnb.mvrx.args import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState import com.google.android.material.dialog.MaterialAlertDialogBuilder +import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R -import im.vector.app.core.di.ScreenComponent import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.configureWith @@ -56,13 +56,13 @@ data class RoomListActionsArgs( /** * Bottom sheet fragment that shows room information with list of contextual actions */ +@AndroidEntryPoint class RoomListQuickActionsBottomSheet : VectorBaseBottomSheetDialogFragment(), RoomListQuickActionsEpoxyController.Listener { private lateinit var sharedActionViewModel: RoomListQuickActionsSharedActionViewModel @Inject lateinit var sharedViewPool: RecyclerView.RecycledViewPool - @Inject lateinit var roomNotificationSettingsViewModelFactory: RoomNotificationSettingsViewModel.Factory @Inject lateinit var roomListActionsEpoxyController: RoomListQuickActionsEpoxyController @Inject lateinit var navigator: Navigator @Inject lateinit var errorFormatter: ErrorFormatter @@ -72,10 +72,6 @@ class RoomListQuickActionsBottomSheet : override val showExpanded = true - override fun injectWith(injector: ScreenComponent) { - injector.inject(this) - } - override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): BottomSheetGenericListBinding { return BottomSheetGenericListBinding.inflate(inflater, container, false) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/actions/RoomListQuickActionsSharedAction.kt b/vector/src/main/java/im/vector/app/features/home/room/list/actions/RoomListQuickActionsSharedAction.kt index 6f93599d02..2d61da0dd5 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/actions/RoomListQuickActionsSharedAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/actions/RoomListQuickActionsSharedAction.kt @@ -24,8 +24,8 @@ import im.vector.app.core.platform.VectorSharedAction sealed class RoomListQuickActionsSharedAction( @StringRes val titleRes: Int, @DrawableRes val iconResId: Int?, - val destructive: Boolean = false) - : VectorSharedAction { + val destructive: Boolean = false) : + VectorSharedAction { data class NotificationsAllNoisy(val roomId: String) : RoomListQuickActionsSharedAction( R.string.room_list_quick_actions_notifications_all_noisy, diff --git a/vector/src/main/java/im/vector/app/features/homeserver/HomeServerCapabilitiesViewModel.kt b/vector/src/main/java/im/vector/app/features/homeserver/HomeServerCapabilitiesViewModel.kt index 083b843b33..9223485eff 100644 --- a/vector/src/main/java/im/vector/app/features/homeserver/HomeServerCapabilitiesViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/homeserver/HomeServerCapabilitiesViewModel.kt @@ -16,20 +16,20 @@ package im.vector.app.features.homeserver -import androidx.lifecycle.viewModelScope -import com.airbnb.mvrx.FragmentViewModelContext -import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.ViewModelContext import dagger.assisted.Assisted -import dagger.assisted.AssistedInject import dagger.assisted.AssistedFactory -import im.vector.app.core.di.HasScreenInjector +import dagger.assisted.AssistedInject +import dagger.hilt.EntryPoints +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.SingletonEntryPoint +import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.platform.EmptyAction import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel import im.vector.app.features.raw.wellknown.getElementWellknown import im.vector.app.features.raw.wellknown.isE2EByDefault -import im.vector.app.features.userdirectory.UserListFragment import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.matrix.android.sdk.api.extensions.tryOrNull @@ -44,19 +44,14 @@ class HomeServerCapabilitiesViewModel @AssistedInject constructor( ) : VectorViewModel(initialState) { @AssistedFactory - interface Factory { - fun create(initialState: HomeServerCapabilitiesViewState): HomeServerCapabilitiesViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: HomeServerCapabilitiesViewState): HomeServerCapabilitiesViewModel } - companion object : MvRxViewModelFactory { - @JvmStatic - override fun create(viewModelContext: ViewModelContext, state: HomeServerCapabilitiesViewState): HomeServerCapabilitiesViewModel? { - val fragment: UserListFragment = (viewModelContext as FragmentViewModelContext).fragment() - return fragment.homeServerCapabilitiesViewModelFactory.create(state) - } + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() { - override fun initialState(viewModelContext: ViewModelContext): HomeServerCapabilitiesViewState? { - val session = (viewModelContext.activity as HasScreenInjector).injector().activeSessionHolder().getSafeActiveSession() + override fun initialState(viewModelContext: ViewModelContext): HomeServerCapabilitiesViewState { + val session = EntryPoints.get(viewModelContext.app(), SingletonEntryPoint::class.java).activeSessionHolder().getSafeActiveSession() return HomeServerCapabilitiesViewState( capabilities = session?.getHomeServerCapabilities() ?: HomeServerCapabilities() ) @@ -64,6 +59,7 @@ class HomeServerCapabilitiesViewModel @AssistedInject constructor( } init { + initAdminE2eByDefault() } diff --git a/vector/src/main/java/im/vector/app/features/homeserver/HomeServerCapabilitiesViewState.kt b/vector/src/main/java/im/vector/app/features/homeserver/HomeServerCapabilitiesViewState.kt index 14d19b2e6a..d7ced5e632 100644 --- a/vector/src/main/java/im/vector/app/features/homeserver/HomeServerCapabilitiesViewState.kt +++ b/vector/src/main/java/im/vector/app/features/homeserver/HomeServerCapabilitiesViewState.kt @@ -16,10 +16,10 @@ package im.vector.app.features.homeserver -import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MavericksState import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities data class HomeServerCapabilitiesViewState( val capabilities: HomeServerCapabilities = HomeServerCapabilities(), val isE2EByDefault: Boolean = true -) : MvRxState +) : MavericksState diff --git a/vector/src/main/java/im/vector/app/features/html/PillImageSpan.kt b/vector/src/main/java/im/vector/app/features/html/PillImageSpan.kt index 76947e8d3d..c1040a8cc0 100644 --- a/vector/src/main/java/im/vector/app/features/html/PillImageSpan.kt +++ b/vector/src/main/java/im/vector/app/features/html/PillImageSpan.kt @@ -30,6 +30,7 @@ import com.bumptech.glide.request.transition.Transition import com.google.android.material.chip.ChipDrawable import im.vector.app.R import im.vector.app.core.glide.GlideRequests +import im.vector.app.features.displayname.getBestName import im.vector.app.features.home.AvatarRenderer import org.matrix.android.sdk.api.session.room.send.MatrixItemSpan import org.matrix.android.sdk.api.util.MatrixItem diff --git a/vector/src/main/java/im/vector/app/features/html/PillsPostProcessor.kt b/vector/src/main/java/im/vector/app/features/html/PillsPostProcessor.kt index 716baff757..f8a2ee5137 100644 --- a/vector/src/main/java/im/vector/app/features/html/PillsPostProcessor.kt +++ b/vector/src/main/java/im/vector/app/features/html/PillsPostProcessor.kt @@ -20,8 +20,8 @@ import android.content.Context import android.text.Spannable import android.text.Spanned import dagger.assisted.Assisted -import dagger.assisted.AssistedInject import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.glide.GlideApp import im.vector.app.features.home.AvatarRenderer @@ -35,8 +35,8 @@ import org.matrix.android.sdk.api.util.toMatrixItem class PillsPostProcessor @AssistedInject constructor(@Assisted private val roomId: String?, private val context: Context, private val avatarRenderer: AvatarRenderer, - private val sessionHolder: ActiveSessionHolder) - : EventHtmlRenderer.PostProcessor { + private val sessionHolder: ActiveSessionHolder) : + EventHtmlRenderer.PostProcessor { @AssistedFactory interface Factory { diff --git a/vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomActivity.kt b/vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomActivity.kt index 88998861bc..6f4aff0041 100644 --- a/vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomActivity.kt +++ b/vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomActivity.kt @@ -21,11 +21,11 @@ import android.content.Intent import android.os.Bundle import android.os.Parcelable import android.view.View -import com.airbnb.mvrx.MvRx +import com.airbnb.mvrx.Mavericks import com.airbnb.mvrx.viewModel import com.google.android.material.dialog.MaterialAlertDialogBuilder +import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R -import im.vector.app.core.di.ScreenComponent import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.extensions.addFragment import im.vector.app.core.extensions.addFragmentToBackstack @@ -37,14 +37,10 @@ import im.vector.app.core.utils.onPermissionDeniedSnackbar import im.vector.app.core.utils.registerForPermissionsResult import im.vector.app.core.utils.toast import im.vector.app.features.contactsbook.ContactsBookFragment -import im.vector.app.features.contactsbook.ContactsBookViewModel -import im.vector.app.features.contactsbook.ContactsBookViewState import im.vector.app.features.userdirectory.UserListFragment import im.vector.app.features.userdirectory.UserListFragmentArgs import im.vector.app.features.userdirectory.UserListSharedAction import im.vector.app.features.userdirectory.UserListSharedActionViewModel -import im.vector.app.features.userdirectory.UserListViewModel -import im.vector.app.features.userdirectory.UserListViewState import kotlinx.parcelize.Parcelize import org.matrix.android.sdk.api.failure.Failure import java.net.HttpURLConnection @@ -53,26 +49,13 @@ import javax.inject.Inject @Parcelize data class InviteUsersToRoomArgs(val roomId: String) : Parcelable -class InviteUsersToRoomActivity : SimpleFragmentActivity(), UserListViewModel.Factory, ContactsBookViewModel.Factory, InviteUsersToRoomViewModel.Factory { +@AndroidEntryPoint +class InviteUsersToRoomActivity : SimpleFragmentActivity() { private val viewModel: InviteUsersToRoomViewModel by viewModel() private lateinit var sharedActionViewModel: UserListSharedActionViewModel - @Inject lateinit var userListViewModelFactory: UserListViewModel.Factory - @Inject lateinit var inviteUsersToRoomViewModelFactory: InviteUsersToRoomViewModel.Factory - @Inject lateinit var contactsBookViewModelFactory: ContactsBookViewModel.Factory @Inject lateinit var errorFormatter: ErrorFormatter - override fun injectWith(injector: ScreenComponent) { - super.injectWith(injector) - injector.inject(this) - } - - override fun create(initialState: UserListViewState) = userListViewModelFactory.create(initialState) - - override fun create(initialState: ContactsBookViewState) = contactsBookViewModelFactory.create(initialState) - - override fun create(initialState: InviteUsersToRoomViewState) = inviteUsersToRoomViewModelFactory.create(initialState) - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -165,7 +148,7 @@ class InviteUsersToRoomActivity : SimpleFragmentActivity(), UserListViewModel.Fa fun getIntent(context: Context, roomId: String): Intent { return Intent(context, InviteUsersToRoomActivity::class.java).also { - it.putExtra(MvRx.KEY_ARG, InviteUsersToRoomArgs(roomId)) + it.putExtra(Mavericks.KEY_ARG, InviteUsersToRoomArgs(roomId)) } } } diff --git a/vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomViewModel.kt b/vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomViewModel.kt index 2f20fef3c9..891194040e 100644 --- a/vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomViewModel.kt @@ -16,45 +16,37 @@ package im.vector.app.features.invite -import com.airbnb.mvrx.ActivityViewModelContext -import com.airbnb.mvrx.FragmentViewModelContext -import com.airbnb.mvrx.MvRxViewModelFactory -import com.airbnb.mvrx.ViewModelContext +import com.airbnb.mvrx.MavericksViewModelFactory import dagger.assisted.Assisted -import dagger.assisted.AssistedInject import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import im.vector.app.R +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider import im.vector.app.features.userdirectory.PendingSelection -import io.reactivex.Observable +import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch import org.matrix.android.sdk.api.session.Session -import org.matrix.android.sdk.rx.rx class InviteUsersToRoomViewModel @AssistedInject constructor(@Assisted initialState: InviteUsersToRoomViewState, session: Session, - val stringProvider: StringProvider) - : VectorViewModel(initialState) { + val stringProvider: StringProvider) : + VectorViewModel(initialState) { private val room = session.getRoom(initialState.roomId)!! @AssistedFactory - interface Factory { - fun create(initialState: InviteUsersToRoomViewState): InviteUsersToRoomViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: InviteUsersToRoomViewState): InviteUsersToRoomViewModel } - companion object : MvRxViewModelFactory { - - @JvmStatic - override fun create(viewModelContext: ViewModelContext, state: InviteUsersToRoomViewState): InviteUsersToRoomViewModel? { - 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") - } - } + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() override fun handle(action: InviteUsersToRoomAction) { when (action) { @@ -63,32 +55,33 @@ class InviteUsersToRoomViewModel @AssistedInject constructor(@Assisted } private fun inviteUsersToRoom(selections: Set) { - _viewEvents.post(InviteUsersToRoomViewEvents.Loading) - - Observable.fromIterable(selections).flatMapCompletable { user -> - when (user) { - is PendingSelection.UserPendingSelection -> room.rx().invite(user.user.userId, null) - is PendingSelection.ThreePidPendingSelection -> room.rx().invite3pid(user.threePid) - } - }.subscribe( - { - val successMessage = when (selections.size) { - 1 -> stringProvider.getString(R.string.invitation_sent_to_one_user, - selections.first().getBestName()) - 2 -> stringProvider.getString(R.string.invitations_sent_to_two_users, - selections.first().getBestName(), - selections.last().getBestName()) - else -> stringProvider.getQuantityString(R.plurals.invitations_sent_to_one_and_more_users, - selections.size - 1, - selections.first().getBestName(), - selections.size - 1) + viewModelScope.launch { + _viewEvents.post(InviteUsersToRoomViewEvents.Loading) + selections.asFlow() + .map { user -> + when (user) { + is PendingSelection.UserPendingSelection -> room.invite(user.user.userId, null) + is PendingSelection.ThreePidPendingSelection -> room.invite3pid(user.threePid) + } } - _viewEvents.post(InviteUsersToRoomViewEvents.Success(successMessage)) - }, - { - _viewEvents.post(InviteUsersToRoomViewEvents.Failure(it)) - }) - .disposeOnClear() + .catch { cause -> + _viewEvents.post(InviteUsersToRoomViewEvents.Failure(cause)) + } + .collect { + val successMessage = when (selections.size) { + 1 -> stringProvider.getString(R.string.invitation_sent_to_one_user, + selections.first().getBestName()) + 2 -> stringProvider.getString(R.string.invitations_sent_to_two_users, + selections.first().getBestName(), + selections.last().getBestName()) + else -> stringProvider.getQuantityString(R.plurals.invitations_sent_to_one_and_more_users, + selections.size - 1, + selections.first().getBestName(), + selections.size - 1) + } + _viewEvents.post(InviteUsersToRoomViewEvents.Success(successMessage)) + } + } } fun getUserIdsOfRoomMembers(): Set { diff --git a/vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomViewState.kt b/vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomViewState.kt index cd688f097b..37f0861a83 100644 --- a/vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomViewState.kt +++ b/vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomViewState.kt @@ -17,13 +17,13 @@ package im.vector.app.features.invite import com.airbnb.mvrx.Async -import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.Uninitialized data class InviteUsersToRoomViewState( val roomId: String, val inviteState: Async = Uninitialized -) : MvRxState { +) : MavericksState { constructor(args: InviteUsersToRoomArgs) : this(roomId = args.roomId) } diff --git a/vector/src/main/java/im/vector/app/features/invite/InvitesAcceptor.kt b/vector/src/main/java/im/vector/app/features/invite/InvitesAcceptor.kt index 6e7de1c35b..09eff756d5 100644 --- a/vector/src/main/java/im/vector/app/features/invite/InvitesAcceptor.kt +++ b/vector/src/main/java/im/vector/app/features/invite/InvitesAcceptor.kt @@ -18,10 +18,14 @@ package im.vector.app.features.invite import im.vector.app.ActiveSessionDataSource import im.vector.app.features.session.coroutineScope -import io.reactivex.Observable import io.reactivex.disposables.Disposable import kotlinx.coroutines.async -import kotlinx.coroutines.launch +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.withPermit import org.matrix.android.sdk.api.extensions.orFalse @@ -31,9 +35,8 @@ import org.matrix.android.sdk.api.session.room.Room import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState 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.rx +import org.matrix.android.sdk.flow.flow import timber.log.Timber -import java.util.concurrent.TimeUnit import javax.inject.Inject import javax.inject.Singleton @@ -50,7 +53,7 @@ class InvitesAcceptor @Inject constructor( private lateinit var activeSessionDisposable: Disposable private val shouldRejectRoomIds = mutableSetOf() - private val invitedRoomDisposables = HashMap() + private val activeSessionIds = mutableSetOf() private val semaphore = Semaphore(1) fun initialize() { @@ -71,34 +74,32 @@ class InvitesAcceptor @Inject constructor( if (!autoAcceptInvites.isEnabled) { return } - if (invitedRoomDisposables.containsKey(session.sessionId)) { + if (activeSessionIds.contains(session.sessionId)) { return } + activeSessionIds.add(session.sessionId) session.addListener(this) val roomQueryParams = roomSummaryQueryParams { this.memberships = listOf(Membership.INVITE) } - val rxSession = session.rx() - Observable - .combineLatest( - rxSession.liveRoomSummaries(roomQueryParams), - rxSession.liveRoomChangeMembershipState().debounce(1, TimeUnit.SECONDS), - { invitedRooms, _ -> invitedRooms.map { it.roomId } } - ) + val flowSession = session.flow() + combine( + flowSession.liveRoomSummaries(roomQueryParams), + flowSession.liveRoomChangeMembershipState().debounce(1000) + ) { invitedRooms, _ -> invitedRooms.map { it.roomId } } .filter { it.isNotEmpty() } - .subscribe { invitedRoomIds -> - session.coroutineScope.launch { - semaphore.withPermit { - Timber.v("Invited roomIds: $invitedRoomIds") - for (roomId in invitedRoomIds) { - async { session.joinRoomSafely(roomId) }.start() - } - } - } - } - .also { - invitedRoomDisposables[session.sessionId] = it - } + .onEach { invitedRoomIds -> + joinInvitedRooms(session, invitedRoomIds) + }.launchIn(session.coroutineScope) + } + + private suspend fun joinInvitedRooms(session: Session, invitedRoomIds: List) = coroutineScope { + semaphore.withPermit { + Timber.v("Invited roomIds: $invitedRoomIds") + for (roomId in invitedRoomIds) { + async { session.joinRoomSafely(roomId) }.start() + } + } } private suspend fun Session.joinRoomSafely(roomId: String) { @@ -138,6 +139,6 @@ class InvitesAcceptor @Inject constructor( override fun onSessionStopped(session: Session) { session.removeListener(this) - invitedRoomDisposables.remove(session.sessionId)?.dispose() + activeSessionIds.remove(session.sessionId) } } diff --git a/vector/src/main/java/im/vector/app/features/invite/VectorInviteView.kt b/vector/src/main/java/im/vector/app/features/invite/VectorInviteView.kt index b2919d59b8..d9f1ad343b 100644 --- a/vector/src/main/java/im/vector/app/features/invite/VectorInviteView.kt +++ b/vector/src/main/java/im/vector/app/features/invite/VectorInviteView.kt @@ -21,8 +21,8 @@ import android.util.AttributeSet import android.view.View import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.view.updateLayoutParams +import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R -import im.vector.app.core.di.HasScreenInjector import im.vector.app.databinding.VectorInviteViewBinding import im.vector.app.features.home.AvatarRenderer import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState @@ -30,8 +30,9 @@ import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary import org.matrix.android.sdk.api.util.toMatrixItem import javax.inject.Inject -class VectorInviteView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) - : ConstraintLayout(context, attrs, defStyle) { +@AndroidEntryPoint +class VectorInviteView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) : + ConstraintLayout(context, attrs, defStyle) { interface Callback { fun onAcceptInvite() @@ -49,9 +50,6 @@ class VectorInviteView @JvmOverloads constructor(context: Context, attrs: Attrib var callback: Callback? = null init { - if (context is HasScreenInjector) { - context.injector().inject(this) - } inflate(context, R.layout.vector_invite_view, this) views = VectorInviteViewBinding.bind(this) views.inviteAcceptView.commonClicked = { callback?.onAcceptInvite() } diff --git a/vector/src/main/java/im/vector/app/features/link/LinkHandlerActivity.kt b/vector/src/main/java/im/vector/app/features/link/LinkHandlerActivity.kt index a0b8efd5aa..c22f8eb779 100644 --- a/vector/src/main/java/im/vector/app/features/link/LinkHandlerActivity.kt +++ b/vector/src/main/java/im/vector/app/features/link/LinkHandlerActivity.kt @@ -20,55 +20,59 @@ import android.content.Intent import android.net.Uri import androidx.lifecycle.lifecycleScope import com.google.android.material.dialog.MaterialAlertDialogBuilder +import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder -import im.vector.app.core.di.ScreenComponent import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.utils.toast import im.vector.app.databinding.ActivityProgressBinding +import im.vector.app.features.home.HomeActivity import im.vector.app.features.login.LoginConfig import im.vector.app.features.permalink.PermalinkHandler -import io.reactivex.android.schedulers.AndroidSchedulers import kotlinx.coroutines.launch import org.matrix.android.sdk.api.session.permalinks.PermalinkService import timber.log.Timber -import java.util.concurrent.TimeUnit import javax.inject.Inject /** * Dummy activity used to dispatch the vector URL links. */ +@AndroidEntryPoint class LinkHandlerActivity : VectorBaseActivity() { @Inject lateinit var sessionHolder: ActiveSessionHolder @Inject lateinit var errorFormatter: ErrorFormatter @Inject lateinit var permalinkHandler: PermalinkHandler - override fun injectWith(injector: ScreenComponent) { - injector.inject(this) - } - override fun getBinding() = ActivityProgressBinding.inflate(layoutInflater) override fun initUiAndData() { + handleIntent() + } + + override fun onNewIntent(intent: Intent?) { + super.onNewIntent(intent) + handleIntent() + } + + private fun handleIntent() { val uri = intent.data - - if (uri == null) { - // Should not happen - Timber.w("Uri is null") - finish() - return - } - - if (uri.getQueryParameter(LoginConfig.CONFIG_HS_PARAMETER) != null) { - handleConfigUrl(uri) - } else if (SUPPORTED_HOSTS.contains(uri.host)) { - handleSupportedHostUrl(uri) - } else { - // Other links are not yet handled, but should not come here (manifest configuration error?) - toast(R.string.universal_link_malformed) - finish() + when { + uri == null -> { + // Should not happen + Timber.w("Uri is null") + finish() + } + uri.getQueryParameter(LoginConfig.CONFIG_HS_PARAMETER) != null -> handleConfigUrl(uri) + uri.toString().startsWith(PermalinkService.MATRIX_TO_URL_BASE) -> handleSupportedHostUrl() + uri.toString().startsWith(PermalinkHandler.MATRIX_TO_CUSTOM_SCHEME_URL_BASE) -> handleSupportedHostUrl() + resources.getStringArray(R.array.permalink_supported_hosts).contains(uri.host) -> handleSupportedHostUrl() + else -> { + // Other links are not yet handled, but should not come here (manifest configuration error?) + toast(R.string.universal_link_malformed) + finish() + } } } @@ -81,53 +85,28 @@ class LinkHandlerActivity : VectorBaseActivity() { } } - private fun handleSupportedHostUrl(uri: Uri) { + private fun handleSupportedHostUrl() { + // If we are not logged in, open login screen. + // In the future, we might want to relaunch the process after login. if (!sessionHolder.hasActiveSession()) { - startLoginActivity(uri) - finish() - } else { - convertUriToPermalink(uri)?.let { permalink -> - startPermalinkHandler(permalink) - } ?: run { - // Host is correct but we do not recognize path - Timber.w("Unable to handle this uri: $uri") - finish() - } + startLoginActivity() + return } + + // We forward intent to HomeActivity (singleTask) to avoid the dueling app problem + // https://stackoverflow.com/questions/25884954/deep-linking-and-multiple-app-instances + intent.setClass(this, HomeActivity::class.java) + intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP + startActivity(intent) } /** - * Convert a URL of element web instance to a matrix.to url - * Examples: - * - https://riot.im/develop/#/room/#element-android:matrix.org -> https://matrix.to/#/#element-android:matrix.org - * - https://app.element.io/#/room/#element-android:matrix.org -> https://matrix.to/#/#element-android:matrix.org + * Start the login screen with identity server and homeserver pre-filled, if any */ - private fun convertUriToPermalink(uri: Uri): String? { - val uriString = uri.toString() - val path = SUPPORTED_PATHS.find { it in uriString } ?: return null - return PermalinkService.MATRIX_TO_URL_BASE + uriString.substringAfter(path) - } - - private fun startPermalinkHandler(permalink: String) { - permalinkHandler.launch(this, permalink, buildTask = true) - .delay(500, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { isHandled -> - if (!isHandled) { - toast(R.string.universal_link_malformed) - } - finish() - } - .disposeOnDestroy() - } - - /** - * Start the login screen with identity server and homeserver pre-filled - */ - private fun startLoginActivity(uri: Uri) { + private fun startLoginActivity(uri: Uri? = null) { navigator.openLogin( context = this, - loginConfig = LoginConfig.parse(uri), + loginConfig = uri?.let { LoginConfig.parse(uri) }, flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK ) finish() @@ -173,21 +152,4 @@ class LinkHandlerActivity : VectorBaseActivity() { .setPositiveButton(R.string.ok) { _, _ -> finish() } .show() } - - companion object { - private val SUPPORTED_HOSTS = listOf( - // Regular Element Web instance - "app.element.io", - // Other known instances of Element Web - "develop.element.io", - "staging.element.io", - // Previous Web instance, kept for compatibility reason - "riot.im" - ) - private val SUPPORTED_PATHS = listOf( - "/#/room/", - "/#/user/", - "/#/group/" - ) - } } diff --git a/vector/src/main/java/im/vector/app/features/login/AbstractLoginFragment.kt b/vector/src/main/java/im/vector/app/features/login/AbstractLoginFragment.kt index 3cb5435c17..8b83873142 100644 --- a/vector/src/main/java/im/vector/app/features/login/AbstractLoginFragment.kt +++ b/vector/src/main/java/im/vector/app/features/login/AbstractLoginFragment.kt @@ -37,7 +37,7 @@ import javax.net.ssl.HttpsURLConnection /** * Parent Fragment for all the login/registration screens */ -abstract class AbstractLoginFragment : VectorBaseFragment(), OnBackPressed { +abstract class AbstractLoginFragment : VectorBaseFragment(), OnBackPressed { protected val loginViewModel: LoginViewModel by activityViewModel() @@ -83,8 +83,8 @@ abstract class AbstractLoginFragment : VectorBaseFragment() /* Ignore this error, user has cancelled the action */ Unit is Failure.ServerError -> - if (throwable.error.code == MatrixError.M_FORBIDDEN - && throwable.httpCode == HttpsURLConnection.HTTP_FORBIDDEN /* 403 */) { + if (throwable.error.code == MatrixError.M_FORBIDDEN && + throwable.httpCode == HttpsURLConnection.HTTP_FORBIDDEN /* 403 */) { MaterialAlertDialogBuilder(requireActivity()) .setTitle(R.string.dialog_title_error) .setMessage(getString(R.string.login_registration_disabled)) diff --git a/vector/src/main/java/im/vector/app/features/login/AbstractSSOLoginFragment.kt b/vector/src/main/java/im/vector/app/features/login/AbstractSSOLoginFragment.kt index 3fc5037ae7..8663b7c73f 100644 --- a/vector/src/main/java/im/vector/app/features/login/AbstractSSOLoginFragment.kt +++ b/vector/src/main/java/im/vector/app/features/login/AbstractSSOLoginFragment.kt @@ -25,7 +25,7 @@ import androidx.viewbinding.ViewBinding import com.airbnb.mvrx.withState import im.vector.app.core.utils.openUrlInChromeCustomTab -abstract class AbstractSSOLoginFragment : AbstractLoginFragment() { +abstract class AbstractSSOLoginFragment : AbstractLoginFragment() { // For sso private var customTabsServiceConnection: CustomTabsServiceConnection? = null diff --git a/vector/src/main/java/im/vector/app/features/login/LoginAction.kt b/vector/src/main/java/im/vector/app/features/login/LoginAction.kt index f6e1ccc9bc..70ca49a10e 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginAction.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginAction.kt @@ -23,6 +23,8 @@ import org.matrix.android.sdk.api.auth.registration.RegisterThreePid import org.matrix.android.sdk.internal.network.ssl.Fingerprint sealed class LoginAction : VectorViewModelAction { + data class OnGetStarted(val resetLoginConfig: Boolean) : LoginAction() + data class UpdateServerType(val serverType: ServerType) : LoginAction() data class UpdateHomeServer(val homeServerUrl: String) : LoginAction() data class UpdateSignMode(val signMode: SignMode) : LoginAction() diff --git a/vector/src/main/java/im/vector/app/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/app/features/login/LoginActivity.kt index 07dfc62602..0ca076ccd7 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginActivity.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginActivity.kt @@ -20,8 +20,6 @@ import android.content.Context import android.content.Intent import android.view.View import android.view.ViewGroup -import androidx.annotation.CallSuper -import com.google.android.material.appbar.MaterialToolbar import androidx.core.view.ViewCompat import androidx.core.view.children import androidx.core.view.isVisible @@ -30,9 +28,10 @@ import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentTransaction import com.airbnb.mvrx.viewModel import com.airbnb.mvrx.withState +import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.dialog.MaterialAlertDialogBuilder +import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R -import im.vector.app.core.di.ScreenComponent import im.vector.app.core.extensions.POP_BACK_STACK_EXCLUSIVE import im.vector.app.core.extensions.addFragment import im.vector.app.core.extensions.addFragmentToBackstack @@ -45,26 +44,18 @@ import im.vector.app.features.login.terms.LoginTermsFragment import im.vector.app.features.login.terms.LoginTermsFragmentArgument import im.vector.app.features.login.terms.toLocalizedLoginTerms import im.vector.app.features.pin.UnlockedActivity - import org.matrix.android.sdk.api.auth.registration.FlowResult import org.matrix.android.sdk.api.auth.registration.Stage import org.matrix.android.sdk.api.extensions.tryOrNull -import javax.inject.Inject /** * The LoginActivity manages the fragment navigation and also display the loading View */ +@AndroidEntryPoint open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable, UnlockedActivity { private val loginViewModel: LoginViewModel by viewModel() - @Inject lateinit var loginViewModelFactory: LoginViewModel.Factory - - @CallSuper - override fun injectWith(injector: ScreenComponent) { - injector.inject(this) - } - private val enterAnim = R.anim.enter_fade_in private val exitAnim = R.anim.exit_fade_out @@ -104,7 +95,6 @@ open class LoginActivity : VectorBaseActivity(), ToolbarCo // Get config extra val loginConfig = intent.getParcelableExtra(EXTRA_CONFIG) if (isFirstCreation()) { - // TODO Check this loginViewModel.handle(LoginAction.InitWith(loginConfig)) } } diff --git a/vector/src/main/java/im/vector/app/features/login/LoginFragment.kt b/vector/src/main/java/im/vector/app/features/login/LoginFragment.kt index 016e4ca0c7..60f02cb2c6 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginFragment.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginFragment.kt @@ -37,7 +37,6 @@ import im.vector.app.core.extensions.toReducedUrl import im.vector.app.databinding.FragmentLoginBinding import io.reactivex.Observable import io.reactivex.rxkotlin.subscribeBy - import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.MatrixError import org.matrix.android.sdk.api.failure.isInvalidPassword @@ -194,7 +193,7 @@ class LoginFragment @Inject constructor() : AbstractSSOLoginFragment { val error = state.asyncLoginAction.error - if (error is Failure.ServerError - && error.error.code == MatrixError.M_FORBIDDEN - && error.error.message.isEmpty()) { + if (error is Failure.ServerError && + error.error.code == MatrixError.M_FORBIDDEN && + error.error.message.isEmpty()) { // Login with email, but email unknown views.loginFieldTil.error = getString(R.string.login_login_with_email_error) } else { diff --git a/vector/src/main/java/im/vector/app/features/login/LoginMode.kt b/vector/src/main/java/im/vector/app/features/login/LoginMode.kt index 2be198ab96..00945968fb 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginMode.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginMode.kt @@ -29,7 +29,7 @@ sealed class LoginMode : Parcelable @Parcelize object Unsupported : LoginMode() } -fun LoginMode.ssoIdentityProviders() : List? { +fun LoginMode.ssoIdentityProviders(): List? { return when (this) { is LoginMode.Sso -> ssoIdentityProviders is LoginMode.SsoAndPassword -> ssoIdentityProviders @@ -37,7 +37,7 @@ fun LoginMode.ssoIdentityProviders() : List? { } } -fun LoginMode.hasSso() : Boolean { +fun LoginMode.hasSso(): Boolean { return when (this) { is LoginMode.Sso -> true is LoginMode.SsoAndPassword -> true diff --git a/vector/src/main/java/im/vector/app/features/login/LoginResetPasswordFragment.kt b/vector/src/main/java/im/vector/app/features/login/LoginResetPasswordFragment.kt index 07c27ab296..cc113b1eed 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginResetPasswordFragment.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginResetPasswordFragment.kt @@ -27,13 +27,12 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.jakewharton.rxbinding3.widget.textChanges import im.vector.app.R import im.vector.app.core.extensions.hideKeyboard -import im.vector.app.core.extensions.isEmail import im.vector.app.core.extensions.hidePassword +import im.vector.app.core.extensions.isEmail import im.vector.app.core.extensions.toReducedUrl import im.vector.app.databinding.FragmentLoginResetPasswordBinding import io.reactivex.Observable import io.reactivex.rxkotlin.subscribeBy - import javax.inject.Inject /** diff --git a/vector/src/main/java/im/vector/app/features/login/LoginResetPasswordMailConfirmationFragment.kt b/vector/src/main/java/im/vector/app/features/login/LoginResetPasswordMailConfirmationFragment.kt index c5551bce91..67a7247130 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginResetPasswordMailConfirmationFragment.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginResetPasswordMailConfirmationFragment.kt @@ -25,7 +25,6 @@ import com.airbnb.mvrx.Success import com.google.android.material.dialog.MaterialAlertDialogBuilder import im.vector.app.R import im.vector.app.databinding.FragmentLoginResetPasswordMailConfirmationBinding - import org.matrix.android.sdk.api.failure.is401 import javax.inject.Inject diff --git a/vector/src/main/java/im/vector/app/features/login/LoginResetPasswordSuccessFragment.kt b/vector/src/main/java/im/vector/app/features/login/LoginResetPasswordSuccessFragment.kt index 059195dbce..8300326b27 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginResetPasswordSuccessFragment.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginResetPasswordSuccessFragment.kt @@ -21,7 +21,6 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import im.vector.app.databinding.FragmentLoginResetPasswordSuccessBinding - import javax.inject.Inject /** diff --git a/vector/src/main/java/im/vector/app/features/login/LoginServerSelectionFragment.kt b/vector/src/main/java/im/vector/app/features/login/LoginServerSelectionFragment.kt index 0c81c47129..89d8985a81 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginServerSelectionFragment.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginServerSelectionFragment.kt @@ -23,7 +23,6 @@ import android.view.ViewGroup import im.vector.app.R import im.vector.app.core.utils.openUrlInChromeCustomTab import im.vector.app.databinding.FragmentLoginServerSelectionBinding - import me.gujun.android.span.span import javax.inject.Inject @@ -88,10 +87,5 @@ class LoginServerSelectionFragment @Inject constructor() : AbstractLoginFragment override fun updateWithState(state: LoginViewState) { updateSelectedChoice(state) - - if (state.loginMode != LoginMode.Unknown) { - // LoginFlow for matrix.org has been retrieved - loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OnLoginFlowRetrieved)) - } } } diff --git a/vector/src/main/java/im/vector/app/features/login/LoginServerUrlFormFragment.kt b/vector/src/main/java/im/vector/app/features/login/LoginServerUrlFormFragment.kt index 538e4700de..ebe82b1163 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginServerUrlFormFragment.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginServerUrlFormFragment.kt @@ -33,7 +33,6 @@ import im.vector.app.core.extensions.hideKeyboard import im.vector.app.core.utils.ensureProtocol import im.vector.app.core.utils.openUrlInChromeCustomTab import im.vector.app.databinding.FragmentLoginServerUrlFormBinding - import org.matrix.android.sdk.api.failure.Failure import java.net.UnknownHostException import javax.inject.Inject @@ -144,8 +143,8 @@ class LoginServerUrlFormFragment @Inject constructor() : AbstractLoginFragment { views.loginSignupSigninSignInSocialLoginContainer.isVisible = true - views.loginSignupSigninSocialLoginButtons.ssoIdentityProviders = state.loginMode.ssoIdentityProviders() + views.loginSignupSigninSocialLoginButtons.ssoIdentityProviders = state.loginMode.ssoIdentityProviders()?.sorted() views.loginSignupSigninSocialLoginButtons.listener = object : SocialLoginButtonsView.InteractionListener { override fun onProviderSelected(id: String?) { loginViewModel.getSsoUrl( diff --git a/vector/src/main/java/im/vector/app/features/login/LoginSplashFragment.kt b/vector/src/main/java/im/vector/app/features/login/LoginSplashFragment.kt index bafe836a8d..527f9f99b3 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginSplashFragment.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginSplashFragment.kt @@ -22,9 +22,13 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.core.view.isVisible +import com.google.android.material.dialog.MaterialAlertDialogBuilder import im.vector.app.BuildConfig +import im.vector.app.R import im.vector.app.databinding.FragmentLoginSplashBinding import im.vector.app.features.settings.VectorPreferences +import org.matrix.android.sdk.api.failure.Failure +import java.net.UnknownHostException import javax.inject.Inject /** @@ -45,7 +49,7 @@ class LoginSplashFragment @Inject constructor( } private fun setupViews() { - views.loginSplashSubmit.setOnClickListener { getStarted() } + views.loginSplashSubmit.debouncedClicks { getStarted() } if (BuildConfig.DEBUG || vectorPreferences.developerMode()) { views.loginSplashVersion.isVisible = true @@ -57,10 +61,28 @@ class LoginSplashFragment @Inject constructor( } private fun getStarted() { - loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OpenServerSelection)) + loginViewModel.handle(LoginAction.OnGetStarted(resetLoginConfig = false)) } override fun resetViewModel() { // Nothing to do } + + override fun onError(throwable: Throwable) { + if (throwable is Failure.NetworkConnection && + throwable.ioException is UnknownHostException) { + // Invalid homeserver from URL config + val url = loginViewModel.getInitialHomeServerUrl().orEmpty() + MaterialAlertDialogBuilder(requireActivity()) + .setTitle(R.string.dialog_title_error) + .setMessage(getString(R.string.login_error_homeserver_from_url_not_found, url)) + .setPositiveButton(R.string.login_error_homeserver_from_url_not_found_enter_manual) { _, _ -> + loginViewModel.handle(LoginAction.OnGetStarted(resetLoginConfig = true)) + } + .setNegativeButton(R.string.cancel, null) + .show() + } else { + super.onError(throwable) + } + } } diff --git a/vector/src/main/java/im/vector/app/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/app/features/login/LoginViewModel.kt index d4fd3101aa..bfa924c155 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginViewModel.kt @@ -18,26 +18,23 @@ package im.vector.app.features.login import android.content.Context import android.net.Uri -import androidx.fragment.app.FragmentActivity -import androidx.lifecycle.viewModelScope -import com.airbnb.mvrx.ActivityViewModelContext import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Loading -import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized -import com.airbnb.mvrx.ViewModelContext import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.extensions.configureAndStart import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider import im.vector.app.core.utils.ensureTrailingSlash -import im.vector.app.features.signout.soft.SoftLogoutActivity import kotlinx.coroutines.Job import kotlinx.coroutines.launch import org.matrix.android.sdk.api.MatrixPatterns.getDomain @@ -72,10 +69,12 @@ class LoginViewModel @AssistedInject constructor( ) : VectorViewModel(initialState) { @AssistedFactory - interface Factory { - fun create(initialState: LoginViewState): LoginViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: LoginViewState): LoginViewModel } + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() + init { getKnownCustomHomeServersUrls() } @@ -86,18 +85,6 @@ class LoginViewModel @AssistedInject constructor( } } - companion object : MvRxViewModelFactory { - - @JvmStatic - override fun create(viewModelContext: ViewModelContext, state: LoginViewState): LoginViewModel? { - return when (val activity: FragmentActivity = (viewModelContext as ActivityViewModelContext).activity()) { - is LoginActivity -> activity.loginViewModelFactory.create(state) - is SoftLogoutActivity -> activity.loginViewModelFactory.create(state) - else -> error("Invalid Activity") - } - } - } - // Store the last action, to redo it after user has trusted the untrusted certificate private var lastAction: LoginAction? = null private var currentHomeServerConnectionConfig: HomeServerConnectionConfig? = null @@ -128,6 +115,7 @@ class LoginViewModel @AssistedInject constructor( override fun handle(action: LoginAction) { when (action) { + is LoginAction.OnGetStarted -> handleOnGetStarted(action) is LoginAction.UpdateServerType -> handleUpdateServerType(action) is LoginAction.UpdateSignMode -> handleUpdateSignMode(action) is LoginAction.InitWith -> handleInitWith(action) @@ -146,6 +134,27 @@ class LoginViewModel @AssistedInject constructor( }.exhaustive } + private fun handleOnGetStarted(action: LoginAction.OnGetStarted) { + if (action.resetLoginConfig) { + loginConfig = null + } + + val configUrl = loginConfig?.homeServerUrl?.takeIf { it.isNotEmpty() } + if (configUrl != null) { + // Use config from uri + val homeServerConnectionConfig = homeServerConnectionConfigFactory.create(configUrl) + if (homeServerConnectionConfig == null) { + // Url is invalid, in this case, just use the regular flow + Timber.w("Url from config url was invalid: $configUrl") + _viewEvents.post(LoginViewEvents.OpenServerSelection) + } else { + getLoginFlow(homeServerConnectionConfig, ServerType.Other) + } + } else { + _viewEvents.post(LoginViewEvents.OpenServerSelection) + } + } + private fun handleUserAcceptCertificate(action: LoginAction.UserAcceptCertificate) { // It happens when we get the login flow, or during direct authentication. // So alter the homeserver config and retrieve again the login flow @@ -692,8 +701,8 @@ class LoginViewModel @AssistedInject constructor( private fun onFlowResponse(flowResult: FlowResult) { // If dummy stage is mandatory, and password is already sent, do the dummy stage now - if (isRegistrationStarted - && flowResult.missingStages.any { it is Stage.Dummy && it.mandatory }) { + if (isRegistrationStarted && + flowResult.missingStages.any { it is Stage.Dummy && it.mandatory }) { handleRegisterDummy() } else { // Notify the user @@ -744,7 +753,8 @@ class LoginViewModel @AssistedInject constructor( } } - private fun getLoginFlow(homeServerConnectionConfig: HomeServerConnectionConfig) { + private fun getLoginFlow(homeServerConnectionConfig: HomeServerConnectionConfig, + serverTypeOverride: ServerType? = null) { currentHomeServerConnectionConfig = homeServerConnectionConfig currentJob = viewModelScope.launch { @@ -755,7 +765,11 @@ class LoginViewModel @AssistedInject constructor( asyncHomeServerLoginFlowRequest = Loading(), // If user has entered https://matrix.org, ensure that server type is ServerType.MatrixOrg // It is also useful to set the value again in the case of a certificate error on matrix.org - serverType = if (homeServerConnectionConfig.homeServerUri.toString() == matrixOrgUrl) ServerType.MatrixOrg else serverType + serverType = if (homeServerConnectionConfig.homeServerUri.toString() == matrixOrgUrl) { + ServerType.MatrixOrg + } else { + serverTypeOverride ?: serverType + } ) } @@ -781,14 +795,13 @@ class LoginViewModel @AssistedInject constructor( val loginMode = when { // SSO login is taken first - data.supportedLoginTypes.contains(LoginFlowTypes.SSO) - && data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.SsoAndPassword(data.ssoIdentityProviders) - data.supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso(data.ssoIdentityProviders) - data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.Password - else -> LoginMode.Unsupported + data.supportedLoginTypes.contains(LoginFlowTypes.SSO) && + data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.SsoAndPassword(data.ssoIdentityProviders) + data.supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso(data.ssoIdentityProviders) + data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.Password + else -> LoginMode.Unsupported } - // FIXME We should post a view event here normally? setState { copy( asyncHomeServerLoginFlowRequest = Uninitialized, @@ -798,11 +811,12 @@ class LoginViewModel @AssistedInject constructor( loginModeSupportedTypes = data.supportedLoginTypes.toList() ) } - if ((loginMode == LoginMode.Password && !data.isLoginAndRegistrationSupported) - || data.isOutdatedHomeserver) { + if ((loginMode == LoginMode.Password && !data.isLoginAndRegistrationSupported) || + data.isOutdatedHomeserver) { // Notify the UI _viewEvents.post(LoginViewEvents.OutdatedHomeserver) } + _viewEvents.post(LoginViewEvents.OnLoginFlowRetrieved) } } diff --git a/vector/src/main/java/im/vector/app/features/login/LoginViewState.kt b/vector/src/main/java/im/vector/app/features/login/LoginViewState.kt index 187d39d9eb..23689225b5 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginViewState.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginViewState.kt @@ -18,7 +18,7 @@ package im.vector.app.features.login import com.airbnb.mvrx.Async import com.airbnb.mvrx.Loading -import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.PersistState import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized @@ -51,20 +51,20 @@ data class LoginViewState( // Network result @PersistState val loginMode: LoginMode = LoginMode.Unknown, - @PersistState // Supported types for the login. We cannot use a sealed class for LoginType because it is not serializable + @PersistState val loginModeSupportedTypes: List = emptyList(), val knownCustomHomeServersUrls: List = emptyList() -) : MvRxState { +) : MavericksState { fun isLoading(): Boolean { - return asyncLoginAction is Loading - || asyncHomeServerLoginFlowRequest is Loading - || asyncResetPassword is Loading - || asyncResetMailConfirmed is Loading - || asyncRegistration is Loading + return asyncLoginAction is Loading || + asyncHomeServerLoginFlowRequest is Loading || + asyncResetPassword is Loading || + asyncResetMailConfirmed is Loading || + asyncRegistration is Loading || // Keep loading when it is success because of the delay to switch to the next Activity - || asyncLoginAction is Success + asyncLoginAction is Success } fun isUserLogged(): Boolean { diff --git a/vector/src/main/java/im/vector/app/features/login/LoginWebFragment.kt b/vector/src/main/java/im/vector/app/features/login/LoginWebFragment.kt index f01bf01029..daa97d732f 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginWebFragment.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginWebFragment.kt @@ -37,7 +37,6 @@ import im.vector.app.core.utils.AssetReader import im.vector.app.databinding.FragmentLoginWebBinding import im.vector.app.features.signout.soft.SoftLogoutAction import im.vector.app.features.signout.soft.SoftLogoutViewModel - import org.matrix.android.sdk.api.auth.data.Credentials import org.matrix.android.sdk.internal.di.MoshiProvider import timber.log.Timber @@ -52,6 +51,8 @@ class LoginWebFragment @Inject constructor( private val assetReader: AssetReader ) : AbstractLoginFragment() { + val softLogoutViewModel: SoftLogoutViewModel by activityViewModel() + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginWebBinding { return FragmentLoginWebBinding.inflate(inflater, container, false) } @@ -233,7 +234,6 @@ class LoginWebFragment @Inject constructor( private fun notifyViewModel(credentials: Credentials) { if (isForSessionRecovery) { - val softLogoutViewModel: SoftLogoutViewModel by activityViewModel() softLogoutViewModel.handle(SoftLogoutAction.WebLoginSuccess(credentials)) } else { loginViewModel.handle(LoginAction.WebLoginSuccess(credentials)) diff --git a/vector/src/main/java/im/vector/app/features/login/SocialLoginButtonsView.kt b/vector/src/main/java/im/vector/app/features/login/SocialLoginButtonsView.kt index 4dc688ad22..f40cad9ec5 100644 --- a/vector/src/main/java/im/vector/app/features/login/SocialLoginButtonsView.kt +++ b/vector/src/main/java/im/vector/app/features/login/SocialLoginButtonsView.kt @@ -27,8 +27,8 @@ import com.google.android.material.button.MaterialButton import im.vector.app.R import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider -class SocialLoginButtonsView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) - : LinearLayout(context, attrs, defStyle) { +class SocialLoginButtonsView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) : + LinearLayout(context, attrs, defStyle) { interface InteractionListener { fun onProviderSelected(id: String?) diff --git a/vector/src/main/java/im/vector/app/features/login/SupportedStage.kt b/vector/src/main/java/im/vector/app/features/login/SupportedStage.kt index d9036f9ee4..5ff00f7e85 100644 --- a/vector/src/main/java/im/vector/app/features/login/SupportedStage.kt +++ b/vector/src/main/java/im/vector/app/features/login/SupportedStage.kt @@ -22,9 +22,9 @@ import org.matrix.android.sdk.api.auth.registration.Stage * Stage.Other is not supported, as well as any other new stages added to the SDK before it is added to the list below */ fun Stage.isSupported(): Boolean { - return this is Stage.ReCaptcha - || this is Stage.Dummy - || this is Stage.Msisdn - || this is Stage.Terms - || this is Stage.Email + return this is Stage.ReCaptcha || + this is Stage.Dummy || + this is Stage.Msisdn || + this is Stage.Terms || + this is Stage.Email } diff --git a/vector/src/main/java/im/vector/app/features/login/terms/LoginTermsViewState.kt b/vector/src/main/java/im/vector/app/features/login/terms/LoginTermsViewState.kt index c09c76efc3..3641b443e3 100644 --- a/vector/src/main/java/im/vector/app/features/login/terms/LoginTermsViewState.kt +++ b/vector/src/main/java/im/vector/app/features/login/terms/LoginTermsViewState.kt @@ -16,12 +16,12 @@ package im.vector.app.features.login.terms -import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MavericksState import org.matrix.android.sdk.internal.auth.registration.LocalizedFlowDataLoginTerms data class LoginTermsViewState( val localizedFlowDataLoginTermsChecked: List -) : MvRxState { +) : MavericksState { fun check(data: LocalizedFlowDataLoginTerms) { localizedFlowDataLoginTermsChecked.find { it.localizedFlowDataLoginTerms == data }?.checked = true } diff --git a/vector/src/main/java/im/vector/app/features/login2/AbstractSSOLoginFragment2.kt b/vector/src/main/java/im/vector/app/features/login2/AbstractSSOLoginFragment2.kt index b12d9638dc..43f301d9b4 100644 --- a/vector/src/main/java/im/vector/app/features/login2/AbstractSSOLoginFragment2.kt +++ b/vector/src/main/java/im/vector/app/features/login2/AbstractSSOLoginFragment2.kt @@ -27,7 +27,7 @@ import im.vector.app.core.utils.openUrlInChromeCustomTab import im.vector.app.features.login.hasSso import im.vector.app.features.login.ssoIdentityProviders -abstract class AbstractSSOLoginFragment2 : AbstractLoginFragment2() { +abstract class AbstractSSOLoginFragment2 : AbstractLoginFragment2() { // For sso private var customTabsServiceConnection: CustomTabsServiceConnection? = null diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginActivity2.kt b/vector/src/main/java/im/vector/app/features/login2/LoginActivity2.kt index 60eb1934d0..40dd1d2872 100644 --- a/vector/src/main/java/im/vector/app/features/login2/LoginActivity2.kt +++ b/vector/src/main/java/im/vector/app/features/login2/LoginActivity2.kt @@ -20,8 +20,6 @@ import android.content.Context import android.content.Intent import android.view.View import android.view.ViewGroup -import androidx.annotation.CallSuper -import com.google.android.material.appbar.MaterialToolbar import androidx.core.view.ViewCompat import androidx.core.view.children import androidx.core.view.isVisible @@ -29,9 +27,10 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentTransaction import com.airbnb.mvrx.viewModel +import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.dialog.MaterialAlertDialogBuilder +import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R -import im.vector.app.core.di.ScreenComponent import im.vector.app.core.extensions.POP_BACK_STACK_EXCLUSIVE import im.vector.app.core.extensions.addFragment import im.vector.app.core.extensions.addFragmentToBackstack @@ -52,26 +51,18 @@ import im.vector.app.features.login.terms.toLocalizedLoginTerms import im.vector.app.features.login2.created.AccountCreatedFragment import im.vector.app.features.login2.terms.LoginTermsFragment2 import im.vector.app.features.pin.UnlockedActivity - import org.matrix.android.sdk.api.auth.registration.FlowResult import org.matrix.android.sdk.api.auth.registration.Stage import org.matrix.android.sdk.api.extensions.tryOrNull -import javax.inject.Inject /** * The LoginActivity manages the fragment navigation and also display the loading View */ +@AndroidEntryPoint open class LoginActivity2 : VectorBaseActivity(), ToolbarConfigurable, UnlockedActivity { private val loginViewModel: LoginViewModel2 by viewModel() - @Inject lateinit var loginViewModelFactory: LoginViewModel2.Factory - - @CallSuper - override fun injectWith(injector: ScreenComponent) { - injector.inject(this) - } - private val enterAnim = R.anim.enter_fade_in private val exitAnim = R.anim.exit_fade_out diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginFragmentSigninUsername2.kt b/vector/src/main/java/im/vector/app/features/login2/LoginFragmentSigninUsername2.kt index 10fe0aae3a..95862a4e90 100644 --- a/vector/src/main/java/im/vector/app/features/login2/LoginFragmentSigninUsername2.kt +++ b/vector/src/main/java/im/vector/app/features/login2/LoginFragmentSigninUsername2.kt @@ -95,9 +95,9 @@ class LoginFragmentSigninUsername2 @Inject constructor() : AbstractLoginFragment } override fun onError(throwable: Throwable) { - if (throwable is Failure.ServerError - && throwable.error.code == MatrixError.M_FORBIDDEN - && throwable.error.message.isEmpty()) { + if (throwable is Failure.ServerError && + throwable.error.code == MatrixError.M_FORBIDDEN && + throwable.error.message.isEmpty()) { // Login with email, but email unknown views.loginFieldTil.error = getString(R.string.login_login_with_email_error) } else { diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginFragmentToAny2.kt b/vector/src/main/java/im/vector/app/features/login2/LoginFragmentToAny2.kt index 8a12b7e9b6..a889c870a0 100644 --- a/vector/src/main/java/im/vector/app/features/login2/LoginFragmentToAny2.kt +++ b/vector/src/main/java/im/vector/app/features/login2/LoginFragmentToAny2.kt @@ -162,13 +162,13 @@ class LoginFragmentToAny2 @Inject constructor() : AbstractSSOLoginFragment2) : LoginViewEvents2() @@ -57,7 +57,7 @@ sealed class LoginViewEvents2 : VectorViewEvents { data class OnWebLoginError(val errorCode: Int, val description: String, val failingUrl: String) : LoginViewEvents2() - data class OnSessionCreated(val newAccount: Boolean): LoginViewEvents2() + data class OnSessionCreated(val newAccount: Boolean) : LoginViewEvents2() object Finish : LoginViewEvents2() } diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginViewModel2.kt b/vector/src/main/java/im/vector/app/features/login2/LoginViewModel2.kt index e3f97a1c01..b73988126b 100644 --- a/vector/src/main/java/im/vector/app/features/login2/LoginViewModel2.kt +++ b/vector/src/main/java/im/vector/app/features/login2/LoginViewModel2.kt @@ -18,17 +18,16 @@ package im.vector.app.features.login2 import android.content.Context import android.net.Uri -import androidx.fragment.app.FragmentActivity import androidx.lifecycle.viewModelScope -import com.airbnb.mvrx.ActivityViewModelContext import com.airbnb.mvrx.Loading -import com.airbnb.mvrx.MvRxViewModelFactory -import com.airbnb.mvrx.ViewModelContext +import com.airbnb.mvrx.MavericksViewModelFactory import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.extensions.configureAndStart import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.tryAsync @@ -72,10 +71,12 @@ class LoginViewModel2 @AssistedInject constructor( ) : VectorViewModel(initialState) { @AssistedFactory - interface Factory { - fun create(initialState: LoginViewState2): LoginViewModel2 + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: LoginViewState2): LoginViewModel2 } + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() + init { getKnownCustomHomeServersUrls() } @@ -86,18 +87,6 @@ class LoginViewModel2 @AssistedInject constructor( } } - companion object : MvRxViewModelFactory { - - @JvmStatic - override fun create(viewModelContext: ViewModelContext, state: LoginViewState2): LoginViewModel2? { - return when (val activity: FragmentActivity = (viewModelContext as ActivityViewModelContext).activity()) { - is LoginActivity2 -> activity.loginViewModelFactory.create(state) - // TODO is SoftLogoutActivity -> activity.loginViewModelFactory.create(state) - else -> error("Invalid Activity") - } - } - } - // Store the last action, to redo it after user has trusted the untrusted certificate private var lastAction: LoginAction2? = null private var currentHomeServerConnectionConfig: HomeServerConnectionConfig? = null @@ -558,7 +547,7 @@ class LoginViewModel2 @AssistedInject constructor( safeLoginWizard.login( login = login, password = password, - deviceName = stringProvider.getString(R.string.login_default_session_public_name) + initialDeviceName = stringProvider.getString(R.string.login_default_session_public_name) ) } catch (failure: Throwable) { _viewEvents.post(LoginViewEvents2.Failure(failure)) @@ -587,16 +576,16 @@ class LoginViewModel2 @AssistedInject constructor( return@launch } when (data) { - is WellknownResult.Prompt -> + is WellknownResult.Prompt -> onWellknownSuccess(action, data, homeServerConnectionConfig) - is WellknownResult.FailPrompt -> + is WellknownResult.FailPrompt -> // Relax on IS discovery if homeserver is valid if (data.homeServerUrl != null && data.wellKnown != null) { onWellknownSuccess(action, WellknownResult.Prompt(data.homeServerUrl!!, null, data.wellKnown!!), homeServerConnectionConfig) } else { onWellKnownError() } - else -> { + else -> { onWellKnownError() } }.exhaustive @@ -630,11 +619,11 @@ class LoginViewModel2 @AssistedInject constructor( } ?: return val loginMode = when { - data.supportedLoginTypes.contains(LoginFlowTypes.SSO) - && data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.SsoAndPassword(data.ssoIdentityProviders) - data.supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso(data.ssoIdentityProviders) - data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.Password - else -> LoginMode.Unsupported + data.supportedLoginTypes.contains(LoginFlowTypes.SSO) && + data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.SsoAndPassword(data.ssoIdentityProviders) + data.supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso(data.ssoIdentityProviders) + data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.Password + else -> LoginMode.Unsupported } val viewEvent = when (loginMode) { @@ -662,8 +651,8 @@ class LoginViewModel2 @AssistedInject constructor( ) } - if ((loginMode == LoginMode.Password && !data.isLoginAndRegistrationSupported) - || data.isOutdatedHomeserver) { + if ((loginMode == LoginMode.Password && !data.isLoginAndRegistrationSupported) || + data.isOutdatedHomeserver) { // Notify the UI _viewEvents.post(LoginViewEvents2.OutdatedHomeserver) } @@ -688,8 +677,8 @@ class LoginViewModel2 @AssistedInject constructor( private fun onFlowResponse(flowResult: FlowResult) { // If dummy stage is mandatory, and password is already sent, do the dummy stage now - if (isRegistrationStarted - && flowResult.missingStages.any { it is Stage.Dummy && it.mandatory }) { + if (isRegistrationStarted && + flowResult.missingStages.any { it is Stage.Dummy && it.mandatory }) { handleRegisterDummy() } else { // Notify the user @@ -757,11 +746,11 @@ class LoginViewModel2 @AssistedInject constructor( rememberHomeServer(homeServerConnectionConfig.homeServerUri.toString()) val loginMode = when { - data.supportedLoginTypes.contains(LoginFlowTypes.SSO) - && data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.SsoAndPassword(data.ssoIdentityProviders) - data.supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso(data.ssoIdentityProviders) - data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.Password - else -> LoginMode.Unsupported + data.supportedLoginTypes.contains(LoginFlowTypes.SSO) && + data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.SsoAndPassword(data.ssoIdentityProviders) + data.supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso(data.ssoIdentityProviders) + data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.Password + else -> LoginMode.Unsupported } val viewEvent = when (loginMode) { @@ -802,8 +791,8 @@ class LoginViewModel2 @AssistedInject constructor( } viewEvent?.let { _viewEvents.post(it) } - if ((loginMode == LoginMode.Password && !data.isLoginAndRegistrationSupported) - || data.isOutdatedHomeserver) { + if ((loginMode == LoginMode.Password && !data.isLoginAndRegistrationSupported) || + data.isOutdatedHomeserver) { // Notify the UI _viewEvents.post(LoginViewEvents2.OutdatedHomeserver) } diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginViewState2.kt b/vector/src/main/java/im/vector/app/features/login2/LoginViewState2.kt index d629c6dfe7..276d1111bb 100644 --- a/vector/src/main/java/im/vector/app/features/login2/LoginViewState2.kt +++ b/vector/src/main/java/im/vector/app/features/login2/LoginViewState2.kt @@ -17,7 +17,7 @@ package im.vector.app.features.login2 import com.airbnb.mvrx.Async -import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.PersistState import com.airbnb.mvrx.Uninitialized import im.vector.app.core.extensions.toReducedUrl @@ -55,7 +55,7 @@ data class LoginViewState2( // From database val knownCustomHomeServersUrls: List = emptyList() -) : MvRxState { +) : MavericksState { // Pending user identifier fun userIdentifier(): String { diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginWebFragment2.kt b/vector/src/main/java/im/vector/app/features/login2/LoginWebFragment2.kt index b1e3eaf098..080cce4958 100644 --- a/vector/src/main/java/im/vector/app/features/login2/LoginWebFragment2.kt +++ b/vector/src/main/java/im/vector/app/features/login2/LoginWebFragment2.kt @@ -38,7 +38,6 @@ import im.vector.app.databinding.FragmentLoginWebBinding import im.vector.app.features.login.JavascriptResponse import im.vector.app.features.signout.soft.SoftLogoutAction import im.vector.app.features.signout.soft.SoftLogoutViewModel - import org.matrix.android.sdk.api.auth.data.Credentials import org.matrix.android.sdk.internal.di.MoshiProvider import timber.log.Timber @@ -57,6 +56,8 @@ class LoginWebFragment2 @Inject constructor( return FragmentLoginWebBinding.inflate(inflater, container, false) } + val softLogoutViewModel: SoftLogoutViewModel by activityViewModel() + private var isWebViewLoaded = false private var isForSessionRecovery = false @@ -234,7 +235,6 @@ class LoginWebFragment2 @Inject constructor( private fun notifyViewModel(credentials: Credentials) { if (isForSessionRecovery) { - val softLogoutViewModel: SoftLogoutViewModel by activityViewModel() softLogoutViewModel.handle(SoftLogoutAction.WebLoginSuccess(credentials)) } else { loginViewModel.handle(LoginAction2.WebLoginSuccess(credentials)) diff --git a/vector/src/main/java/im/vector/app/features/login2/created/AccountCreatedFragment.kt b/vector/src/main/java/im/vector/app/features/login2/created/AccountCreatedFragment.kt index bb190943a7..efa4bd29c6 100644 --- a/vector/src/main/java/im/vector/app/features/login2/created/AccountCreatedFragment.kt +++ b/vector/src/main/java/im/vector/app/features/login2/created/AccountCreatedFragment.kt @@ -33,6 +33,7 @@ import im.vector.app.core.intent.getFilenameFromUri import im.vector.app.core.resources.ColorProvider import im.vector.app.databinding.DialogBaseEditTextBinding import im.vector.app.databinding.FragmentLoginAccountCreatedBinding +import im.vector.app.features.displayname.getBestName import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.detail.timeline.helper.MatrixItemColorProvider import im.vector.app.features.login2.AbstractLoginFragment2 @@ -48,7 +49,6 @@ import javax.inject.Inject * - the account has been created and we propose the user to set an avatar and a display name */ class AccountCreatedFragment @Inject constructor( - val accountCreatedViewModelFactory: AccountCreatedViewModel.Factory, private val avatarRenderer: AvatarRenderer, private val dateFormatter: VectorDateFormatter, private val matrixItemColorProvider: MatrixItemColorProvider, @@ -71,7 +71,7 @@ class AccountCreatedFragment @Inject constructor( setupSubmitButton() observeViewEvents() - viewModel.subscribe { invalidateState(it) } + viewModel.onEach { invalidateState(it) } views.loginAccountCreatedTime.text = dateFormatter.format(System.currentTimeMillis(), DateFormatKind.MESSAGE_SIMPLE) } diff --git a/vector/src/main/java/im/vector/app/features/login2/created/AccountCreatedViewModel.kt b/vector/src/main/java/im/vector/app/features/login2/created/AccountCreatedViewModel.kt index 1acec968b6..568cdab119 100644 --- a/vector/src/main/java/im/vector/app/features/login2/created/AccountCreatedViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/login2/created/AccountCreatedViewModel.kt @@ -16,21 +16,21 @@ package im.vector.app.features.login2.created -import androidx.lifecycle.viewModelScope -import com.airbnb.mvrx.FragmentViewModelContext -import com.airbnb.mvrx.MvRxViewModelFactory -import com.airbnb.mvrx.ViewModelContext +import com.airbnb.mvrx.MavericksViewModelFactory import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.platform.VectorViewModel +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import org.matrix.android.sdk.api.MatrixPatterns import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.util.MatrixItem import org.matrix.android.sdk.api.util.toMatrixItem -import org.matrix.android.sdk.rx.rx -import org.matrix.android.sdk.rx.unwrap +import org.matrix.android.sdk.flow.flow +import org.matrix.android.sdk.flow.unwrap import timber.log.Timber class AccountCreatedViewModel @AssistedInject constructor( @@ -39,18 +39,11 @@ class AccountCreatedViewModel @AssistedInject constructor( ) : VectorViewModel(initialState) { @AssistedFactory - interface Factory { - fun create(initialState: AccountCreatedViewState): AccountCreatedViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: AccountCreatedViewState): AccountCreatedViewModel } - companion object : MvRxViewModelFactory { - - @JvmStatic - override fun create(viewModelContext: ViewModelContext, state: AccountCreatedViewState): AccountCreatedViewModel? { - val fragment: AccountCreatedFragment = (viewModelContext as FragmentViewModelContext).fragment() - return fragment.accountCreatedViewModelFactory.create(state) - } - } + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() init { setState { @@ -62,7 +55,7 @@ class AccountCreatedViewModel @AssistedInject constructor( } private fun observeUser() { - session.rx() + session.flow() .liveUser(session.myUserId) .unwrap() .map { diff --git a/vector/src/main/java/im/vector/app/features/login2/created/AccountCreatedViewState.kt b/vector/src/main/java/im/vector/app/features/login2/created/AccountCreatedViewState.kt index 80211b3da2..0ae60e910c 100644 --- a/vector/src/main/java/im/vector/app/features/login2/created/AccountCreatedViewState.kt +++ b/vector/src/main/java/im/vector/app/features/login2/created/AccountCreatedViewState.kt @@ -17,7 +17,7 @@ package im.vector.app.features.login2.created import com.airbnb.mvrx.Async -import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.Uninitialized import org.matrix.android.sdk.api.util.MatrixItem @@ -26,4 +26,4 @@ data class AccountCreatedViewState( val isLoading: Boolean = false, val currentUser: Async = Uninitialized, val hasBeenModified: Boolean = false -) : MvRxState +) : MavericksState diff --git a/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheet.kt b/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheet.kt index 3e75b96c32..029234a66d 100644 --- a/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheet.kt @@ -24,22 +24,22 @@ import android.view.ViewGroup import androidx.core.view.isVisible import androidx.fragment.app.Fragment import com.airbnb.mvrx.Incomplete -import com.airbnb.mvrx.MvRx +import com.airbnb.mvrx.Mavericks import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState import com.google.android.material.dialog.MaterialAlertDialogBuilder +import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R -import im.vector.app.core.di.ScreenComponent import im.vector.app.core.extensions.commitTransaction import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment import im.vector.app.databinding.BottomSheetMatrixToCardBinding import im.vector.app.features.home.AvatarRenderer import kotlinx.parcelize.Parcelize import org.matrix.android.sdk.api.session.permalinks.PermalinkData -import java.lang.ref.WeakReference import javax.inject.Inject import kotlin.reflect.KClass +@AndroidEntryPoint class MatrixToBottomSheet : VectorBaseBottomSheetDialogFragment() { @@ -50,20 +50,7 @@ class MatrixToBottomSheet : @Inject lateinit var avatarRenderer: AvatarRenderer - @Inject - lateinit var matrixToBottomSheetViewModelFactory: MatrixToBottomSheetViewModel.Factory - - override fun injectWith(injector: ScreenComponent) { - injector.inject(this) - } - - private var weakReference = WeakReference(null) - - var interactionListener: InteractionListener? - set(value) { - weakReference = WeakReference(value) - } - get() = weakReference.get() + var interactionListener: InteractionListener? = null override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): BottomSheetMatrixToCardBinding { return BottomSheetMatrixToCardBinding.inflate(inflater, container, false) @@ -72,8 +59,8 @@ class MatrixToBottomSheet : private val viewModel by fragmentViewModel(MatrixToBottomSheetViewModel::class) interface InteractionListener { - fun navigateToRoom(roomId: String) - fun switchToSpace(spaceId: String) {} + fun mxToBottomSheetNavigateToRoom(roomId: String) + fun mxToBottomSheetSwitchToSpace(spaceId: String) } override fun invalidate() = withState(viewModel) { state -> @@ -112,12 +99,12 @@ class MatrixToBottomSheet : viewModel.observeViewEvents { when (it) { is MatrixToViewEvents.NavigateToRoom -> { - interactionListener?.navigateToRoom(it.roomId) + interactionListener?.mxToBottomSheetNavigateToRoom(it.roomId) dismiss() } MatrixToViewEvents.Dismiss -> dismiss() is MatrixToViewEvents.NavigateToSpace -> { - interactionListener?.switchToSpace(it.spaceId) + interactionListener?.mxToBottomSheetSwitchToSpace(it.spaceId) dismiss() } is MatrixToViewEvents.ShowModalError -> { @@ -131,14 +118,13 @@ class MatrixToBottomSheet : } companion object { - fun withLink(matrixToLink: String, listener: InteractionListener?): MatrixToBottomSheet { + fun withLink(matrixToLink: String): MatrixToBottomSheet { return MatrixToBottomSheet().apply { arguments = Bundle().apply { - putParcelable(MvRx.KEY_ARG, MatrixToArgs( + putParcelable(Mavericks.KEY_ARG, MatrixToArgs( matrixToLink = matrixToLink )) } - interactionListener = listener } } } diff --git a/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheetState.kt b/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheetState.kt index 40213dc0ee..0fff2495f9 100644 --- a/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheetState.kt +++ b/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheetState.kt @@ -17,7 +17,7 @@ package im.vector.app.features.matrixto import com.airbnb.mvrx.Async -import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.Uninitialized import org.matrix.android.sdk.api.session.permalinks.PermalinkData import org.matrix.android.sdk.api.session.permalinks.PermalinkParser @@ -31,7 +31,7 @@ data class MatrixToBottomSheetState( val startChattingState: Async = Uninitialized, val roomPeekResult: Async = Uninitialized, val peopleYouKnow: Async> = Uninitialized -) : MvRxState { +) : MavericksState { constructor(args: MatrixToBottomSheet.MatrixToArgs) : this( deepLink = args.matrixToLink, diff --git a/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheetViewModel.kt b/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheetViewModel.kt index 566cf769f2..e741f6fb39 100644 --- a/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheetViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheetViewModel.kt @@ -16,18 +16,17 @@ package im.vector.app.features.matrixto -import androidx.lifecycle.viewModelScope import com.airbnb.mvrx.Fail -import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.Loading -import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized -import com.airbnb.mvrx.ViewModelContext import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import im.vector.app.R +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel @@ -50,14 +49,16 @@ class MatrixToBottomSheetViewModel @AssistedInject constructor( private val session: Session, private val stringProvider: StringProvider, private val directRoomHelper: DirectRoomHelper, - private val errorFormatter: ErrorFormatter) - : VectorViewModel(initialState) { + private val errorFormatter: ErrorFormatter) : + VectorViewModel(initialState) { @AssistedFactory - interface Factory { - fun create(initialState: MatrixToBottomSheetState): MatrixToBottomSheetViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: MatrixToBottomSheetState): MatrixToBottomSheetViewModel } + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() + init { when (initialState.linkType) { is PermalinkData.RoomLink -> { @@ -245,14 +246,6 @@ class MatrixToBottomSheetViewModel @AssistedInject constructor( return session.peekRoom(roomIdOrAlias) } - companion object : MvRxViewModelFactory { - override fun create(viewModelContext: ViewModelContext, state: MatrixToBottomSheetState): MatrixToBottomSheetViewModel? { - val fragment: MatrixToBottomSheet = (viewModelContext as FragmentViewModelContext).fragment() - - return fragment.matrixToBottomSheetViewModelFactory.create(state) - } - } - override fun handle(action: MatrixToAction) { when (action) { is MatrixToAction.StartChattingWithUser -> handleStartChatting(action) diff --git a/vector/src/main/java/im/vector/app/features/media/BigImageViewerActivity.kt b/vector/src/main/java/im/vector/app/features/media/BigImageViewerActivity.kt index 6b4ed34cfb..84454ee509 100644 --- a/vector/src/main/java/im/vector/app/features/media/BigImageViewerActivity.kt +++ b/vector/src/main/java/im/vector/app/features/media/BigImageViewerActivity.kt @@ -20,25 +20,21 @@ import android.content.Context import android.content.Intent import android.os.Bundle import androidx.core.net.toUri +import dagger.hilt.android.AndroidEntryPoint import im.vector.app.core.di.ActiveSessionHolder -import im.vector.app.core.di.ScreenComponent import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.databinding.ActivityBigImageViewerBinding - import javax.inject.Inject /** * Simple Activity to display an avatar in fullscreen */ +@AndroidEntryPoint class BigImageViewerActivity : VectorBaseActivity() { @Inject lateinit var sessionHolder: ActiveSessionHolder override fun getBinding() = ActivityBigImageViewerBinding.inflate(layoutInflater) - override fun injectWith(injector: ScreenComponent) { - injector.inject(this) - } - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) diff --git a/vector/src/main/java/im/vector/app/features/media/VectorAttachmentViewerActivity.kt b/vector/src/main/java/im/vector/app/features/media/VectorAttachmentViewerActivity.kt index bc3acf3eec..103511bad5 100644 --- a/vector/src/main/java/im/vector/app/features/media/VectorAttachmentViewerActivity.kt +++ b/vector/src/main/java/im/vector/app/features/media/VectorAttachmentViewerActivity.kt @@ -30,12 +30,9 @@ import androidx.core.view.isInvisible import androidx.core.view.isVisible import androidx.lifecycle.lifecycleScope import androidx.transition.Transition +import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder -import im.vector.app.core.di.DaggerScreenComponent -import im.vector.app.core.di.HasVectorInjector -import im.vector.app.core.di.ScreenComponent -import im.vector.app.core.di.VectorComponent import im.vector.app.core.intent.getMimeTypeFromUri import im.vector.app.core.utils.shareMedia import im.vector.app.features.themes.ActivityOtherThemes @@ -48,8 +45,8 @@ import kotlinx.coroutines.withContext import kotlinx.parcelize.Parcelize import timber.log.Timber import javax.inject.Inject -import kotlin.system.measureTimeMillis +@AndroidEntryPoint class VectorAttachmentViewerActivity : AttachmentViewerActivity(), BaseAttachmentProvider.InteractionListener { @Parcelize @@ -61,15 +58,11 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity(), BaseAttachmen @Inject lateinit var sessionHolder: ActiveSessionHolder - @Inject lateinit var dataSourceFactory: AttachmentProviderFactory - @Inject lateinit var imageContentRenderer: ImageContentRenderer - private lateinit var screenComponent: ScreenComponent - private var initialIndex = 0 private var isAnimatingOut = false @@ -78,12 +71,6 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity(), BaseAttachmen override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) Timber.i("onCreate Activity ${javaClass.simpleName}") - val vectorComponent = getVectorComponent() - screenComponent = DaggerScreenComponent.factory().create(vectorComponent, this) - val timeForInjection = measureTimeMillis { - screenComponent.inject(this) - } - Timber.v("Injecting dependencies into ${javaClass.simpleName} took $timeForInjection ms") ThemeUtils.setActivityTheme(this, getOtherThemes()) val args = args() ?: throw IllegalArgumentException("Missing arguments") @@ -220,10 +207,6 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity(), BaseAttachmen private fun args() = intent.getParcelableExtra(EXTRA_ARGS) - private fun getVectorComponent(): VectorComponent { - return (application as HasVectorInjector).injector() - } - private fun scheduleStartPostponedTransition(sharedElement: View) { sharedElement.viewTreeObserver.addOnPreDrawListener( object : ViewTreeObserver.OnPreDrawListener { diff --git a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt index fd163f7a34..debdf3739c 100644 --- a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt @@ -257,17 +257,11 @@ class DefaultNavigator @Inject constructor( override fun openMatrixToBottomSheet(context: Context, link: String) { if (context is AppCompatActivity) { - val listener = object : MatrixToBottomSheet.InteractionListener { - override fun navigateToRoom(roomId: String) { - openRoom(context, roomId) - } - - override fun switchToSpace(spaceId: String) { - this@DefaultNavigator.switchToSpace(context, spaceId, Navigator.PostSwitchSpaceAction.None) - } + if (context !is MatrixToBottomSheet.InteractionListener) { + fatalError("Caller context should implement MatrixToBottomSheet.InteractionListener", vectorPreferences.failFast()) } // TODO check if there is already one?? - MatrixToBottomSheet.withLink(link, listener) + MatrixToBottomSheet.withLink(link) .show(context.supportFragmentManager, "HA#MatrixToBottomSheet") } } @@ -359,6 +353,11 @@ class DefaultNavigator @Inject constructor( context.startActivity(intent) } + override fun openSettings(context: Context, payload: SettingsActivityPayload) { + val intent = VectorSettingsActivity.getIntent(context, payload) + context.startActivity(intent) + } + override fun openDebug(context: Context) { context.startActivity(Intent(context, DebugMenuActivity::class.java)) } @@ -366,8 +365,8 @@ class DefaultNavigator @Inject constructor( override fun openKeysBackupSetup(context: Context, showManualExport: Boolean) { // if cross signing is enabled and trusted or not set up at all we should propose full 4S sessionHolder.getSafeActiveSession()?.let { session -> - if (session.cryptoService().crossSigningService().getMyCrossSigningKeys() == null - || session.cryptoService().crossSigningService().canCrossSign()) { + if (session.cryptoService().crossSigningService().getMyCrossSigningKeys() == null || + session.cryptoService().crossSigningService().canCrossSign()) { (context as? AppCompatActivity)?.let { BootstrapBottomSheet.show(it.supportFragmentManager, SetupMode.NORMAL) } diff --git a/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt index ffe9562761..612643c804 100644 --- a/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt +++ b/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt @@ -23,6 +23,7 @@ import android.view.View import androidx.activity.result.ActivityResultLauncher import androidx.core.util.Pair import im.vector.app.features.crypto.recover.SetupMode +import im.vector.app.features.displayname.getBestName import im.vector.app.features.login.LoginConfig import im.vector.app.features.media.AttachmentData import im.vector.app.features.pin.PinMode @@ -82,6 +83,8 @@ interface Navigator { fun openSettings(context: Context, directAccess: Int = VectorSettingsActivity.EXTRA_DIRECT_ACCESS_ROOT) + fun openSettings(context: Context, payload: SettingsActivityPayload) + fun openDebug(context: Context) fun openKeysBackupSetup(context: Context, showManualExport: Boolean) diff --git a/vector/src/main/java/im/vector/app/features/navigation/SettingsActivityPayload.kt b/vector/src/main/java/im/vector/app/features/navigation/SettingsActivityPayload.kt new file mode 100644 index 0000000000..0b128c51b1 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/navigation/SettingsActivityPayload.kt @@ -0,0 +1,33 @@ +/* + * 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.navigation + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +sealed interface SettingsActivityPayload : Parcelable { + + @Parcelize object Root : SettingsActivityPayload + @Parcelize object AdvancedSettings : SettingsActivityPayload + @Parcelize object SecurityPrivacy : SettingsActivityPayload + @Parcelize object SecurityPrivacyManageSessions : SettingsActivityPayload + @Parcelize object General : SettingsActivityPayload + @Parcelize object Notifications : SettingsActivityPayload + + @Parcelize + data class DiscoverySettings(val expandIdentityPolicies: Boolean = false) : SettingsActivityPayload +} diff --git a/vector/src/main/java/im/vector/app/features/notifications/CircularCache.kt b/vector/src/main/java/im/vector/app/features/notifications/CircularCache.kt new file mode 100644 index 0000000000..1ac66d0ae8 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/notifications/CircularCache.kt @@ -0,0 +1,41 @@ +/* + * 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.notifications + +/** + * A FIFO circular buffer of T + * This class is not thread safe + */ +class CircularCache(cacheSize: Int, factory: (Int) -> Array) { + + companion object { + inline fun create(cacheSize: Int) = CircularCache(cacheSize) { Array(cacheSize) { null } } + } + + private val cache = factory(cacheSize) + private var writeIndex = 0 + + fun contains(key: T): Boolean = cache.contains(key) + + fun put(key: T) { + if (writeIndex == cache.size) { + writeIndex = 0 + } + cache[writeIndex] = key + writeIndex++ + } +} diff --git a/vector/src/main/java/im/vector/app/features/notifications/InviteNotifiableEvent.kt b/vector/src/main/java/im/vector/app/features/notifications/InviteNotifiableEvent.kt index 61fd5c677a..832f97bc4e 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/InviteNotifiableEvent.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/InviteNotifiableEvent.kt @@ -15,22 +15,18 @@ */ package im.vector.app.features.notifications -import androidx.core.app.NotificationCompat - data class InviteNotifiableEvent( - override var matrixID: String?, + val matrixID: String?, override val eventId: String, override val editedEventId: String?, - var roomId: String, - override var noisy: Boolean, - override val title: String, - override val description: String, - override val type: String?, - override val timestamp: Long, - override var soundName: String?, - override var isPushGatewayEvent: Boolean = false) : NotifiableEvent { - - override var hasBeenDisplayed: Boolean = false - override var isRedacted: Boolean = false - override var lockScreenVisibility = NotificationCompat.VISIBILITY_PUBLIC -} + override val canBeReplaced: Boolean, + val roomId: String, + val roomName: String?, + val noisy: Boolean, + val title: String, + val description: String, + val type: String?, + val timestamp: Long, + val soundName: String?, + override val isRedacted: Boolean = false +) : NotifiableEvent diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEvent.kt b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEvent.kt index a4f099b905..52d8119cbb 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEvent.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEvent.kt @@ -20,24 +20,11 @@ import java.io.Serializable /** * Parent interface for all events which can be displayed as a Notification */ -interface NotifiableEvent : Serializable { - var matrixID: String? +sealed interface NotifiableEvent : Serializable { val eventId: String val editedEventId: String? - var noisy: Boolean - val title: String - val description: String? - val type: String? - val timestamp: Long - - // NotificationCompat.VISIBILITY_PUBLIC , VISIBILITY_PRIVATE , VISIBILITY_SECRET - var lockScreenVisibility: Int - - // Compat: Only for android <7, for newer version the sound is defined in the channel - var soundName: String? - var hasBeenDisplayed: Boolean - var isRedacted: Boolean // Used to know if event should be replaced with the one coming from eventstream - var isPushGatewayEvent: Boolean + val canBeReplaced: Boolean + val isRedacted: Boolean } diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventProcessor.kt b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventProcessor.kt new file mode 100644 index 0000000000..3d10d74fe3 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventProcessor.kt @@ -0,0 +1,57 @@ +/* + * 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.notifications + +import im.vector.app.features.invite.AutoAcceptInvites +import im.vector.app.features.notifications.ProcessedEvent.Type.KEEP +import im.vector.app.features.notifications.ProcessedEvent.Type.REMOVE +import org.matrix.android.sdk.api.session.events.model.EventType +import javax.inject.Inject + +private typealias ProcessedEvents = List> + +class NotifiableEventProcessor @Inject constructor( + private val outdatedDetector: OutdatedEventDetector, + private val autoAcceptInvites: AutoAcceptInvites +) { + + fun process(queuedEvents: List, currentRoomId: String?, renderedEvents: ProcessedEvents): ProcessedEvents { + val processedEvents = queuedEvents.map { + val type = when (it) { + is InviteNotifiableEvent -> if (autoAcceptInvites.hideInvites) REMOVE else KEEP + is NotifiableMessageEvent -> if (shouldIgnoreMessageEventInRoom(currentRoomId, it.roomId) || outdatedDetector.isMessageOutdated(it)) { + REMOVE + } else KEEP + is SimpleNotifiableEvent -> when (it.type) { + EventType.REDACTION -> REMOVE + else -> KEEP + } + } + ProcessedEvent(type, it) + } + + val removedEventsDiff = renderedEvents.filter { renderedEvent -> + queuedEvents.none { it.eventId == renderedEvent.event.eventId } + }.map { ProcessedEvent(REMOVE, it.event) } + + return removedEventsDiff + processedEvents + } + + private fun shouldIgnoreMessageEventInRoom(currentRoomId: String?, roomId: String?): Boolean { + return currentRoomId != null && roomId == currentRoomId + } +} diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt index a19d579c05..d2db73af3d 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt @@ -15,10 +15,10 @@ */ package im.vector.app.features.notifications -import androidx.core.app.NotificationCompat import im.vector.app.BuildConfig import im.vector.app.R import im.vector.app.core.resources.StringProvider +import im.vector.app.features.displayname.getBestName import im.vector.app.features.home.room.detail.timeline.format.DisplayableEventFormatter import im.vector.app.features.home.room.detail.timeline.format.NoticeEventFormatter import org.matrix.android.sdk.api.extensions.orFalse @@ -34,6 +34,7 @@ import org.matrix.android.sdk.api.session.room.model.RoomMemberContent import org.matrix.android.sdk.api.session.room.sender.SenderInfo import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.room.timeline.getEditedEventId +import org.matrix.android.sdk.api.util.toMatrixItem import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult import timber.log.Timber import java.util.UUID @@ -52,21 +53,19 @@ class NotifiableEventResolver @Inject constructor( // private val eventDisplay = RiotEventDisplay(context) - fun resolveEvent(event: Event/*, roomState: RoomState?, bingRule: PushRule?*/, session: Session): NotifiableEvent? { + fun resolveEvent(event: Event/*, roomState: RoomState?, bingRule: PushRule?*/, session: Session, isNoisy: Boolean): NotifiableEvent? { val roomID = event.roomId ?: return null val eventId = event.eventId ?: return null if (event.getClearType() == EventType.STATE_ROOM_MEMBER) { - return resolveStateRoomEvent(event, session) + return resolveStateRoomEvent(event, session, canBeReplaced = false, isNoisy = isNoisy) } val timelineEvent = session.getRoom(roomID)?.getTimeLineEvent(eventId) ?: return null when (event.getClearType()) { EventType.MESSAGE -> { - return resolveMessageEvent(timelineEvent, session) + return resolveMessageEvent(timelineEvent, session, canBeReplaced = false, isNoisy = isNoisy) } EventType.ENCRYPTED -> { - val messageEvent = resolveMessageEvent(timelineEvent, session) - messageEvent?.lockScreenVisibility = NotificationCompat.VISIBILITY_PRIVATE - return messageEvent + return resolveMessageEvent(timelineEvent, session, canBeReplaced = false, isNoisy = isNoisy) } else -> { // If the event can be displayed, display it as is @@ -83,12 +82,14 @@ class NotifiableEventResolver @Inject constructor( description = bodyPreview, title = stringProvider.getString(R.string.notification_unknown_new_event), soundName = null, - type = event.type) + type = event.type, + canBeReplaced = false + ) } } } - fun resolveInMemoryEvent(session: Session, event: Event): NotifiableEvent? { + fun resolveInMemoryEvent(session: Session, event: Event, canBeReplaced: Boolean): NotifiableEvent? { if (event.getClearType() != EventType.MESSAGE) return null // Ignore message edition @@ -107,29 +108,19 @@ class NotifiableEventResolver @Inject constructor( displayIndex = 0, senderInfo = SenderInfo( userId = user.userId, - displayName = user.getBestName(), + displayName = user.toMatrixItem().getBestName(), isUniqueDisplayName = true, avatarUrl = user.avatarUrl ) ) - - val notifiableEvent = resolveMessageEvent(timelineEvent, session) - - if (notifiableEvent == null) { - Timber.d("## Failed to resolve event") - // TODO - null - } else { - notifiableEvent.noisy = !notificationAction.soundName.isNullOrBlank() - notifiableEvent - } + resolveMessageEvent(timelineEvent, session, canBeReplaced = canBeReplaced, isNoisy = !notificationAction.soundName.isNullOrBlank()) } else { Timber.d("Matched push rule is set to not notify") null } } - private fun resolveMessageEvent(event: TimelineEvent, session: Session): NotifiableEvent? { + private fun resolveMessageEvent(event: TimelineEvent, session: Session, canBeReplaced: Boolean, isNoisy: Boolean): NotifiableEvent { // The event only contains an eventId, and roomId (type is m.room.*) , we need to get the displayable content (names, avatar, text, etc...) val room = session.getRoom(event.root.roomId!! /*roomID cannot be null*/) @@ -140,19 +131,19 @@ class NotifiableEventResolver @Inject constructor( val roomName = stringProvider.getString(R.string.notification_unknown_room_name) val senderDisplayName = event.senderInfo.disambiguatedDisplayName - val notifiableEvent = NotifiableMessageEvent( + return NotifiableMessageEvent( eventId = event.root.eventId!!, editedEventId = event.getEditedEventId(), + canBeReplaced = canBeReplaced, timestamp = event.root.originServerTs ?: 0, - noisy = false, // will be updated + noisy = isNoisy, senderName = senderDisplayName, senderId = event.root.senderId, body = body.toString(), roomId = event.root.roomId!!, - roomName = roomName) - - notifiableEvent.matrixID = session.myUserId - return notifiableEvent + roomName = roomName, + matrixID = session.myUserId + ) } else { if (event.root.isEncrypted() && event.root.mxDecryptionResult == null) { // TODO use a global event decryptor? attache to session and that listen to new sessionId? @@ -173,57 +164,56 @@ class NotifiableEventResolver @Inject constructor( val roomName = room.roomSummary()?.displayName ?: "" val senderDisplayName = event.senderInfo.disambiguatedDisplayName - val notifiableEvent = NotifiableMessageEvent( + return NotifiableMessageEvent( eventId = event.root.eventId!!, editedEventId = event.getEditedEventId(), + canBeReplaced = canBeReplaced, timestamp = event.root.originServerTs ?: 0, - noisy = false, // will be updated + noisy = isNoisy, senderName = senderDisplayName, senderId = event.root.senderId, body = body, roomId = event.root.roomId!!, roomName = roomName, - roomIsDirect = room.roomSummary()?.isDirect ?: false) - - notifiableEvent.matrixID = session.myUserId - notifiableEvent.soundName = null - - // Get the avatars URL - notifiableEvent.roomAvatarPath = session.contentUrlResolver() - .resolveThumbnail(room.roomSummary()?.avatarUrl, - 250, - 250, - ContentUrlResolver.ThumbnailMethod.SCALE) - - notifiableEvent.senderAvatarPath = session.contentUrlResolver() - .resolveThumbnail(event.senderInfo.avatarUrl, - 250, - 250, - ContentUrlResolver.ThumbnailMethod.SCALE) - - return notifiableEvent + roomIsDirect = room.roomSummary()?.isDirect ?: false, + roomAvatarPath = session.contentUrlResolver() + .resolveThumbnail(room.roomSummary()?.avatarUrl, + 250, + 250, + ContentUrlResolver.ThumbnailMethod.SCALE), + senderAvatarPath = session.contentUrlResolver() + .resolveThumbnail(event.senderInfo.avatarUrl, + 250, + 250, + ContentUrlResolver.ThumbnailMethod.SCALE), + matrixID = session.myUserId, + soundName = null + ) } } - private fun resolveStateRoomEvent(event: Event, session: Session): NotifiableEvent? { + private fun resolveStateRoomEvent(event: Event, session: Session, canBeReplaced: Boolean, isNoisy: Boolean): NotifiableEvent? { val content = event.content?.toModel() ?: return null val roomId = event.roomId ?: return null val dName = event.senderId?.let { session.getRoomMember(it, roomId)?.displayName } if (Membership.INVITE == content.membership) { - val body = noticeEventFormatter.format(event, dName, isDm = session.getRoomSummary(roomId)?.isDirect.orFalse()) + val roomSummary = session.getRoomSummary(roomId) + val body = noticeEventFormatter.format(event, dName, isDm = roomSummary?.isDirect.orFalse()) ?: stringProvider.getString(R.string.notification_new_invitation) return InviteNotifiableEvent( session.myUserId, eventId = event.eventId!!, editedEventId = null, + canBeReplaced = canBeReplaced, roomId = roomId, + roomName = roomSummary?.displayName, timestamp = event.originServerTs ?: 0, - noisy = false, // will be set later + noisy = isNoisy, title = stringProvider.getString(R.string.notification_new_invitation), description = body.toString(), soundName = null, // will be set later - type = event.getClearType(), - isPushGatewayEvent = false) + type = event.getClearType() + ) } else { Timber.e("## unsupported notifiable event for eventId [${event.eventId}]") if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) { diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotifiableMessageEvent.kt b/vector/src/main/java/im/vector/app/features/notifications/NotifiableMessageEvent.kt index fb9ca8d23c..161c9f74a6 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotifiableMessageEvent.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotifiableMessageEvent.kt @@ -15,43 +15,31 @@ */ package im.vector.app.features.notifications -import androidx.core.app.NotificationCompat import org.matrix.android.sdk.api.session.events.model.EventType data class NotifiableMessageEvent( override val eventId: String, override val editedEventId: String?, - override var noisy: Boolean, - override val timestamp: Long, - var senderName: String?, - var senderId: String?, - var body: String?, - var roomId: String, - var roomName: String?, - var roomIsDirect: Boolean = false + override val canBeReplaced: Boolean, + val noisy: Boolean, + val timestamp: Long, + val senderName: String?, + val senderId: String?, + val body: String?, + val roomId: String, + val roomName: String?, + val roomIsDirect: Boolean = false, + val roomAvatarPath: String? = null, + val senderAvatarPath: String? = null, + val matrixID: String? = null, + val soundName: String? = null, + // This is used for >N notification, as the result of a smart reply + val outGoingMessage: Boolean = false, + val outGoingMessageFailed: Boolean = false, + override val isRedacted: Boolean = false ) : NotifiableEvent { - override var matrixID: String? = null - override var soundName: String? = null - override var lockScreenVisibility = NotificationCompat.VISIBILITY_PUBLIC - override var hasBeenDisplayed: Boolean = false - override var isRedacted: Boolean = false - - var roomAvatarPath: String? = null - var senderAvatarPath: String? = null - - override var isPushGatewayEvent: Boolean = false - - override val type: String - get() = EventType.MESSAGE - - override val description: String? - get() = body ?: "" - - override val title: String - get() = senderName ?: "" - - // This is used for >N notification, as the result of a smart reply - var outGoingMessage = false - var outGoingMessageFailed = false + val type: String = EventType.MESSAGE + val description: String = body ?: "" + val title: String = senderName ?: "" } diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationBroadcastReceiver.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationBroadcastReceiver.kt index 2c4cdab25e..33e43cd7e4 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationBroadcastReceiver.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationBroadcastReceiver.kt @@ -20,9 +20,9 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import androidx.core.app.RemoteInput +import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder -import im.vector.app.core.extensions.vectorComponent import im.vector.app.features.session.coroutineScope import kotlinx.coroutines.launch import org.matrix.android.sdk.api.extensions.tryOrNull @@ -36,6 +36,7 @@ import javax.inject.Inject /** * Receives actions broadcast by notification (on click, on dismiss, inline replies, etc.) */ +@AndroidEntryPoint class NotificationBroadcastReceiver : BroadcastReceiver() { @Inject lateinit var notificationDrawerManager: NotificationDrawerManager @@ -44,7 +45,6 @@ class NotificationBroadcastReceiver : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { if (intent == null || context == null) return Timber.v("NotificationBroadcastReceiver received : $intent") - context.vectorComponent().inject(this) when (intent.action) { NotificationUtils.SMART_REPLY_ACTION -> handleSmartReply(intent, context) @@ -130,19 +130,20 @@ class NotificationBroadcastReceiver : BroadcastReceiver() { val notifiableMessageEvent = NotifiableMessageEvent( // Generate a Fake event id - UUID.randomUUID().toString(), - null, - false, - System.currentTimeMillis(), - session.getRoomMember(session.myUserId, room.roomId)?.displayName + eventId = UUID.randomUUID().toString(), + editedEventId = null, + noisy = false, + timestamp = System.currentTimeMillis(), + senderName = session.getRoomMember(session.myUserId, room.roomId)?.displayName ?: context?.getString(R.string.notification_sender_me), - session.myUserId, - message, - room.roomId, - room.roomSummary()?.displayName ?: room.roomId, - room.roomSummary()?.isDirect == true + senderId = session.myUserId, + body = message, + roomId = room.roomId, + roomName = room.roomSummary()?.displayName ?: room.roomId, + roomIsDirect = room.roomSummary()?.isDirect == true, + outGoingMessage = true, + canBeReplaced = false ) - notifiableMessageEvent.outGoingMessage = true notificationDrawerManager.onNotifiableEventReceived(notifiableMessageEvent) notificationDrawerManager.refreshNotificationDrawer() diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationDisplayer.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationDisplayer.kt new file mode 100644 index 0000000000..680ff32a52 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationDisplayer.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.notifications + +import android.app.Notification +import android.content.Context +import androidx.core.app.NotificationManagerCompat +import timber.log.Timber +import javax.inject.Inject + +class NotificationDisplayer @Inject constructor(context: Context) { + + private val notificationManager = NotificationManagerCompat.from(context) + + fun showNotificationMessage(tag: String?, id: Int, notification: Notification) { + notificationManager.notify(tag, id, notification) + } + + fun cancelNotificationMessage(tag: String?, id: Int) { + notificationManager.cancel(tag, id) + } + + fun cancelAllNotifications() { + // Keep this try catch (reported by GA) + try { + notificationManager.cancelAll() + } catch (e: Exception) { + Timber.e(e, "## cancelAllNotifications() failed") + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt index fea4716b39..c052de650e 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt @@ -16,22 +16,18 @@ package im.vector.app.features.notifications import android.content.Context -import android.graphics.Bitmap import android.os.Handler import android.os.HandlerThread import androidx.annotation.WorkerThread -import androidx.core.app.NotificationCompat -import androidx.core.app.Person import im.vector.app.ActiveSessionDataSource import im.vector.app.BuildConfig import im.vector.app.R -import im.vector.app.core.resources.StringProvider import im.vector.app.core.utils.FirstThrottler -import im.vector.app.features.invite.AutoAcceptInvites +import im.vector.app.features.displayname.getBestName import im.vector.app.features.settings.VectorPreferences -import me.gujun.android.span.span import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.content.ContentUrlResolver +import org.matrix.android.sdk.api.util.toMatrixItem import timber.log.Timber import java.io.File import java.io.FileOutputStream @@ -45,14 +41,11 @@ import javax.inject.Singleton */ @Singleton class NotificationDrawerManager @Inject constructor(private val context: Context, - private val notificationUtils: NotificationUtils, + private val notificationDisplayer: NotificationDisplayer, private val vectorPreferences: VectorPreferences, - private val stringProvider: StringProvider, private val activeSessionDataSource: ActiveSessionDataSource, - private val iconLoader: IconLoader, - private val bitmapLoader: BitmapLoader, - private val outdatedDetector: OutdatedEventDetector?, - private val autoAcceptInvites: AutoAcceptInvites) { + private val notifiableEventProcessor: NotifiableEventProcessor, + private val notificationRenderer: NotificationRenderer) { private val handlerThread: HandlerThread = HandlerThread("NotificationDrawerManager", Thread.MIN_PRIORITY) private var backgroundHandler: Handler @@ -62,13 +55,23 @@ class NotificationDrawerManager @Inject constructor(private val context: Context backgroundHandler = Handler(handlerThread.looper) } - // The first time the notification drawer is refreshed, we force re-render of all notifications - private var firstTime = true - - private val eventList = loadEventInfo() + /** + * The notifiable events to render + * this is our source of truth for notifications, any changes to this list will be rendered as notifications + * when events are removed the previously rendered notifications will be cancelled + * when adding or updating, the notifications will be notified + * + * Events are unique by their properties, we should be careful not to insert multiple events with the same event-id + */ + private val queuedEvents = loadEventInfo() + /** + * The last known rendered notifiable events + * we keep track of them in order to know which events have been removed from the eventList + * allowing us to cancel any notifications previous displayed by now removed events + */ + private var renderedEvents = emptyList>() private val avatarSize = context.resources.getDimensionPixelSize(R.dimen.profile_avatar_size) - private var currentRoomId: String? = null // TODO Multi-session: this will have to be improved @@ -77,6 +80,13 @@ class NotificationDrawerManager @Inject constructor(private val context: Context private var useCompleteNotificationFormat = vectorPreferences.useCompleteNotificationFormat() + /** + * An in memory FIFO cache of the seen events. + * Acts as a notification debouncer to stop already dismissed push notifications from + * displaying again when the /sync response is delayed. + */ + private val seenEventIds = CircularCache.create(cacheSize = 25) + /** Should be called as soon as a new event is ready to be displayed. The notification corresponding to this event will not be displayed until @@ -93,12 +103,12 @@ class NotificationDrawerManager @Inject constructor(private val context: Context if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) { Timber.d("onNotifiableEventReceived(): $notifiableEvent") } else { - Timber.d("onNotifiableEventReceived(): is push: ${notifiableEvent.isPushGatewayEvent}") + Timber.d("onNotifiableEventReceived(): is push: ${notifiableEvent.canBeReplaced}") } - synchronized(eventList) { - val existing = eventList.firstOrNull { it.eventId == notifiableEvent.eventId } + synchronized(queuedEvents) { + val existing = queuedEvents.firstOrNull { it.eventId == notifiableEvent.eventId } if (existing != null) { - if (existing.isPushGatewayEvent) { + if (existing.canBeReplaced) { // Use the event coming from the event stream as it may contains more info than // the fcm one (like type/content/clear text) (e.g when an encrypted message from // FCM should be update with clear text after a sync) @@ -107,9 +117,8 @@ class NotificationDrawerManager @Inject constructor(private val context: Context // Use setOnlyAlertOnce to ensure update notification does not interfere with sound // from first notify invocation as outlined in: // https://developer.android.com/training/notify-user/build-notification#Updating - notifiableEvent.hasBeenDisplayed = false - eventList.remove(existing) - eventList.add(notifiableEvent) + queuedEvents.remove(existing) + queuedEvents.add(notifiableEvent) } else { // keep the existing one, do not replace } @@ -117,34 +126,43 @@ class NotificationDrawerManager @Inject constructor(private val context: Context // Check if this is an edit if (notifiableEvent.editedEventId != null) { // This is an edition - val eventBeforeEdition = eventList.firstOrNull { + val eventBeforeEdition = queuedEvents.firstOrNull { // Edition of an event - it.eventId == notifiableEvent.editedEventId + it.eventId == notifiableEvent.editedEventId || // or edition of an edition - || it.editedEventId == notifiableEvent.editedEventId + it.editedEventId == notifiableEvent.editedEventId } if (eventBeforeEdition != null) { // Replace the existing notification with the new content - eventList.remove(eventBeforeEdition) + queuedEvents.remove(eventBeforeEdition) - eventList.add(notifiableEvent) + queuedEvents.add(notifiableEvent) } else { // Ignore an edit of a not displayed event in the notification drawer } } else { // Not an edit - eventList.add(notifiableEvent) + if (seenEventIds.contains(notifiableEvent.eventId)) { + // we've already seen the event, lets skip + Timber.d("onNotifiableEventReceived(): skipping event, already seen") + } else { + seenEventIds.put(notifiableEvent.eventId) + queuedEvents.add(notifiableEvent) + } } } } } fun onEventRedacted(eventId: String) { - synchronized(eventList) { - eventList.find { it.eventId == eventId }?.apply { - isRedacted = true - hasBeenDisplayed = false + synchronized(queuedEvents) { + queuedEvents.replace(eventId) { + when (it) { + is InviteNotifiableEvent -> it.copy(isRedacted = true) + is NotifiableMessageEvent -> it.copy(isRedacted = true) + is SimpleNotifiableEvent -> it.copy(isRedacted = true) + } } } } @@ -153,8 +171,8 @@ class NotificationDrawerManager @Inject constructor(private val context: Context * Clear all known events and refresh the notification drawer */ fun clearAllEvents() { - synchronized(eventList) { - eventList.clear() + synchronized(queuedEvents) { + queuedEvents.clear() } refreshNotificationDrawer() } @@ -163,14 +181,8 @@ class NotificationDrawerManager @Inject constructor(private val context: Context fun clearMessageEventOfRoom(roomId: String?) { Timber.v("clearMessageEventOfRoom $roomId") if (roomId != null) { - var shouldUpdate = false - synchronized(eventList) { - shouldUpdate = eventList.removeAll { e -> - e is NotifiableMessageEvent && e.roomId == roomId - } - } + val shouldUpdate = removeAll { it is NotifiableMessageEvent && it.roomId == roomId } if (shouldUpdate) { - notificationUtils.cancelNotificationMessage(roomId, ROOM_MESSAGES_NOTIFICATION_ID) refreshNotificationDrawer() } } @@ -182,7 +194,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context */ fun setCurrentRoom(roomId: String?) { var hasChanged: Boolean - synchronized(eventList) { + synchronized(queuedEvents) { hasChanged = roomId != currentRoomId currentRoomId = roomId } @@ -192,12 +204,16 @@ class NotificationDrawerManager @Inject constructor(private val context: Context } fun clearMemberShipNotificationForRoom(roomId: String) { - synchronized(eventList) { - eventList.removeAll { e -> - e is InviteNotifiableEvent && e.roomId == roomId - } + val shouldUpdate = removeAll { it is InviteNotifiableEvent && it.roomId == roomId } + if (shouldUpdate) { + refreshNotificationDrawerBg() + } + } + + private fun removeAll(predicate: (NotifiableEvent) -> Boolean): Boolean { + return synchronized(queuedEvents) { + queuedEvents.removeAll(predicate) } - notificationUtils.cancelNotificationMessage(roomId, ROOM_INVITATION_NOTIFICATION_ID) } private var firstThrottler = FirstThrottler(200) @@ -224,342 +240,36 @@ class NotificationDrawerManager @Inject constructor(private val context: Context private fun refreshNotificationDrawerBg() { Timber.v("refreshNotificationDrawerBg()") - val session = currentSession ?: return - - val user = session.getUser(session.myUserId) - // myUserDisplayName cannot be empty else NotificationCompat.MessagingStyle() will crash - val myUserDisplayName = user?.getBestName() ?: session.myUserId - val myUserAvatarUrl = session.contentUrlResolver().resolveThumbnail(user?.avatarUrl, avatarSize, avatarSize, ContentUrlResolver.ThumbnailMethod.SCALE) - synchronized(eventList) { - Timber.v("%%%%%%%% REFRESH NOTIFICATION DRAWER ") - // TMP code - var hasNewEvent = false - var summaryIsNoisy = false - val summaryInboxStyle = NotificationCompat.InboxStyle() - - // group events by room to create a single MessagingStyle notif - val roomIdToEventMap: MutableMap> = LinkedHashMap() - val simpleEvents: MutableList = ArrayList() - val invitationEvents: MutableList = ArrayList() - - val eventIterator = eventList.listIterator() - while (eventIterator.hasNext()) { - when (val event = eventIterator.next()) { - is NotifiableMessageEvent -> { - val roomId = event.roomId - val roomEvents = roomIdToEventMap.getOrPut(roomId) { ArrayList() } - - if (shouldIgnoreMessageEventInRoom(roomId) || outdatedDetector?.isMessageOutdated(event) == true) { - // forget this event - eventIterator.remove() - } else { - roomEvents.add(event) - } - } - is InviteNotifiableEvent -> { - if (autoAcceptInvites.hideInvites) { - // Forget this event - eventIterator.remove() - } else { - invitationEvents.add(event) - } - } - is SimpleNotifiableEvent -> simpleEvents.add(event) - else -> Timber.w("Type not handled") - } - } - - Timber.v("%%%%%%%% REFRESH NOTIFICATION DRAWER ${roomIdToEventMap.size} room groups") - - var globalLastMessageTimestamp = 0L - - val newSettings = vectorPreferences.useCompleteNotificationFormat() - if (newSettings != useCompleteNotificationFormat) { - // Settings has changed, remove all current notifications - notificationUtils.cancelAllNotifications() - useCompleteNotificationFormat = newSettings - } - - var simpleNotificationRoomCounter = 0 - var simpleNotificationMessageCounter = 0 - - // events have been grouped by roomId - for ((roomId, events) in roomIdToEventMap) { - // Build the notification for the room - if (events.isEmpty() || events.all { it.isRedacted }) { - // Just clear this notification - Timber.v("%%%%%%%% REFRESH NOTIFICATION DRAWER $roomId has no more events") - notificationUtils.cancelNotificationMessage(roomId, ROOM_MESSAGES_NOTIFICATION_ID) - continue - } - - simpleNotificationRoomCounter++ - val roomName = events[0].roomName ?: events[0].senderName ?: "" - - val roomEventGroupInfo = RoomEventGroupInfo( - roomId = roomId, - isDirect = events[0].roomIsDirect, - roomDisplayName = roomName) - - val style = NotificationCompat.MessagingStyle(Person.Builder() - .setName(myUserDisplayName) - .setIcon(iconLoader.getUserIcon(myUserAvatarUrl)) - .setKey(events[0].matrixID) - .build()) - - style.isGroupConversation = !roomEventGroupInfo.isDirect - - if (!roomEventGroupInfo.isDirect) { - style.conversationTitle = roomEventGroupInfo.roomDisplayName - } - - val largeBitmap = getRoomBitmap(events) - - for (event in events) { - // if all events in this room have already been displayed there is no need to update it - if (!event.hasBeenDisplayed && !event.isRedacted) { - roomEventGroupInfo.shouldBing = roomEventGroupInfo.shouldBing || event.noisy - roomEventGroupInfo.customSound = event.soundName - } - roomEventGroupInfo.hasNewEvent = roomEventGroupInfo.hasNewEvent || !event.hasBeenDisplayed - - val senderPerson = Person.Builder() - .setName(event.senderName) - .setIcon(iconLoader.getUserIcon(event.senderAvatarPath)) - .setKey(event.senderId) - .build() - - if (event.outGoingMessage && event.outGoingMessageFailed) { - style.addMessage(stringProvider.getString(R.string.notification_inline_reply_failed), event.timestamp, senderPerson) - roomEventGroupInfo.hasSmartReplyError = true - } else { - if (!event.isRedacted) { - simpleNotificationMessageCounter++ - style.addMessage(event.body, event.timestamp, senderPerson) - } - } - event.hasBeenDisplayed = true // we can consider it as displayed - - // It is possible that this event was previously shown as an 'anonymous' simple notif. - // And now it will be merged in a single MessageStyle notif, so we can clean to be sure - notificationUtils.cancelNotificationMessage(event.eventId, ROOM_EVENT_NOTIFICATION_ID) - } - - try { - if (events.size == 1) { - val event = events[0] - if (roomEventGroupInfo.isDirect) { - val line = span { - span { - textStyle = "bold" - +String.format("%s: ", event.senderName) - } - +(event.description ?: "") - } - summaryInboxStyle.addLine(line) - } else { - val line = span { - span { - textStyle = "bold" - +String.format("%s: %s ", roomName, event.senderName) - } - +(event.description ?: "") - } - summaryInboxStyle.addLine(line) - } - } else { - val summaryLine = stringProvider.getQuantityString( - R.plurals.notification_compat_summary_line_for_room, events.size, roomName, events.size) - summaryInboxStyle.addLine(summaryLine) - } - } catch (e: Throwable) { - // String not found or bad format - Timber.v("%%%%%%%% REFRESH NOTIFICATION DRAWER failed to resolve string") - summaryInboxStyle.addLine(roomName) - } - - if (firstTime || roomEventGroupInfo.hasNewEvent) { - // Should update displayed notification - Timber.v("%%%%%%%% REFRESH NOTIFICATION DRAWER $roomId need refresh") - val lastMessageTimestamp = events.last().timestamp - - if (globalLastMessageTimestamp < lastMessageTimestamp) { - globalLastMessageTimestamp = lastMessageTimestamp - } - - val tickerText = if (roomEventGroupInfo.isDirect) { - stringProvider.getString(R.string.notification_ticker_text_dm, events.last().senderName, events.last().description) - } else { - stringProvider.getString(R.string.notification_ticker_text_group, roomName, events.last().senderName, events.last().description) - } - - if (useCompleteNotificationFormat) { - val notification = notificationUtils.buildMessagesListNotification( - style, - roomEventGroupInfo, - largeBitmap, - lastMessageTimestamp, - myUserDisplayName, - tickerText) - - // is there an id for this room? - notificationUtils.showNotificationMessage(roomId, ROOM_MESSAGES_NOTIFICATION_ID, notification) - } - - hasNewEvent = true - summaryIsNoisy = summaryIsNoisy || roomEventGroupInfo.shouldBing - } else { - Timber.v("%%%%%%%% REFRESH NOTIFICATION DRAWER $roomId is up to date") - } - } - - // Handle invitation events - for (event in invitationEvents) { - // We build a invitation notification - if (firstTime || !event.hasBeenDisplayed) { - if (useCompleteNotificationFormat) { - val notification = notificationUtils.buildRoomInvitationNotification(event, session.myUserId) - notificationUtils.showNotificationMessage(event.roomId, ROOM_INVITATION_NOTIFICATION_ID, notification) - } - event.hasBeenDisplayed = true // we can consider it as displayed - hasNewEvent = true - summaryIsNoisy = summaryIsNoisy || event.noisy - summaryInboxStyle.addLine(event.description) - } - } - - // Handle simple events - for (event in simpleEvents) { - // We build a simple notification - if (firstTime || !event.hasBeenDisplayed) { - if (useCompleteNotificationFormat) { - val notification = notificationUtils.buildSimpleEventNotification(event, session.myUserId) - notificationUtils.showNotificationMessage(event.eventId, ROOM_EVENT_NOTIFICATION_ID, notification) - } - event.hasBeenDisplayed = true // we can consider it as displayed - hasNewEvent = true - summaryIsNoisy = summaryIsNoisy || event.noisy - summaryInboxStyle.addLine(event.description) - } - } - - // ======== Build summary notification ========= - // On Android 7.0 (API level 24) and higher, the system automatically builds a summary for - // your group using snippets of text from each notification. The user can expand this - // notification to see each separate notification. - // To support older versions, which cannot show a nested group of notifications, - // you must create an extra notification that acts as the summary. - // This appears as the only notification and the system hides all the others. - // So this summary should include a snippet from all the other notifications, - // which the user can tap to open your app. - // The behavior of the group summary may vary on some device types such as wearables. - // To ensure the best experience on all devices and versions, always include a group summary when you create a group - // https://developer.android.com/training/notify-user/group - - if (eventList.isEmpty() || eventList.all { it.isRedacted }) { - notificationUtils.cancelNotificationMessage(null, SUMMARY_NOTIFICATION_ID) - } else { - // FIXME roomIdToEventMap.size is not correct, this is the number of rooms - val nbEvents = roomIdToEventMap.size + simpleEvents.size - val sumTitle = stringProvider.getQuantityString(R.plurals.notification_compat_summary_title, nbEvents, nbEvents) - summaryInboxStyle.setBigContentTitle(sumTitle) - // TODO get latest event? - .setSummaryText(stringProvider.getQuantityString(R.plurals.notification_unread_notified_messages, nbEvents, nbEvents)) - - if (useCompleteNotificationFormat) { - val notification = notificationUtils.buildSummaryListNotification( - summaryInboxStyle, - sumTitle, - noisy = hasNewEvent && summaryIsNoisy, - lastMessageTimestamp = globalLastMessageTimestamp) - - notificationUtils.showNotificationMessage(null, SUMMARY_NOTIFICATION_ID, notification) - } else { - // Add the simple events as message (?) - simpleNotificationMessageCounter += simpleEvents.size - val numberOfInvitations = invitationEvents.size - - val privacyTitle = if (numberOfInvitations > 0) { - val invitationsStr = stringProvider.getQuantityString(R.plurals.notification_invitations, numberOfInvitations, numberOfInvitations) - if (simpleNotificationMessageCounter > 0) { - // Invitation and message - val messageStr = stringProvider.getQuantityString(R.plurals.room_new_messages_notification, - simpleNotificationMessageCounter, simpleNotificationMessageCounter) - if (simpleNotificationRoomCounter > 1) { - // In several rooms - val roomStr = stringProvider.getQuantityString(R.plurals.notification_unread_notified_messages_in_room_rooms, - simpleNotificationRoomCounter, simpleNotificationRoomCounter) - stringProvider.getString( - R.string.notification_unread_notified_messages_in_room_and_invitation, - messageStr, - roomStr, - invitationsStr - ) - } else { - // In one room - stringProvider.getString( - R.string.notification_unread_notified_messages_and_invitation, - messageStr, - invitationsStr - ) - } - } else { - // Only invitation - invitationsStr - } - } else { - // No invitation, only messages - val messageStr = stringProvider.getQuantityString(R.plurals.room_new_messages_notification, - simpleNotificationMessageCounter, simpleNotificationMessageCounter) - if (simpleNotificationRoomCounter > 1) { - // In several rooms - val roomStr = stringProvider.getQuantityString(R.plurals.notification_unread_notified_messages_in_room_rooms, - simpleNotificationRoomCounter, simpleNotificationRoomCounter) - stringProvider.getString(R.string.notification_unread_notified_messages_in_room, messageStr, roomStr) - } else { - // In one room - messageStr - } - } - val notification = notificationUtils.buildSummaryListNotification( - style = null, - compatSummary = privacyTitle, - noisy = hasNewEvent && summaryIsNoisy, - lastMessageTimestamp = globalLastMessageTimestamp) - - notificationUtils.showNotificationMessage(null, SUMMARY_NOTIFICATION_ID, notification) - } - - if (hasNewEvent && summaryIsNoisy) { - try { - // turn the screen on for 3 seconds - /* - TODO - if (Matrix.getInstance(VectorApp.getInstance())!!.pushManager.isScreenTurnedOn) { - val pm = VectorApp.getInstance().getSystemService()!! - val wl = pm.newWakeLock(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON or PowerManager.ACQUIRE_CAUSES_WAKEUP, - NotificationDrawerManager::class.java.name) - wl.acquire(3000) - wl.release() - } - */ - } catch (e: Throwable) { - Timber.e(e, "## Failed to turn screen on") - } - } - } - // notice that we can get bit out of sync with actual display but not a big issue - firstTime = false + val newSettings = vectorPreferences.useCompleteNotificationFormat() + if (newSettings != useCompleteNotificationFormat) { + // Settings has changed, remove all current notifications + notificationDisplayer.cancelAllNotifications() + useCompleteNotificationFormat = newSettings } - } - private fun getRoomBitmap(events: List): Bitmap? { - if (events.isEmpty()) return null + val eventsToRender = synchronized(queuedEvents) { + notifiableEventProcessor.process(queuedEvents, currentRoomId, renderedEvents).also { + queuedEvents.clear() + queuedEvents.addAll(it.onlyKeptEvents()) + } + } - // Use the last event (most recent?) - val roomAvatarPath = events.last().roomAvatarPath ?: events.last().senderAvatarPath - - return bitmapLoader.getRoomBitmap(roomAvatarPath) + if (renderedEvents == eventsToRender) { + Timber.d("Skipping notification update due to event list not changing") + } else { + renderedEvents = eventsToRender + val session = currentSession ?: return + val user = session.getUser(session.myUserId) + // myUserDisplayName cannot be empty else NotificationCompat.MessagingStyle() will crash + val myUserDisplayName = user?.toMatrixItem()?.getBestName() ?: session.myUserId + val myUserAvatarUrl = session.contentUrlResolver().resolveThumbnail( + contentUrl = user?.avatarUrl, + width = avatarSize, + height = avatarSize, + method = ContentUrlResolver.ThumbnailMethod.SCALE + ) + notificationRenderer.render(session.myUserId, myUserDisplayName, myUserAvatarUrl, useCompleteNotificationFormat, eventsToRender) + } } fun shouldIgnoreMessageEventInRoom(roomId: String?): Boolean { @@ -567,8 +277,8 @@ class NotificationDrawerManager @Inject constructor(private val context: Context } fun persistInfo() { - synchronized(eventList) { - if (eventList.isEmpty()) { + synchronized(queuedEvents) { + if (queuedEvents.isEmpty()) { deleteCachedRoomNotifications() return } @@ -576,7 +286,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context val file = File(context.applicationContext.cacheDir, ROOMS_NOTIFICATIONS_FILE_NAME) if (!file.exists()) file.createNewFile() FileOutputStream(file).use { - currentSession?.securelyStoreObject(eventList, KEY_ALIAS_SECRET_STORAGE, it) + currentSession?.securelyStoreObject(queuedEvents, KEY_ALIAS_SECRET_STORAGE, it) } } catch (e: Throwable) { Timber.e(e, "## Failed to save cached notification info") @@ -608,15 +318,11 @@ class NotificationDrawerManager @Inject constructor(private val context: Context } } - fun displayDiagnosticNotification() { - notificationUtils.displayDiagnosticNotification() - } - companion object { - private const val SUMMARY_NOTIFICATION_ID = 0 - private const val ROOM_MESSAGES_NOTIFICATION_ID = 1 - private const val ROOM_EVENT_NOTIFICATION_ID = 2 - private const val ROOM_INVITATION_NOTIFICATION_ID = 3 + const val SUMMARY_NOTIFICATION_ID = 0 + const val ROOM_MESSAGES_NOTIFICATION_ID = 1 + const val ROOM_EVENT_NOTIFICATION_ID = 2 + const val ROOM_INVITATION_NOTIFICATION_ID = 3 // TODO Mutliaccount private const val ROOMS_NOTIFICATIONS_FILE_NAME = "im.vector.notifications.cache" @@ -624,3 +330,11 @@ class NotificationDrawerManager @Inject constructor(private val context: Context private const val KEY_ALIAS_SECRET_STORAGE = "notificationMgr" } } + +private fun MutableList.replace(eventId: String, block: (NotifiableEvent) -> NotifiableEvent) { + val indexToReplace = indexOfFirst { it.eventId == eventId } + if (indexToReplace == -1) { + return + } + set(indexToReplace, block(get(indexToReplace))) +} diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationFactory.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationFactory.kt new file mode 100644 index 0000000000..adc4e44bcc --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationFactory.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.notifications + +import android.app.Notification +import androidx.core.content.pm.ShortcutInfoCompat +import javax.inject.Inject + +private typealias ProcessedMessageEvents = List> + +class NotificationFactory @Inject constructor( + private val notificationUtils: NotificationUtils, + private val roomGroupMessageCreator: RoomGroupMessageCreator, + private val summaryGroupMessageCreator: SummaryGroupMessageCreator +) { + + fun Map.toNotifications(myUserDisplayName: String, myUserAvatarUrl: String?): List { + return map { (roomId, events) -> + when { + events.hasNoEventsToDisplay() -> RoomNotification.Removed(roomId) + else -> { + val messageEvents = events.onlyKeptEvents().filterNot { it.isRedacted } + roomGroupMessageCreator.createRoomMessage(messageEvents, roomId, myUserDisplayName, myUserAvatarUrl) + } + } + } + } + + private fun ProcessedMessageEvents.hasNoEventsToDisplay() = isEmpty() || all { + it.type == ProcessedEvent.Type.REMOVE || it.event.canNotBeDisplayed() + } + + private fun NotifiableMessageEvent.canNotBeDisplayed() = isRedacted + + @JvmName("toNotificationsInviteNotifiableEvent") + fun List>.toNotifications(myUserId: String): List { + return map { (processed, event) -> + when (processed) { + ProcessedEvent.Type.REMOVE -> OneShotNotification.Removed(key = event.roomId) + ProcessedEvent.Type.KEEP -> OneShotNotification.Append( + notificationUtils.buildRoomInvitationNotification(event, myUserId), + OneShotNotification.Append.Meta( + key = event.roomId, + summaryLine = event.description, + isNoisy = event.noisy, + timestamp = event.timestamp + ) + ) + } + } + } + + @JvmName("toNotificationsSimpleNotifiableEvent") + fun List>.toNotifications(myUserId: String): List { + return map { (processed, event) -> + when (processed) { + ProcessedEvent.Type.REMOVE -> OneShotNotification.Removed(key = event.eventId) + ProcessedEvent.Type.KEEP -> OneShotNotification.Append( + notificationUtils.buildSimpleEventNotification(event, myUserId), + OneShotNotification.Append.Meta( + key = event.eventId, + summaryLine = event.description, + isNoisy = event.noisy, + timestamp = event.timestamp + ) + ) + } + } + } + + fun createSummaryNotification(roomNotifications: List, + invitationNotifications: List, + simpleNotifications: List, + useCompleteNotificationFormat: Boolean): SummaryNotification { + val roomMeta = roomNotifications.filterIsInstance().map { it.meta } + val invitationMeta = invitationNotifications.filterIsInstance().map { it.meta } + val simpleMeta = simpleNotifications.filterIsInstance().map { it.meta } + return when { + roomMeta.isEmpty() && invitationMeta.isEmpty() && simpleMeta.isEmpty() -> SummaryNotification.Removed + else -> SummaryNotification.Update( + summaryGroupMessageCreator.createSummaryNotification( + roomNotifications = roomMeta, + invitationNotifications = invitationMeta, + simpleNotifications = simpleMeta, + useCompleteNotificationFormat = useCompleteNotificationFormat + )) + } + } +} + +sealed interface RoomNotification { + data class Removed(val roomId: String) : RoomNotification + data class Message(val notification: Notification, val shortcutInfo: ShortcutInfoCompat?, val meta: Meta) : RoomNotification { + data class Meta( + val summaryLine: CharSequence, + val messageCount: Int, + val latestTimestamp: Long, + val roomId: String, + val shouldBing: Boolean + ) + } +} + +sealed interface OneShotNotification { + data class Removed(val key: String) : OneShotNotification + data class Append(val notification: Notification, val meta: Meta) : OneShotNotification { + data class Meta( + val key: String, + val summaryLine: CharSequence, + val isNoisy: Boolean, + val timestamp: Long, + ) + } +} + +sealed interface SummaryNotification { + object Removed : SummaryNotification + data class Update(val notification: Notification) : SummaryNotification +} diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationRenderer.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationRenderer.kt new file mode 100644 index 0000000000..5afff89402 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationRenderer.kt @@ -0,0 +1,135 @@ +/* + * Copyright 2019 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.notifications + +import android.content.Context +import androidx.annotation.WorkerThread +import androidx.core.content.pm.ShortcutManagerCompat +import im.vector.app.features.notifications.NotificationDrawerManager.Companion.ROOM_EVENT_NOTIFICATION_ID +import im.vector.app.features.notifications.NotificationDrawerManager.Companion.ROOM_INVITATION_NOTIFICATION_ID +import im.vector.app.features.notifications.NotificationDrawerManager.Companion.ROOM_MESSAGES_NOTIFICATION_ID +import im.vector.app.features.notifications.NotificationDrawerManager.Companion.SUMMARY_NOTIFICATION_ID +import timber.log.Timber +import javax.inject.Inject + +class NotificationRenderer @Inject constructor(private val notificationDisplayer: NotificationDisplayer, + private val notificationFactory: NotificationFactory, + private val appContext: Context) { + + @WorkerThread + fun render(myUserId: String, + myUserDisplayName: String, + myUserAvatarUrl: String?, + useCompleteNotificationFormat: Boolean, + eventsToProcess: List>) { + val (roomEvents, simpleEvents, invitationEvents) = eventsToProcess.groupByType() + with(notificationFactory) { + val roomNotifications = roomEvents.toNotifications(myUserDisplayName, myUserAvatarUrl) + val invitationNotifications = invitationEvents.toNotifications(myUserId) + val simpleNotifications = simpleEvents.toNotifications(myUserId) + val summaryNotification = createSummaryNotification( + roomNotifications = roomNotifications, + invitationNotifications = invitationNotifications, + simpleNotifications = simpleNotifications, + useCompleteNotificationFormat = useCompleteNotificationFormat + ) + + // Remove summary first to avoid briefly displaying it after dismissing the last notification + when (summaryNotification) { + SummaryNotification.Removed -> { + Timber.d("Removing summary notification") + notificationDisplayer.cancelNotificationMessage(null, SUMMARY_NOTIFICATION_ID) + } + } + + roomNotifications.forEach { wrapper -> + when (wrapper) { + is RoomNotification.Removed -> { + Timber.d("Removing room messages notification ${wrapper.roomId}") + notificationDisplayer.cancelNotificationMessage(wrapper.roomId, ROOM_MESSAGES_NOTIFICATION_ID) + } + is RoomNotification.Message -> if (useCompleteNotificationFormat) { + Timber.d("Updating room messages notification ${wrapper.meta.roomId}") + wrapper.shortcutInfo?.let { + ShortcutManagerCompat.pushDynamicShortcut(appContext, it) + } + notificationDisplayer.showNotificationMessage(wrapper.meta.roomId, ROOM_MESSAGES_NOTIFICATION_ID, wrapper.notification) + } + } + } + + invitationNotifications.forEach { wrapper -> + when (wrapper) { + is OneShotNotification.Removed -> { + Timber.d("Removing invitation notification ${wrapper.key}") + notificationDisplayer.cancelNotificationMessage(wrapper.key, ROOM_INVITATION_NOTIFICATION_ID) + } + is OneShotNotification.Append -> if (useCompleteNotificationFormat) { + Timber.d("Updating invitation notification ${wrapper.meta.key}") + notificationDisplayer.showNotificationMessage(wrapper.meta.key, ROOM_INVITATION_NOTIFICATION_ID, wrapper.notification) + } + } + } + + simpleNotifications.forEach { wrapper -> + when (wrapper) { + is OneShotNotification.Removed -> { + Timber.d("Removing simple notification ${wrapper.key}") + notificationDisplayer.cancelNotificationMessage(wrapper.key, ROOM_EVENT_NOTIFICATION_ID) + } + is OneShotNotification.Append -> if (useCompleteNotificationFormat) { + Timber.d("Updating simple notification ${wrapper.meta.key}") + notificationDisplayer.showNotificationMessage(wrapper.meta.key, ROOM_EVENT_NOTIFICATION_ID, wrapper.notification) + } + } + } + + // Update summary last to avoid briefly displaying it before other notifications + when (summaryNotification) { + is SummaryNotification.Update -> { + Timber.d("Updating summary notification") + notificationDisplayer.showNotificationMessage(null, SUMMARY_NOTIFICATION_ID, summaryNotification.notification) + } + } + } + } +} + +private fun List>.groupByType(): GroupedNotificationEvents { + val roomIdToEventMap: MutableMap>> = LinkedHashMap() + val simpleEvents: MutableList> = ArrayList() + val invitationEvents: MutableList> = ArrayList() + forEach { + when (val event = it.event) { + is InviteNotifiableEvent -> invitationEvents.add(it.castedToEventType()) + is NotifiableMessageEvent -> { + val roomEvents = roomIdToEventMap.getOrPut(event.roomId) { ArrayList() } + roomEvents.add(it.castedToEventType()) + } + is SimpleNotifiableEvent -> simpleEvents.add(it.castedToEventType()) + } + } + return GroupedNotificationEvents(roomIdToEventMap, simpleEvents, invitationEvents) +} + +@Suppress("UNCHECKED_CAST") +private fun ProcessedEvent.castedToEventType(): ProcessedEvent = this as ProcessedEvent + +data class GroupedNotificationEvents( + val roomEvents: Map>>, + val simpleEvents: List>, + val invitationEvents: List> +) diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt index a74c13a496..491302a225 100755 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt @@ -47,12 +47,14 @@ import androidx.core.graphics.drawable.IconCompat import androidx.fragment.app.Fragment import im.vector.app.BuildConfig import im.vector.app.R +import im.vector.app.core.extensions.createIgnoredUri import im.vector.app.core.resources.StringProvider import im.vector.app.core.services.CallService import im.vector.app.core.utils.startNotificationChannelSettingsIntent import im.vector.app.features.call.VectorCallActivity import im.vector.app.features.call.service.CallHeadsUpActionReceiver import im.vector.app.features.call.webrtc.WebRtcCall +import im.vector.app.features.displayname.getBestName import im.vector.app.features.home.HomeActivity import im.vector.app.features.home.room.detail.RoomDetailActivity import im.vector.app.features.home.room.detail.RoomDetailArgs @@ -316,7 +318,7 @@ class NotificationUtils @Inject constructor(private val context: Context, mode = VectorCallActivity.INCOMING_RINGING ).apply { flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP - data = Uri.parse("foobar://${call.callId}") + data = createIgnoredUri(call.callId) } val contentPendingIntent = PendingIntent.getActivity(context, System.currentTimeMillis().toInt(), contentIntent, 0) @@ -377,7 +379,7 @@ class NotificationUtils @Inject constructor(private val context: Context, call = call, mode = null).apply { flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP - data = Uri.parse("foobar://$call.callId") + data = createIgnoredUri(call.callId) } val contentPendingIntent = PendingIntent.getActivity(context, System.currentTimeMillis().toInt(), contentIntent, 0) @@ -468,7 +470,6 @@ class NotificationUtils @Inject constructor(private val context: Context, setSmallIcon(R.drawable.ic_call_answer) } } - // This is a trick to make the previous notification with same id disappear as cancel notification is not working with Foreground Service. .setTimeoutAfter(1) .setColor(ThemeUtils.getColor(context, android.R.attr.colorPrimary)) .setCategory(NotificationCompat.CATEGORY_CALL) @@ -544,36 +545,27 @@ class NotificationUtils @Inject constructor(private val context: Context, .setWhen(lastMessageTimestamp) // MESSAGING_STYLE sets title and content for API 16 and above devices. .setStyle(messageStyle) - // A category allows groups of notifications to be ranked and filtered – per user or system settings. // For example, alarm notifications should display before promo notifications, or message from known contact // that can be displayed in not disturb mode if white listed (the later will need compat28.x) .setCategory(NotificationCompat.CATEGORY_MESSAGE) - // ID of the corresponding shortcut, for conversation features under API 30+ .setShortcutId(roomInfo.roomId) - // Title for API < 16 devices. .setContentTitle(roomInfo.roomDisplayName) // Content for API < 16 devices. .setContentText(stringProvider.getString(R.string.notification_new_messages)) - // Number of new notifications for API <24 (M and below) devices. .setSubText(stringProvider.getQuantityString(R.plurals.room_new_messages_notification, messageStyle.messages.size, messageStyle.messages.size)) - // Auto-bundling is enabled for 4 or more notifications on API 24+ (N+) // devices and all Wear devices. But we want a custom grouping, so we specify the groupID // TODO Group should be current user display name .setGroup(stringProvider.getString(R.string.app_name)) - // In order to avoid notification making sound twice (due to the summary notification) - .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY) - + .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_ALL) .setSmallIcon(smallIcon) - // Set primary color (important for Wear 2.0 Notifications). .setColor(accentColor) - // Sets priority for 25 and below. For 26 and above, 'priority' is deprecated for // 'importance' which is set in the NotificationChannel. The integers representing // 'priority' are different from 'importance', so make sure you don't mix them. @@ -593,15 +585,17 @@ class NotificationUtils @Inject constructor(private val context: Context, // Mark room as read val markRoomReadIntent = Intent(context, NotificationBroadcastReceiver::class.java) markRoomReadIntent.action = MARK_ROOM_READ_ACTION - markRoomReadIntent.data = Uri.parse("foobar://${roomInfo.roomId}") + markRoomReadIntent.data = createIgnoredUri(roomInfo.roomId) markRoomReadIntent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomInfo.roomId) val markRoomReadPendingIntent = PendingIntent.getBroadcast(context, System.currentTimeMillis().toInt(), markRoomReadIntent, PendingIntent.FLAG_UPDATE_CURRENT) - addAction(NotificationCompat.Action( - R.drawable.ic_material_done_all_white, - stringProvider.getString(R.string.action_mark_room_read), - markRoomReadPendingIntent)) + NotificationCompat.Action.Builder(R.drawable.ic_material_done_all_white, + stringProvider.getString(R.string.action_mark_room_read), markRoomReadPendingIntent) + .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_MARK_AS_READ) + .setShowsUserInterface(false) + .build() + .let { addAction(it) } // Quick reply if (!roomInfo.hasSmartReplyError) { @@ -612,6 +606,8 @@ class NotificationUtils @Inject constructor(private val context: Context, NotificationCompat.Action.Builder(R.drawable.vector_notification_quick_reply, stringProvider.getString(R.string.action_quick_reply), replyPendingIntent) .addRemoteInput(remoteInput) + .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_REPLY) + .setShowsUserInterface(false) .build() .let { addAction(it) } } @@ -646,10 +642,10 @@ class NotificationUtils @Inject constructor(private val context: Context, return NotificationCompat.Builder(context, channelID) .setOnlyAlertOnce(true) - .setContentTitle(stringProvider.getString(R.string.app_name)) + .setContentTitle(inviteNotifiableEvent.roomName ?: stringProvider.getString(R.string.app_name)) .setContentText(inviteNotifiableEvent.description) .setGroup(stringProvider.getString(R.string.app_name)) - .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY) + .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_ALL) .setSmallIcon(smallIcon) .setColor(accentColor) .apply { @@ -657,7 +653,7 @@ class NotificationUtils @Inject constructor(private val context: Context, // offer to type a quick reject button val rejectIntent = Intent(context, NotificationBroadcastReceiver::class.java) rejectIntent.action = REJECT_ACTION - rejectIntent.data = Uri.parse("foobar://$roomId&$matrixId") + rejectIntent.data = createIgnoredUri("$roomId&$matrixId") rejectIntent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomId) val rejectIntentPendingIntent = PendingIntent.getBroadcast(context, System.currentTimeMillis().toInt(), rejectIntent, PendingIntent.FLAG_UPDATE_CURRENT) @@ -670,7 +666,7 @@ class NotificationUtils @Inject constructor(private val context: Context, // offer to type a quick accept button val joinIntent = Intent(context, NotificationBroadcastReceiver::class.java) joinIntent.action = JOIN_ACTION - joinIntent.data = Uri.parse("foobar://$roomId&$matrixId") + joinIntent.data = createIgnoredUri("$roomId&$matrixId") joinIntent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomId) val joinIntentPendingIntent = PendingIntent.getBroadcast(context, System.currentTimeMillis().toInt(), joinIntent, PendingIntent.FLAG_UPDATE_CURRENT) @@ -679,10 +675,10 @@ class NotificationUtils @Inject constructor(private val context: Context, stringProvider.getString(R.string.join), joinIntentPendingIntent) - val contentIntent = HomeActivity.newIntent(context) + val contentIntent = HomeActivity.newIntent(context, inviteNotificationRoomId = inviteNotifiableEvent.roomId) contentIntent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP // pending intent get reused by system, this will mess up the extra params, so put unique info to avoid that - contentIntent.data = Uri.parse("foobar://" + inviteNotifiableEvent.eventId) + contentIntent.data = createIgnoredUri(inviteNotifiableEvent.eventId) setContentIntent(PendingIntent.getActivity(context, 0, contentIntent, 0)) if (inviteNotifiableEvent.noisy) { @@ -713,7 +709,7 @@ class NotificationUtils @Inject constructor(private val context: Context, .setContentTitle(stringProvider.getString(R.string.app_name)) .setContentText(simpleNotifiableEvent.description) .setGroup(stringProvider.getString(R.string.app_name)) - .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY) + .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_ALL) .setSmallIcon(smallIcon) .setColor(accentColor) .setAutoCancel(true) @@ -721,7 +717,7 @@ class NotificationUtils @Inject constructor(private val context: Context, val contentIntent = HomeActivity.newIntent(context) contentIntent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP // pending intent get reused by system, this will mess up the extra params, so put unique info to avoid that - contentIntent.data = Uri.parse("foobar://" + simpleNotifiableEvent.eventId) + contentIntent.data = createIgnoredUri(simpleNotifiableEvent.eventId) setContentIntent(PendingIntent.getActivity(context, 0, contentIntent, 0)) if (simpleNotifiableEvent.noisy) { @@ -743,7 +739,7 @@ class NotificationUtils @Inject constructor(private val context: Context, val roomIntentTap = RoomDetailActivity.newIntent(context, RoomDetailArgs(roomId)) roomIntentTap.action = TAP_TO_VIEW_ACTION // pending intent get reused by system, this will mess up the extra params, so put unique info to avoid that - roomIntentTap.data = Uri.parse("foobar://openRoom?$roomId") + roomIntentTap.data = createIgnoredUri("openRoom?$roomId") // Recreate the back stack return TaskStackBuilder.create(context) @@ -755,7 +751,7 @@ class NotificationUtils @Inject constructor(private val context: Context, private fun buildOpenHomePendingIntentForSummary(): PendingIntent { val intent = HomeActivity.newIntent(context, clearNotification = true) intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP - intent.data = Uri.parse("foobar://tapSummary") + intent.data = createIgnoredUri("tapSummary") return PendingIntent.getActivity(context, Random.nextInt(1000), intent, PendingIntent.FLAG_UPDATE_CURRENT) } @@ -771,7 +767,7 @@ class NotificationUtils @Inject constructor(private val context: Context, if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { intent = Intent(context, NotificationBroadcastReceiver::class.java) intent.action = SMART_REPLY_ACTION - intent.data = Uri.parse("foobar://$roomId") + intent.data = createIgnoredUri(roomId) intent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomId) return PendingIntent.getBroadcast(context, System.currentTimeMillis().toInt(), intent, PendingIntent.FLAG_UPDATE_CURRENT) @@ -786,7 +782,7 @@ class NotificationUtils @Inject constructor(private val context: Context, // the action must be unique else the parameters are ignored quickReplyIntent.action = QUICK_LAUNCH_ACTION - quickReplyIntent.data = Uri.parse("foobar://$roomId") + quickReplyIntent.data = createIgnoredUri($roomId") return PendingIntent.getActivity(context, 0, quickReplyIntent, 0) } */ @@ -806,6 +802,7 @@ class NotificationUtils @Inject constructor(private val context: Context, val smallIcon = R.drawable.ic_status_bar return NotificationCompat.Builder(context, if (noisy) NOISY_NOTIFICATION_CHANNEL_ID else SILENT_NOTIFICATION_CHANNEL_ID) + .setOnlyAlertOnce(true) // used in compat < N, after summary is built based on child notifications .setWhen(lastMessageTimestamp) .setStyle(style) @@ -839,7 +836,7 @@ class NotificationUtils @Inject constructor(private val context: Context, private fun getDismissSummaryPendingIntent(): PendingIntent { val intent = Intent(context, NotificationBroadcastReceiver::class.java) intent.action = DISMISS_SUMMARY_ACTION - intent.data = Uri.parse("foobar://deleteSummary") + intent.data = createIgnoredUri("deleteSummary") return PendingIntent.getBroadcast(context.applicationContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) } @@ -919,8 +916,8 @@ class NotificationUtils @Inject constructor(private val context: Context, // We cannot use NotificationManagerCompat here. val setting = context.getSystemService()!!.currentInterruptionFilter - return setting == NotificationManager.INTERRUPTION_FILTER_NONE - || setting == NotificationManager.INTERRUPTION_FILTER_ALARMS + return setting == NotificationManager.INTERRUPTION_FILTER_NONE || + setting == NotificationManager.INTERRUPTION_FILTER_ALARMS } private fun getActionText(@StringRes stringRes: Int, @AttrRes colorRes: Int): Spannable { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/initsync/InitialSyncProgressService.kt b/vector/src/main/java/im/vector/app/features/notifications/ProcessedEvent.kt similarity index 56% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/initsync/InitialSyncProgressService.kt rename to vector/src/main/java/im/vector/app/features/notifications/ProcessedEvent.kt index b5d4ef4dbb..8bd9819ca9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/initsync/InitialSyncProgressService.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/ProcessedEvent.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 The Matrix.org Foundation C.I.C. + * 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. @@ -13,19 +13,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.matrix.android.sdk.api.session.initsync -import androidx.lifecycle.LiveData +package im.vector.app.features.notifications -interface InitialSyncProgressService { +data class ProcessedEvent( + val type: Type, + val event: T +) { - fun getInitialSyncProgressStatus(): LiveData - - sealed class Status { - object Idle : Status() - data class Progressing( - val initSyncStep: InitSyncStep, - val percentProgress: Int = 0 - ) : Status() + enum class Type { + KEEP, + REMOVE } } + +fun List>.onlyKeptEvents() = mapNotNull { processedEvent -> + processedEvent.event.takeIf { processedEvent.type == ProcessedEvent.Type.KEEP } +} diff --git a/vector/src/main/java/im/vector/app/features/notifications/PushRuleTriggerListener.kt b/vector/src/main/java/im/vector/app/features/notifications/PushRuleTriggerListener.kt index 791803fa49..abbbd47f95 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/PushRuleTriggerListener.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/PushRuleTriggerListener.kt @@ -40,12 +40,11 @@ class PushRuleTriggerListener @Inject constructor( val notificationAction = actions.toNotificationAction() if (notificationAction.shouldNotify) { - val notifiableEvent = resolver.resolveEvent(event, safeSession) + val notifiableEvent = resolver.resolveEvent(event, safeSession, isNoisy = !notificationAction.soundName.isNullOrBlank()) if (notifiableEvent == null) { Timber.v("## Failed to resolve event") // TODO } else { - notifiableEvent.noisy = !notificationAction.soundName.isNullOrBlank() Timber.v("New event to notify") notificationDrawerManager.onNotifiableEventReceived(notifiableEvent) } diff --git a/vector/src/main/java/im/vector/app/features/notifications/RoomGroupMessageCreator.kt b/vector/src/main/java/im/vector/app/features/notifications/RoomGroupMessageCreator.kt new file mode 100644 index 0000000000..bdd7d026f9 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/notifications/RoomGroupMessageCreator.kt @@ -0,0 +1,170 @@ +/* + * 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.notifications + +import android.content.Context +import android.graphics.Bitmap +import android.os.Build +import androidx.core.app.NotificationCompat +import androidx.core.app.Person +import androidx.core.content.pm.ShortcutInfoCompat +import androidx.core.graphics.drawable.IconCompat +import im.vector.app.R +import im.vector.app.core.resources.StringProvider +import im.vector.app.features.home.room.detail.RoomDetailActivity +import me.gujun.android.span.Span +import me.gujun.android.span.span +import timber.log.Timber +import javax.inject.Inject + +class RoomGroupMessageCreator @Inject constructor( + private val iconLoader: IconLoader, + private val bitmapLoader: BitmapLoader, + private val stringProvider: StringProvider, + private val notificationUtils: NotificationUtils, + private val appContext: Context +) { + + fun createRoomMessage(events: List, roomId: String, userDisplayName: String, userAvatarUrl: String?): RoomNotification.Message { + val firstKnownRoomEvent = events[0] + val roomName = firstKnownRoomEvent.roomName ?: firstKnownRoomEvent.senderName ?: "" + val roomIsGroup = !firstKnownRoomEvent.roomIsDirect + val style = NotificationCompat.MessagingStyle(Person.Builder() + .setName(userDisplayName) + .setIcon(iconLoader.getUserIcon(userAvatarUrl)) + .setKey(firstKnownRoomEvent.matrixID) + .build() + ).also { + it.conversationTitle = roomName.takeIf { roomIsGroup } + it.isGroupConversation = roomIsGroup + it.addMessagesFromEvents(events) + } + + val tickerText = if (roomIsGroup) { + stringProvider.getString(R.string.notification_ticker_text_group, roomName, events.last().senderName, events.last().description) + } else { + stringProvider.getString(R.string.notification_ticker_text_dm, events.last().senderName, events.last().description) + } + + val largeBitmap = getRoomBitmap(events) + val shortcutInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + val openRoomIntent = RoomDetailActivity.shortcutIntent(appContext, roomId) + ShortcutInfoCompat.Builder(appContext, roomId) + .setLongLived(true) + .setIntent(openRoomIntent) + .setShortLabel(roomName) + .setIcon(largeBitmap?.let { IconCompat.createWithAdaptiveBitmap(it) } ?: iconLoader.getUserIcon(events.last().senderAvatarPath)) + .build() + } else { + null + } + + val lastMessageTimestamp = events.last().timestamp + val smartReplyErrors = events.filter { it.isSmartReplyError() } + val messageCount = (events.size - smartReplyErrors.size) + val meta = RoomNotification.Message.Meta( + summaryLine = createRoomMessagesGroupSummaryLine(events, roomName, roomIsDirect = !roomIsGroup), + messageCount = messageCount, + latestTimestamp = lastMessageTimestamp, + roomId = roomId, + shouldBing = events.any { it.noisy } + ) + return RoomNotification.Message( + notificationUtils.buildMessagesListNotification( + style, + RoomEventGroupInfo(roomId, roomName, isDirect = !roomIsGroup).also { + it.hasSmartReplyError = smartReplyErrors.isNotEmpty() + it.shouldBing = meta.shouldBing + it.customSound = events.last().soundName + }, + largeIcon = largeBitmap, + lastMessageTimestamp, + userDisplayName, + tickerText + ), + shortcutInfo, + meta + ) + } + + private fun NotificationCompat.MessagingStyle.addMessagesFromEvents(events: List) { + events.forEach { event -> + val senderPerson = if (event.outGoingMessage) { + null + } else { + Person.Builder() + .setName(event.senderName) + .setIcon(iconLoader.getUserIcon(event.senderAvatarPath)) + .setKey(event.senderId) + .build() + } + when { + event.isSmartReplyError() -> addMessage(stringProvider.getString(R.string.notification_inline_reply_failed), event.timestamp, senderPerson) + else -> addMessage(event.body, event.timestamp, senderPerson) + } + } + } + + private fun createRoomMessagesGroupSummaryLine(events: List, roomName: String, roomIsDirect: Boolean): CharSequence { + return try { + when (events.size) { + 1 -> createFirstMessageSummaryLine(events.first(), roomName, roomIsDirect) + else -> { + stringProvider.getQuantityString( + R.plurals.notification_compat_summary_line_for_room, + events.size, + roomName, + events.size + ) + } + } + } catch (e: Throwable) { + // String not found or bad format + Timber.v("%%%%%%%% REFRESH NOTIFICATION DRAWER failed to resolve string") + roomName + } + } + + private fun createFirstMessageSummaryLine(event: NotifiableMessageEvent, roomName: String, roomIsDirect: Boolean): Span { + return if (roomIsDirect) { + span { + span { + textStyle = "bold" + +String.format("%s: ", event.senderName) + } + +(event.description) + } + } else { + span { + span { + textStyle = "bold" + +String.format("%s: %s ", roomName, event.senderName) + } + +(event.description) + } + } + } + + private fun getRoomBitmap(events: List): Bitmap? { + // Use the last event (most recent?) + return events.lastOrNull() + ?.roomAvatarPath + ?.let { bitmapLoader.getRoomBitmap(it) } + } +} + +private fun NotifiableMessageEvent.isSmartReplyError() = outGoingMessage && outGoingMessageFailed diff --git a/vector/src/main/java/im/vector/app/features/notifications/SimpleNotifiableEvent.kt b/vector/src/main/java/im/vector/app/features/notifications/SimpleNotifiableEvent.kt index 2f74737ba2..8c72372204 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/SimpleNotifiableEvent.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/SimpleNotifiableEvent.kt @@ -15,21 +15,16 @@ */ package im.vector.app.features.notifications -import androidx.core.app.NotificationCompat - data class SimpleNotifiableEvent( - override var matrixID: String?, + val matrixID: String?, override val eventId: String, override val editedEventId: String?, - override var noisy: Boolean, - override val title: String, - override val description: String, - override val type: String?, - override val timestamp: Long, - override var soundName: String?, - override var isPushGatewayEvent: Boolean = false) : NotifiableEvent { - - override var hasBeenDisplayed: Boolean = false - override var isRedacted: Boolean = false - override var lockScreenVisibility = NotificationCompat.VISIBILITY_PUBLIC -} + val noisy: Boolean, + val title: String, + val description: String, + val type: String?, + val timestamp: Long, + val soundName: String?, + override var canBeReplaced: Boolean, + override val isRedacted: Boolean = false +) : NotifiableEvent diff --git a/vector/src/main/java/im/vector/app/features/notifications/SummaryGroupMessageCreator.kt b/vector/src/main/java/im/vector/app/features/notifications/SummaryGroupMessageCreator.kt new file mode 100644 index 0000000000..91163434c2 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/notifications/SummaryGroupMessageCreator.kt @@ -0,0 +1,146 @@ +/* + * 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.notifications + +import android.app.Notification +import androidx.core.app.NotificationCompat +import im.vector.app.R +import im.vector.app.core.resources.StringProvider +import javax.inject.Inject + +/** + * ======== Build summary notification ========= + * On Android 7.0 (API level 24) and higher, the system automatically builds a summary for + * your group using snippets of text from each notification. The user can expand this + * notification to see each separate notification. + * To support older versions, which cannot show a nested group of notifications, + * you must create an extra notification that acts as the summary. + * This appears as the only notification and the system hides all the others. + * So this summary should include a snippet from all the other notifications, + * which the user can tap to open your app. + * The behavior of the group summary may vary on some device types such as wearables. + * To ensure the best experience on all devices and versions, always include a group summary when you create a group + * https://developer.android.com/training/notify-user/group + */ +class SummaryGroupMessageCreator @Inject constructor( + private val stringProvider: StringProvider, + private val notificationUtils: NotificationUtils +) { + + fun createSummaryNotification(roomNotifications: List, + invitationNotifications: List, + simpleNotifications: List, + useCompleteNotificationFormat: Boolean): Notification { + val summaryInboxStyle = NotificationCompat.InboxStyle().also { style -> + roomNotifications.forEach { style.addLine(it.summaryLine) } + invitationNotifications.forEach { style.addLine(it.summaryLine) } + simpleNotifications.forEach { style.addLine(it.summaryLine) } + } + + val summaryIsNoisy = roomNotifications.any { it.shouldBing } || + invitationNotifications.any { it.isNoisy } || + simpleNotifications.any { it.isNoisy } + + val messageCount = roomNotifications.fold(initial = 0) { acc, current -> acc + current.messageCount } + + val lastMessageTimestamp = roomNotifications.lastOrNull()?.latestTimestamp + ?: invitationNotifications.lastOrNull()?.timestamp + ?: simpleNotifications.last().timestamp + + // FIXME roomIdToEventMap.size is not correct, this is the number of rooms + val nbEvents = roomNotifications.size + simpleNotifications.size + val sumTitle = stringProvider.getQuantityString(R.plurals.notification_compat_summary_title, nbEvents, nbEvents) + summaryInboxStyle.setBigContentTitle(sumTitle) + // TODO get latest event? + .setSummaryText(stringProvider.getQuantityString(R.plurals.notification_unread_notified_messages, nbEvents, nbEvents)) + return if (useCompleteNotificationFormat) { + notificationUtils.buildSummaryListNotification( + summaryInboxStyle, + sumTitle, + noisy = summaryIsNoisy, + lastMessageTimestamp = lastMessageTimestamp + ) + } else { + processSimpleGroupSummary( + summaryIsNoisy, + messageCount, + simpleNotifications.size, + invitationNotifications.size, + roomNotifications.size, + lastMessageTimestamp + ) + } + } + + private fun processSimpleGroupSummary(summaryIsNoisy: Boolean, + messageEventsCount: Int, + simpleEventsCount: Int, + invitationEventsCount: Int, + roomCount: Int, + lastMessageTimestamp: Long): Notification { + // Add the simple events as message (?) + val messageNotificationCount = messageEventsCount + simpleEventsCount + + val privacyTitle = if (invitationEventsCount > 0) { + val invitationsStr = stringProvider.getQuantityString(R.plurals.notification_invitations, invitationEventsCount, invitationEventsCount) + if (messageNotificationCount > 0) { + // Invitation and message + val messageStr = stringProvider.getQuantityString(R.plurals.room_new_messages_notification, + messageNotificationCount, messageNotificationCount) + if (roomCount > 1) { + // In several rooms + val roomStr = stringProvider.getQuantityString(R.plurals.notification_unread_notified_messages_in_room_rooms, + roomCount, roomCount) + stringProvider.getString( + R.string.notification_unread_notified_messages_in_room_and_invitation, + messageStr, + roomStr, + invitationsStr + ) + } else { + // In one room + stringProvider.getString( + R.string.notification_unread_notified_messages_and_invitation, + messageStr, + invitationsStr + ) + } + } else { + // Only invitation + invitationsStr + } + } else { + // No invitation, only messages + val messageStr = stringProvider.getQuantityString(R.plurals.room_new_messages_notification, + messageNotificationCount, messageNotificationCount) + if (roomCount > 1) { + // In several rooms + val roomStr = stringProvider.getQuantityString(R.plurals.notification_unread_notified_messages_in_room_rooms, roomCount, roomCount) + stringProvider.getString(R.string.notification_unread_notified_messages_in_room, messageStr, roomStr) + } else { + // In one room + messageStr + } + } + return notificationUtils.buildSummaryListNotification( + style = null, + compatSummary = privacyTitle, + noisy = summaryIsNoisy, + lastMessageTimestamp = lastMessageTimestamp + ) + } +} diff --git a/vector/src/main/java/im/vector/app/features/permalink/PermalinkHandler.kt b/vector/src/main/java/im/vector/app/features/permalink/PermalinkHandler.kt index ecaeea1899..40cc0b3e13 100644 --- a/vector/src/main/java/im/vector/app/features/permalink/PermalinkHandler.kt +++ b/vector/src/main/java/im/vector/app/features/permalink/PermalinkHandler.kt @@ -18,95 +18,90 @@ package im.vector.app.features.permalink import android.content.Context import android.net.Uri +import androidx.core.net.toUri import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder +import im.vector.app.core.extensions.isIgnored import im.vector.app.core.utils.toast import im.vector.app.features.navigation.Navigator import im.vector.app.features.roomdirectory.roompreview.RoomPreviewData -import io.reactivex.Single -import io.reactivex.android.schedulers.AndroidSchedulers -import io.reactivex.schedulers.Schedulers +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.permalinks.PermalinkData import org.matrix.android.sdk.api.session.permalinks.PermalinkParser +import org.matrix.android.sdk.api.session.permalinks.PermalinkService import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.RoomType -import org.matrix.android.sdk.api.util.Optional -import org.matrix.android.sdk.api.util.toOptional -import org.matrix.android.sdk.rx.rx import javax.inject.Inject class PermalinkHandler @Inject constructor(private val activeSessionHolder: ActiveSessionHolder, private val navigator: Navigator) { - fun launch( + suspend fun launch( context: Context, deepLink: String?, navigationInterceptor: NavigationInterceptor? = null, buildTask: Boolean = false - ): Single { + ): Boolean { val uri = deepLink?.let { Uri.parse(it) } return launch(context, uri, navigationInterceptor, buildTask) } - fun launch( + suspend fun launch( context: Context, deepLink: Uri?, navigationInterceptor: NavigationInterceptor? = null, buildTask: Boolean = false - ): Single { - if (deepLink == null) { - return Single.just(false) + ): Boolean { + return when { + deepLink == null -> false + deepLink.isIgnored() -> true + !isPermalinkSupported(context, deepLink.toString()) -> false + else -> { + tryOrNull { + withContext(Dispatchers.Default) { + val permalinkData = PermalinkParser.parse(deepLink) + handlePermalink(permalinkData, deepLink, context, navigationInterceptor, buildTask) + } + } ?: false + } } - return Single - .fromCallable { - PermalinkParser.parse(deepLink) - } - .subscribeOn(Schedulers.computation()) - .observeOn(AndroidSchedulers.mainThread()) - .flatMap { permalinkData -> - handlePermalink(permalinkData, deepLink, context, navigationInterceptor, buildTask) - } - .onErrorReturnItem(false) } - private fun handlePermalink( + private suspend fun handlePermalink( permalinkData: PermalinkData, rawLink: Uri, context: Context, navigationInterceptor: NavigationInterceptor?, buildTask: Boolean - ): Single { + ): Boolean { return when (permalinkData) { is PermalinkData.RoomLink -> { - permalinkData.getRoomId() - .observeOn(AndroidSchedulers.mainThread()) - .map { - val roomId = it.getOrNull() - if (navigationInterceptor?.navToRoom(roomId, permalinkData.eventId, rawLink) != true) { - openRoom( - context = context, - roomId = roomId, - permalinkData = permalinkData, - rawLink = rawLink, - buildTask = buildTask - ) - } - true - } + val roomId = permalinkData.getRoomId() + openRoom( + navigationInterceptor, + context = context, + roomId = roomId, + permalinkData = permalinkData, + rawLink = rawLink, + buildTask = buildTask + ) + true } is PermalinkData.GroupLink -> { navigator.openGroupDetail(permalinkData.groupId, context, buildTask) - Single.just(true) + true } is PermalinkData.UserLink -> { if (navigationInterceptor?.navToMemberProfile(permalinkData.userId, rawLink) != true) { navigator.openRoomMemberProfile(userId = permalinkData.userId, roomId = null, context = context, buildTask = buildTask) } - Single.just(true) + true } is PermalinkData.FallbackLink -> { - Single.just(false) + false } is PermalinkData.RoomEmailInviteLink -> { val data = RoomPreviewData( @@ -117,20 +112,24 @@ class PermalinkHandler @Inject constructor(private val activeSessionHolder: Acti roomType = permalinkData.roomType ) navigator.openRoomPreview(context, data) - Single.just(true) + true } } } - private fun PermalinkData.RoomLink.getRoomId(): Single> { + private fun isPermalinkSupported(context: Context, url: String): Boolean { + return url.startsWith(PermalinkService.MATRIX_TO_URL_BASE) || + context.resources.getStringArray(R.array.permalink_supported_hosts) + .any { url.toUri().host == it } + } + + private suspend fun PermalinkData.RoomLink.getRoomId(): String? { val session = activeSessionHolder.getSafeActiveSession() return if (isRoomAlias && session != null) { - session.rx() - .getRoomIdByAlias(roomIdOrAlias, true) - .map { it.getOrNull()?.roomId.toOptional() } - .subscribeOn(Schedulers.io()) + val roomIdByAlias = session.getRoomIdByAlias(roomIdOrAlias, true) + roomIdByAlias.getOrNull()?.roomId } else { - Single.just(Optional.from(roomIdOrAlias)) + roomIdOrAlias } } @@ -146,6 +145,7 @@ class PermalinkHandler @Inject constructor(private val activeSessionHolder: Acti * Open room either joined, or not */ private fun openRoom( + navigationInterceptor: NavigationInterceptor?, context: Context, roomId: String?, permalinkData: PermalinkData.RoomLink, @@ -167,7 +167,7 @@ class PermalinkHandler @Inject constructor(private val activeSessionHolder: Acti membership?.isActive().orFalse() -> { if (!isSpace && membership == Membership.JOIN) { // If it's a room you're in, let's just open it, you can tap back if needed - navigator.openRoom(context, roomId, eventId, buildTask) + navigationInterceptor.openJoinedRoomScreen(buildTask, roomId, eventId, rawLink, context) } else { // maybe open space preview navigator.openSpacePreview(context, roomId)? if already joined? navigator.openMatrixToBottomSheet(context, rawLink.toString()) @@ -179,6 +179,18 @@ class PermalinkHandler @Inject constructor(private val activeSessionHolder: Acti } } } + + private fun NavigationInterceptor?.openJoinedRoomScreen(buildTask: Boolean, roomId: String, eventId: String?, rawLink: Uri, context: Context) { + if (this?.navToRoom(roomId, eventId, rawLink) != true) { + navigator.openRoom(context, roomId, eventId, buildTask) + } + } + + companion object { + const val MATRIX_TO_CUSTOM_SCHEME_URL_BASE = "element://" + const val ROOM_LINK_PREFIX = "${MATRIX_TO_CUSTOM_SCHEME_URL_BASE}room/" + const val USER_LINK_PREFIX = "${MATRIX_TO_CUSTOM_SCHEME_URL_BASE}user/" + } } interface NavigationInterceptor { diff --git a/vector/src/main/java/im/vector/app/features/permalink/PermalinkHandlerActivity.kt b/vector/src/main/java/im/vector/app/features/permalink/PermalinkHandlerActivity.kt deleted file mode 100644 index ee4e0e05b5..0000000000 --- a/vector/src/main/java/im/vector/app/features/permalink/PermalinkHandlerActivity.kt +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright 2019 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.permalink - -import android.content.Intent -import android.os.Bundle -import im.vector.app.R -import im.vector.app.core.di.ActiveSessionHolder -import im.vector.app.core.di.ScreenComponent -import im.vector.app.core.extensions.replaceFragment -import im.vector.app.core.platform.VectorBaseActivity -import im.vector.app.databinding.FragmentProgressBinding -import im.vector.app.features.home.HomeActivity -import im.vector.app.features.home.LoadingFragment -import javax.inject.Inject - -class PermalinkHandlerActivity : VectorBaseActivity() { - - @Inject lateinit var permalinkHandler: PermalinkHandler - @Inject lateinit var sessionHolder: ActiveSessionHolder - - override fun getBinding() = FragmentProgressBinding.inflate(layoutInflater) - - override fun injectWith(injector: ScreenComponent) { - injector.inject(this) - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_simple) - if (isFirstCreation()) { - replaceFragment(R.id.simpleFragmentContainer, LoadingFragment::class.java) - } - handleIntent() - } - - private fun handleIntent() { - // If we are not logged in, open login screen. - // In the future, we might want to relaunch the process after login. - if (!sessionHolder.hasActiveSession()) { - startLoginActivity() - return - } - // We forward intent to HomeActivity (singleTask) to avoid the dueling app problem - // https://stackoverflow.com/questions/25884954/deep-linking-and-multiple-app-instances - intent.setClass(this, HomeActivity::class.java) - intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP - startActivity(intent) - - finish() - } - - override fun onNewIntent(intent: Intent?) { - super.onNewIntent(intent) - handleIntent() - } - - private fun startLoginActivity() { - navigator.openLogin( - context = this, - flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK - ) - finish() - } -} diff --git a/vector/src/main/java/im/vector/app/features/pin/PinActivity.kt b/vector/src/main/java/im/vector/app/features/pin/PinActivity.kt index 6866afa0a6..0978f0d5b5 100644 --- a/vector/src/main/java/im/vector/app/features/pin/PinActivity.kt +++ b/vector/src/main/java/im/vector/app/features/pin/PinActivity.kt @@ -18,20 +18,22 @@ package im.vector.app.features.pin import android.content.Context import android.content.Intent +import com.airbnb.mvrx.Mavericks import com.google.android.material.appbar.MaterialToolbar -import com.airbnb.mvrx.MvRx +import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R import im.vector.app.core.extensions.addFragment import im.vector.app.core.platform.ToolbarConfigurable import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.databinding.ActivitySimpleBinding +@AndroidEntryPoint class PinActivity : VectorBaseActivity(), ToolbarConfigurable, UnlockedActivity { companion object { fun newIntent(context: Context, args: PinArgs): Intent { return Intent(context, PinActivity::class.java).apply { - putExtra(MvRx.KEY_ARG, args) + putExtra(Mavericks.KEY_ARG, args) } } } @@ -42,7 +44,7 @@ class PinActivity : VectorBaseActivity(), ToolbarConfigur override fun initUiAndData() { if (isFirstCreation()) { - val fragmentArgs: PinArgs = intent?.extras?.getParcelable(MvRx.KEY_ARG) ?: return + val fragmentArgs: PinArgs = intent?.extras?.getParcelable(Mavericks.KEY_ARG) ?: return addFragment(R.id.simpleFragmentContainer, PinFragment::class.java, fragmentArgs) } } diff --git a/vector/src/main/java/im/vector/app/features/pin/PinCodeStore.kt b/vector/src/main/java/im/vector/app/features/pin/PinCodeStore.kt index fb7c6897e2..d3632edbca 100644 --- a/vector/src/main/java/im/vector/app/features/pin/PinCodeStore.kt +++ b/vector/src/main/java/im/vector/app/features/pin/PinCodeStore.kt @@ -25,6 +25,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.matrix.android.sdk.api.extensions.orFalse import javax.inject.Inject +import javax.inject.Singleton import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine @@ -56,26 +57,40 @@ interface PinCodeStore { * Will reset the counters */ fun resetCounters() + + fun addListener(listener: PinCodeStoreListener) + fun removeListener(listener: PinCodeStoreListener) } -class SharedPrefPinCodeStore @Inject constructor(private val sharedPreferences: SharedPreferences) : PinCodeStore { +interface PinCodeStoreListener { + fun onPinSetUpChange(isConfigured: Boolean) +} - override suspend fun storeEncodedPin(encodePin: String) = withContext(Dispatchers.IO) { - sharedPreferences.edit { - putString(ENCODED_PIN_CODE_KEY, encodePin) +@Singleton +class SharedPrefPinCodeStore @Inject constructor(private val sharedPreferences: SharedPreferences) : PinCodeStore { + private val listeners = mutableSetOf() + + override suspend fun storeEncodedPin(encodePin: String) { + withContext(Dispatchers.IO) { + sharedPreferences.edit { + putString(ENCODED_PIN_CODE_KEY, encodePin) + } } + listeners.forEach { it.onPinSetUpChange(isConfigured = true) } } - override suspend fun deleteEncodedPin() = withContext(Dispatchers.IO) { - // Also reset the counters - resetCounters() - sharedPreferences.edit { - remove(ENCODED_PIN_CODE_KEY) + override suspend fun deleteEncodedPin() { + withContext(Dispatchers.IO) { + // Also reset the counters + resetCounters() + sharedPreferences.edit { + remove(ENCODED_PIN_CODE_KEY) + } + awaitPinCodeCallback { + PFSecurityManager.getInstance().pinCodeHelper.delete(it) + } } - awaitPinCodeCallback { - PFSecurityManager.getInstance().pinCodeHelper.delete(it) - } - return@withContext + listeners.forEach { it.onPinSetUpChange(isConfigured = false) } } override fun getEncodedPin(): String? { @@ -124,6 +139,14 @@ class SharedPrefPinCodeStore @Inject constructor(private val sharedPreferences: } } + override fun addListener(listener: PinCodeStoreListener) { + listeners.add(listener) + } + + override fun removeListener(listener: PinCodeStoreListener) { + listeners.remove(listener) + } + private suspend inline fun awaitPinCodeCallback(crossinline callback: (PFPinCodeHelperCallback) -> Unit) = suspendCoroutine> { cont -> callback(PFPinCodeHelperCallback { result -> cont.resume(result) }) } diff --git a/vector/src/main/java/im/vector/app/features/pin/PinFragment.kt b/vector/src/main/java/im/vector/app/features/pin/PinFragment.kt index 99b0a93528..2309d42f60 100644 --- a/vector/src/main/java/im/vector/app/features/pin/PinFragment.kt +++ b/vector/src/main/java/im/vector/app/features/pin/PinFragment.kt @@ -36,8 +36,8 @@ import im.vector.app.databinding.FragmentPinBinding import im.vector.app.features.MainActivity import im.vector.app.features.MainActivityArgs import im.vector.app.features.settings.VectorPreferences -import kotlinx.parcelize.Parcelize import kotlinx.coroutines.launch +import kotlinx.parcelize.Parcelize import javax.inject.Inject @Parcelize diff --git a/vector/src/main/java/im/vector/app/features/popup/IncomingCallAlert.kt b/vector/src/main/java/im/vector/app/features/popup/IncomingCallAlert.kt index 17ad35f07a..3b7aa18ee9 100644 --- a/vector/src/main/java/im/vector/app/features/popup/IncomingCallAlert.kt +++ b/vector/src/main/java/im/vector/app/features/popup/IncomingCallAlert.kt @@ -22,6 +22,7 @@ import im.vector.app.R import im.vector.app.core.extensions.setLeftDrawable import im.vector.app.core.glide.GlideApp import im.vector.app.databinding.AlerterIncomingCallLayoutBinding +import im.vector.app.features.displayname.getBestName import im.vector.app.features.home.AvatarRenderer import org.matrix.android.sdk.api.util.MatrixItem diff --git a/vector/src/main/java/im/vector/app/features/popup/JitsiCallAlert.kt b/vector/src/main/java/im/vector/app/features/popup/JitsiCallAlert.kt index 1c0ec65c36..00c429a621 100644 --- a/vector/src/main/java/im/vector/app/features/popup/JitsiCallAlert.kt +++ b/vector/src/main/java/im/vector/app/features/popup/JitsiCallAlert.kt @@ -21,6 +21,7 @@ import android.view.View import im.vector.app.R import im.vector.app.core.glide.GlideApp import im.vector.app.databinding.AlerterJitsiCallLayoutBinding +import im.vector.app.features.displayname.getBestName import im.vector.app.features.home.AvatarRenderer import org.matrix.android.sdk.api.util.MatrixItem diff --git a/vector/src/main/java/im/vector/app/features/popup/PopupAlertManager.kt b/vector/src/main/java/im/vector/app/features/popup/PopupAlertManager.kt index 9842709f9e..9084c64a40 100644 --- a/vector/src/main/java/im/vector/app/features/popup/PopupAlertManager.kt +++ b/vector/src/main/java/im/vector/app/features/popup/PopupAlertManager.kt @@ -52,7 +52,7 @@ class PopupAlertManager @Inject constructor() { private val alertQueue = mutableListOf() - fun hasAlertsToShow() : Boolean { + fun hasAlertsToShow(): Boolean { return currentAlerter != null || alertQueue.isNotEmpty() } @@ -298,10 +298,10 @@ class PopupAlertManager @Inject constructor() { } private fun shouldBeDisplayedIn(alert: VectorAlert?, activity: Activity): Boolean { - return alert != null - && activity !is PinActivity - && activity !is SignedOutActivity - && activity is VectorBaseActivity<*> - && alert.shouldBeDisplayedIn.invoke(activity) + return alert != null && + activity !is PinActivity && + activity !is SignedOutActivity && + activity is VectorBaseActivity<*> && + alert.shouldBeDisplayedIn.invoke(activity) } } diff --git a/vector/src/main/java/im/vector/app/features/powerlevel/PowerLevelsObservableFactory.kt b/vector/src/main/java/im/vector/app/features/powerlevel/PowerLevelsFlowFactory.kt similarity index 73% rename from vector/src/main/java/im/vector/app/features/powerlevel/PowerLevelsObservableFactory.kt rename to vector/src/main/java/im/vector/app/features/powerlevel/PowerLevelsFlowFactory.kt index a9d4578bf0..767d6f1ba7 100644 --- a/vector/src/main/java/im/vector/app/features/powerlevel/PowerLevelsObservableFactory.kt +++ b/vector/src/main/java/im/vector/app/features/powerlevel/PowerLevelsFlowFactory.kt @@ -16,23 +16,24 @@ package im.vector.app.features.powerlevel -import io.reactivex.Observable -import io.reactivex.schedulers.Schedulers +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn 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.Room import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent -import org.matrix.android.sdk.rx.mapOptional -import org.matrix.android.sdk.rx.rx -import org.matrix.android.sdk.rx.unwrap +import org.matrix.android.sdk.flow.flow +import org.matrix.android.sdk.flow.mapOptional +import org.matrix.android.sdk.flow.unwrap -class PowerLevelsObservableFactory(private val room: Room) { +class PowerLevelsFlowFactory(private val room: Room) { - fun createObservable(): Observable { - return room.rx() + fun createFlow(): Flow { + return room.flow() .liveStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.NoCondition) - .observeOn(Schedulers.computation()) + .flowOn(Dispatchers.Default) .mapOptional { it.content.toModel() } .unwrap() } diff --git a/vector/src/main/java/im/vector/app/features/qrcode/QrCodeScannerActivity.kt b/vector/src/main/java/im/vector/app/features/qrcode/QrCodeScannerActivity.kt index 13f989994a..7fb2f1f254 100644 --- a/vector/src/main/java/im/vector/app/features/qrcode/QrCodeScannerActivity.kt +++ b/vector/src/main/java/im/vector/app/features/qrcode/QrCodeScannerActivity.kt @@ -23,22 +23,19 @@ import androidx.activity.result.ActivityResultLauncher import com.google.zxing.BarcodeFormat import com.google.zxing.Result import com.google.zxing.ResultMetadataType +import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R -import im.vector.app.core.di.ScreenComponent import im.vector.app.core.extensions.replaceFragment import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.databinding.ActivitySimpleBinding +@AndroidEntryPoint class QrCodeScannerActivity : VectorBaseActivity() { override fun getBinding() = ActivitySimpleBinding.inflate(layoutInflater) override fun getCoordinatorLayout() = views.coordinatorLayout - override fun injectWith(injector: ScreenComponent) { - injector.inject(this) - } - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) if (isFirstCreation()) { diff --git a/vector/src/main/java/im/vector/app/features/qrcode/QrCodeScannerFragment.kt b/vector/src/main/java/im/vector/app/features/qrcode/QrCodeScannerFragment.kt index 1455a85edf..3a3841c026 100644 --- a/vector/src/main/java/im/vector/app/features/qrcode/QrCodeScannerFragment.kt +++ b/vector/src/main/java/im/vector/app/features/qrcode/QrCodeScannerFragment.kt @@ -21,12 +21,11 @@ import android.view.ViewGroup import com.google.zxing.Result import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.FragmentQrCodeScannerBinding - import me.dm7.barcodescanner.zxing.ZXingScannerView import javax.inject.Inject -class QrCodeScannerFragment @Inject constructor() - : VectorBaseFragment(), +class QrCodeScannerFragment @Inject constructor() : + VectorBaseFragment(), ZXingScannerView.ResultHandler { override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentQrCodeScannerBinding { diff --git a/vector/src/main/java/im/vector/app/features/rageshake/BugReportActivity.kt b/vector/src/main/java/im/vector/app/features/rageshake/BugReportActivity.kt index 27adefd50f..b27c2e9818 100755 --- a/vector/src/main/java/im/vector/app/features/rageshake/BugReportActivity.kt +++ b/vector/src/main/java/im/vector/app/features/rageshake/BugReportActivity.kt @@ -25,28 +25,21 @@ import androidx.core.view.isVisible import androidx.core.widget.doOnTextChanged import com.airbnb.mvrx.viewModel import com.airbnb.mvrx.withState +import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R -import im.vector.app.core.di.ScreenComponent import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.databinding.ActivityBugReportBinding import org.matrix.android.sdk.api.extensions.tryOrNull - import timber.log.Timber -import javax.inject.Inject /** * Form to send a bug report */ +@AndroidEntryPoint class BugReportActivity : VectorBaseActivity() { - override fun injectWith(injector: ScreenComponent) { - injector.inject(this) - } - override fun getBinding() = ActivityBugReportBinding.inflate(layoutInflater) - @Inject lateinit var bugReportViewModelFactory: BugReportViewModel.Factory - private val viewModel: BugReportViewModel by viewModel() private var reportType: ReportType = ReportType.BUG_REPORT diff --git a/vector/src/main/java/im/vector/app/features/rageshake/BugReportState.kt b/vector/src/main/java/im/vector/app/features/rageshake/BugReportState.kt index a5019115fb..37a379104b 100644 --- a/vector/src/main/java/im/vector/app/features/rageshake/BugReportState.kt +++ b/vector/src/main/java/im/vector/app/features/rageshake/BugReportState.kt @@ -16,8 +16,8 @@ package im.vector.app.features.rageshake -import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MavericksState data class BugReportState( val serverVersion: String = "" -) : MvRxState +) : MavericksState diff --git a/vector/src/main/java/im/vector/app/features/rageshake/BugReportViewModel.kt b/vector/src/main/java/im/vector/app/features/rageshake/BugReportViewModel.kt index c71a89553e..d0a1280868 100644 --- a/vector/src/main/java/im/vector/app/features/rageshake/BugReportViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/rageshake/BugReportViewModel.kt @@ -16,14 +16,13 @@ package im.vector.app.features.rageshake -import androidx.lifecycle.viewModelScope -import com.airbnb.mvrx.ActivityViewModelContext -import com.airbnb.mvrx.MvRxViewModelFactory -import com.airbnb.mvrx.ViewModelContext +import com.airbnb.mvrx.MavericksViewModelFactory import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import im.vector.app.core.di.ActiveSessionHolder +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.platform.EmptyAction import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel @@ -36,18 +35,11 @@ class BugReportViewModel @AssistedInject constructor( ) : VectorViewModel(initialState) { @AssistedFactory - interface Factory { - fun create(initialState: BugReportState): BugReportViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: BugReportState): BugReportViewModel } - companion object : MvRxViewModelFactory { - - @JvmStatic - override fun create(viewModelContext: ViewModelContext, state: BugReportState): BugReportViewModel? { - val activity: BugReportActivity = (viewModelContext as ActivityViewModelContext).activity() - return activity.bugReportViewModelFactory.create(state) - } - } + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() init { fetchHomeserverVersion() diff --git a/vector/src/main/java/im/vector/app/features/rageshake/BugReporter.kt b/vector/src/main/java/im/vector/app/features/rageshake/BugReporter.kt index 9ae1d37f4d..5b3d194d33 100755 --- a/vector/src/main/java/im/vector/app/features/rageshake/BugReporter.kt +++ b/vector/src/main/java/im/vector/app/features/rageshake/BugReporter.kt @@ -267,8 +267,8 @@ class BugReporter @Inject constructor( .addFormDataPart("device", Build.MODEL.trim()) .addFormDataPart("verbose_log", vectorPreferences.labAllowedExtendedLogging().toOnOff()) .addFormDataPart("multi_window", inMultiWindowMode.toOnOff()) - .addFormDataPart("os", Build.VERSION.RELEASE + " (API " + Build.VERSION.SDK_INT + ") " - + Build.VERSION.INCREMENTAL + "-" + Build.VERSION.CODENAME) + .addFormDataPart("os", Build.VERSION.RELEASE + " (API " + Build.VERSION.SDK_INT + ") " + + Build.VERSION.INCREMENTAL + "-" + Build.VERSION.CODENAME) .addFormDataPart("locale", Locale.getDefault().toString()) .addFormDataPart("app_language", VectorLocale.applicationLocale.toString()) .addFormDataPart("default_app_language", systemLocaleProvider.getSystemLocale().toString()) diff --git a/vector/src/main/java/im/vector/app/features/rageshake/RageShake.kt b/vector/src/main/java/im/vector/app/features/rageshake/RageShake.kt index 48a6e8a679..93c72ea69e 100644 --- a/vector/src/main/java/im/vector/app/features/rageshake/RageShake.kt +++ b/vector/src/main/java/im/vector/app/features/rageshake/RageShake.kt @@ -19,8 +19,8 @@ package im.vector.app.features.rageshake import android.content.Context import android.hardware.Sensor import android.hardware.SensorManager -import androidx.appcompat.app.AppCompatActivity import androidx.core.content.getSystemService +import androidx.fragment.app.FragmentActivity import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.squareup.seismic.ShakeDetector import im.vector.app.R @@ -30,7 +30,7 @@ import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorSettingsActivity import javax.inject.Inject -class RageShake @Inject constructor(private val activity: AppCompatActivity, +class RageShake @Inject constructor(private val activity: FragmentActivity, private val bugReporter: BugReporter, private val navigator: Navigator, private val vectorPreferences: VectorPreferences) : ShakeDetector.Listener { diff --git a/vector/src/main/java/im/vector/app/features/reactions/EmojiChooserFragment.kt b/vector/src/main/java/im/vector/app/features/reactions/EmojiChooserFragment.kt index 51dc62af8b..bcc18a995a 100644 --- a/vector/src/main/java/im/vector/app/features/reactions/EmojiChooserFragment.kt +++ b/vector/src/main/java/im/vector/app/features/reactions/EmojiChooserFragment.kt @@ -23,7 +23,6 @@ import androidx.lifecycle.lifecycleScope import im.vector.app.core.extensions.cleanup import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.EmojiChooserFragmentBinding - import javax.inject.Inject class EmojiChooserFragment @Inject constructor( @@ -41,15 +40,15 @@ class EmojiChooserFragment @Inject constructor( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) viewModel = activityViewModelProvider.get(EmojiChooserViewModel::class.java) - emojiRecyclerAdapter.reactionClickListener = this emojiRecyclerAdapter.interactionListener = this - views.emojiRecyclerView.adapter = emojiRecyclerAdapter - viewModel.moveToSection.observe(viewLifecycleOwner) { section -> emojiRecyclerAdapter.scrollToSection(section) } + viewModel.emojiData.observe(viewLifecycleOwner) { + emojiRecyclerAdapter.update(it) + } } override fun getCoroutineScope() = lifecycleScope diff --git a/vector/src/main/java/im/vector/app/features/reactions/EmojiChooserViewModel.kt b/vector/src/main/java/im/vector/app/features/reactions/EmojiChooserViewModel.kt index df2085e41b..3a4caa296a 100644 --- a/vector/src/main/java/im/vector/app/features/reactions/EmojiChooserViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/reactions/EmojiChooserViewModel.kt @@ -17,11 +17,16 @@ package im.vector.app.features.reactions import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import im.vector.app.core.utils.LiveEvent +import im.vector.app.features.reactions.data.EmojiData +import im.vector.app.features.reactions.data.EmojiDataSource +import kotlinx.coroutines.launch import javax.inject.Inject -class EmojiChooserViewModel @Inject constructor() : ViewModel() { +class EmojiChooserViewModel @Inject constructor(private val emojiDataSource: EmojiDataSource) : ViewModel() { + val emojiData: MutableLiveData = MutableLiveData() val navigateEvent: MutableLiveData> = MutableLiveData() var selectedReaction: String? = null var eventId: String? = null @@ -29,6 +34,17 @@ class EmojiChooserViewModel @Inject constructor() : ViewModel() { val currentSection: MutableLiveData = MutableLiveData() val moveToSection: MutableLiveData = MutableLiveData() + init { + loadEmojiData() + } + + private fun loadEmojiData() { + viewModelScope.launch { + val rawData = emojiDataSource.rawData.await() + emojiData.postValue(rawData) + } + } + fun onReactionSelected(reaction: String) { selectedReaction = reaction navigateEvent.value = LiveEvent(NAVIGATE_FINISH) diff --git a/vector/src/main/java/im/vector/app/features/reactions/EmojiReactionPickerActivity.kt b/vector/src/main/java/im/vector/app/features/reactions/EmojiReactionPickerActivity.kt index ecfaf93747..5675af6960 100644 --- a/vector/src/main/java/im/vector/app/features/reactions/EmojiReactionPickerActivity.kt +++ b/vector/src/main/java/im/vector/app/features/reactions/EmojiReactionPickerActivity.kt @@ -25,18 +25,19 @@ import android.view.MenuInflater import android.view.MenuItem import android.widget.SearchView import androidx.core.view.isVisible +import androidx.lifecycle.lifecycleScope import com.airbnb.mvrx.viewModel import com.google.android.material.tabs.TabLayout import com.jakewharton.rxbinding3.widget.queryTextChanges +import dagger.hilt.android.AndroidEntryPoint import im.vector.app.EmojiCompatFontProvider import im.vector.app.R -import im.vector.app.core.di.ScreenComponent import im.vector.app.core.extensions.observeEvent import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.databinding.ActivityEmojiReactionPickerBinding import im.vector.app.features.reactions.data.EmojiDataSource import io.reactivex.android.schedulers.AndroidSchedulers - +import kotlinx.coroutines.launch import timber.log.Timber import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -46,6 +47,7 @@ import javax.inject.Inject * TODO: Loading indicator while getting emoji data source? * TODO: Finish Refactor to vector base activity */ +@AndroidEntryPoint class EmojiReactionPickerActivity : VectorBaseActivity(), EmojiCompatFontProvider.FontProviderListener { @@ -59,7 +61,6 @@ class EmojiReactionPickerActivity : VectorBaseActivity - val s = category.emojis[0] - views.tabs.newTab() - .also { tab -> - tab.text = emojiDataSource.rawData.emojis[s]!!.emoji - tab.contentDescription = category.name - } - .also { tab -> - views.tabs.addTab(tab) - } + lifecycleScope.launch { + val rawData = emojiDataSource.rawData.await() + rawData.categories.forEach { category -> + val s = category.emojis[0] + views.tabs.newTab() + .also { tab -> + tab.text = rawData.emojis[s]!!.emoji + tab.contentDescription = category.name + } + .also { tab -> + views.tabs.addTab(tab) + } + } } views.tabs.addOnTabSelectedListener(tabLayoutSelectionListener) @@ -161,6 +160,12 @@ class EmojiReactionPickerActivity : VectorBaseActivity Timber.e(err) } @@ -170,6 +175,7 @@ class EmojiReactionPickerActivity : VectorBaseActivity() { var reactionClickListener: ReactionClickListener? = null var interactionListener: InteractionListener? = null + + private var rawData: EmojiData = EmojiData(emptyList(), emptyMap(), emptyMap()) private var mRecyclerView: RecyclerView? = null private var currentFirstVisibleSection = 0 @@ -61,6 +62,12 @@ class EmojiRecyclerAdapter @Inject constructor( UNKNOWN } + @SuppressLint("NotifyDataSetChanged") + fun update(emojiData: EmojiData) { + rawData = emojiData + notifyDataSetChanged() + } + private var scrollState = ScrollState.UNKNOWN private var isFastScroll = false @@ -71,10 +78,10 @@ class EmojiRecyclerAdapter @Inject constructor( if (itemPosition != RecyclerView.NO_POSITION) { val sectionNumber = getSectionForAbsoluteIndex(itemPosition) if (!isSection(itemPosition)) { - val sectionMojis = dataSource.rawData.categories[sectionNumber].emojis + val sectionMojis = rawData.categories[sectionNumber].emojis val sectionOffset = getSectionOffset(sectionNumber) val emoji = sectionMojis[itemPosition - sectionOffset] - val item = dataSource.rawData.emojis.getValue(emoji).emoji + val item = rawData.emojis.getValue(emoji).emoji reactionClickListener?.onReactionSelected(item) } } @@ -115,7 +122,7 @@ class EmojiRecyclerAdapter @Inject constructor( } fun scrollToSection(section: Int) { - if (section < 0 || section >= dataSource.rawData.categories.size) { + if (section < 0 || section >= rawData.categories.size) { // ignore return } @@ -149,7 +156,7 @@ class EmojiRecyclerAdapter @Inject constructor( private fun isSection(position: Int): Boolean { var sectionOffset = 1 var lastItemInSection: Int - dataSource.rawData.categories.forEach { category -> + rawData.categories.forEach { category -> lastItemInSection = sectionOffset + category.emojis.size - 1 if (position == sectionOffset - 1) return true sectionOffset = lastItemInSection + 2 @@ -161,7 +168,7 @@ class EmojiRecyclerAdapter @Inject constructor( var sectionOffset = 1 var lastItemInSection: Int var index = 0 - dataSource.rawData.categories.forEach { category -> + rawData.categories.forEach { category -> lastItemInSection = sectionOffset + category.emojis.size - 1 if (position <= lastItemInSection) return index sectionOffset = lastItemInSection + 2 @@ -174,7 +181,7 @@ class EmojiRecyclerAdapter @Inject constructor( // Todo cache this for fast access var sectionOffset = 1 var lastItemInSection: Int - dataSource.rawData.categories.forEachIndexed { index, category -> + rawData.categories.forEachIndexed { index, category -> lastItemInSection = sectionOffset + category.emojis.size - 1 if (section == index) return sectionOffset sectionOffset = lastItemInSection + 2 @@ -186,12 +193,12 @@ class EmojiRecyclerAdapter @Inject constructor( Trace.beginSection("MyAdapter.onBindViewHolder") val sectionNumber = getSectionForAbsoluteIndex(position) if (isSection(position)) { - holder.bind(dataSource.rawData.categories[sectionNumber].name) + holder.bind(rawData.categories[sectionNumber].name) } else { - val sectionMojis = dataSource.rawData.categories[sectionNumber].emojis + val sectionMojis = rawData.categories[sectionNumber].emojis val sectionOffset = getSectionOffset(sectionNumber) val emoji = sectionMojis[position - sectionOffset] - val item = dataSource.rawData.emojis[emoji]!!.emoji + val item = rawData.emojis[emoji]!!.emoji (holder as EmojiViewHolder).data = item if (scrollState != ScrollState.SETTLING || !isFastScroll) { // Log.i("PERF","Bind with draw at position:$position") @@ -220,7 +227,7 @@ class EmojiRecyclerAdapter @Inject constructor( super.onViewRecycled(holder) } - override fun getItemCount() = dataSource.rawData.categories + override fun getItemCount() = rawData.categories .sumOf { emojiCategory -> 1 /* Section */ + emojiCategory.emojis.size } abstract class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { diff --git a/vector/src/main/java/im/vector/app/features/reactions/EmojiSearchResultFragment.kt b/vector/src/main/java/im/vector/app/features/reactions/EmojiSearchResultFragment.kt index bcd7465887..9292ad8fc6 100644 --- a/vector/src/main/java/im/vector/app/features/reactions/EmojiSearchResultFragment.kt +++ b/vector/src/main/java/im/vector/app/features/reactions/EmojiSearchResultFragment.kt @@ -27,7 +27,6 @@ import im.vector.app.core.extensions.configureWith import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.utils.LiveEvent import im.vector.app.databinding.FragmentGenericRecyclerBinding - import javax.inject.Inject class EmojiSearchResultFragment @Inject constructor( diff --git a/vector/src/main/java/im/vector/app/features/reactions/EmojiSearchResultViewModel.kt b/vector/src/main/java/im/vector/app/features/reactions/EmojiSearchResultViewModel.kt index ac7aee797a..e1e52fdca1 100644 --- a/vector/src/main/java/im/vector/app/features/reactions/EmojiSearchResultViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/reactions/EmojiSearchResultViewModel.kt @@ -15,41 +15,35 @@ */ package im.vector.app.features.reactions -import com.airbnb.mvrx.ActivityViewModelContext -import com.airbnb.mvrx.MvRxState -import com.airbnb.mvrx.MvRxViewModelFactory -import com.airbnb.mvrx.ViewModelContext +import com.airbnb.mvrx.MavericksState +import com.airbnb.mvrx.MavericksViewModelFactory import dagger.assisted.Assisted -import dagger.assisted.AssistedInject import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel import im.vector.app.features.reactions.data.EmojiDataSource import im.vector.app.features.reactions.data.EmojiItem +import kotlinx.coroutines.launch data class EmojiSearchResultViewState( val query: String = "", val results: List = emptyList() -) : MvRxState +) : MavericksState class EmojiSearchResultViewModel @AssistedInject constructor( @Assisted initialState: EmojiSearchResultViewState, - private val dataSource: EmojiDataSource) - : VectorViewModel(initialState) { + private val dataSource: EmojiDataSource) : + VectorViewModel(initialState) { @AssistedFactory - interface Factory { - fun create(initialState: EmojiSearchResultViewState): EmojiSearchResultViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: EmojiSearchResultViewState): EmojiSearchResultViewModel } - companion object : MvRxViewModelFactory { - - @JvmStatic - override fun create(viewModelContext: ViewModelContext, state: EmojiSearchResultViewState): EmojiSearchResultViewModel? { - val activity: EmojiReactionPickerActivity = (viewModelContext as ActivityViewModelContext).activity() - return activity.emojiSearchResultViewModelFactory.create(state) - } - } + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() override fun handle(action: EmojiSearchAction) { when (action) { @@ -58,11 +52,14 @@ class EmojiSearchResultViewModel @AssistedInject constructor( } private fun updateQuery(action: EmojiSearchAction.UpdateQuery) { - setState { - copy( - query = action.queryString, - results = dataSource.filterWith(action.queryString) - ) + viewModelScope.launch { + val results = dataSource.filterWith(action.queryString) + setState { + copy( + query = action.queryString, + results = results + ) + } } } } diff --git a/vector/src/main/java/im/vector/app/features/reactions/data/EmojiDataSource.kt b/vector/src/main/java/im/vector/app/features/reactions/data/EmojiDataSource.kt index 96eda22eb9..7218eb993b 100644 --- a/vector/src/main/java/im/vector/app/features/reactions/data/EmojiDataSource.kt +++ b/vector/src/main/java/im/vector/app/features/reactions/data/EmojiDataSource.kt @@ -20,53 +20,60 @@ import android.graphics.Paint import androidx.core.graphics.PaintCompat import com.squareup.moshi.Moshi import im.vector.app.R +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async import javax.inject.Inject import javax.inject.Singleton @Singleton class EmojiDataSource @Inject constructor( + appScope: CoroutineScope, resources: Resources ) { private val paint = Paint() - val rawData = resources.openRawResource(R.raw.emoji_picker_datasource) - .use { input -> - Moshi.Builder() - .build() - .adapter(EmojiData::class.java) - .fromJson(input.bufferedReader().use { it.readText() }) - } - ?.let { parsedRawData -> - // Add key as a keyword, it will solve the issue that ":tada" is not available in completion - // Only add emojis to emojis/categories that can be rendered by the system - parsedRawData.copy( - emojis = mutableMapOf().apply { - parsedRawData.emojis.keys.forEach { key -> - val origin = parsedRawData.emojis[key] ?: return@forEach + val rawData = appScope.async(Dispatchers.IO, CoroutineStart.LAZY) { + resources.openRawResource(R.raw.emoji_picker_datasource) + .use { input -> + Moshi.Builder() + .build() + .adapter(EmojiData::class.java) + .fromJson(input.bufferedReader().use { it.readText() }) + } + ?.let { parsedRawData -> + // Add key as a keyword, it will solve the issue that ":tada" is not available in completion + // Only add emojis to emojis/categories that can be rendered by the system + parsedRawData.copy( + emojis = mutableMapOf().apply { + parsedRawData.emojis.keys.forEach { key -> + val origin = parsedRawData.emojis[key] ?: return@forEach - // Do not add keys containing '_' - if (isEmojiRenderable(origin.emoji)) { - if (origin.keywords.contains(key) || key.contains("_")) { - put(key, origin) - } else { - put(key, origin.copy(keywords = origin.keywords + key)) - } - } - } - }, - categories = mutableListOf().apply { - parsedRawData.categories.forEach { entry -> - add(EmojiCategory(entry.id, entry.name, mutableListOf().apply { - entry.emojis.forEach { e -> - if (isEmojiRenderable(parsedRawData.emojis[e]!!.emoji)) { - add(e) + // Do not add keys containing '_' + if (isEmojiRenderable(origin.emoji)) { + if (origin.keywords.contains(key) || key.contains("_")) { + put(key, origin) + } else { + put(key, origin.copy(keywords = origin.keywords + key)) } } - })) + } + }, + categories = mutableListOf().apply { + parsedRawData.categories.forEach { entry -> + add(EmojiCategory(entry.id, entry.name, mutableListOf().apply { + entry.emojis.forEach { e -> + if (isEmojiRenderable(parsedRawData.emojis[e]!!.emoji)) { + add(e) + } + } + })) + } } - } - ) - } - ?: EmojiData(emptyList(), emptyMap(), emptyMap()) + ) + } + ?: EmojiData(emptyList(), emptyMap(), emptyMap()) + } private val quickReactions = mutableListOf() @@ -74,9 +81,9 @@ class EmojiDataSource @Inject constructor( return PaintCompat.hasGlyph(paint, emoji) } - fun filterWith(query: String): List { + suspend fun filterWith(query: String): List { val words = query.split("\\s".toRegex()) - + val rawData = this.rawData.await() // First add emojis with name matching query, sorted by name return (rawData.emojis.values .asSequence() @@ -87,9 +94,9 @@ class EmojiDataSource @Inject constructor( // Then emojis with keyword matching any of the word in the query, sorted by name rawData.emojis.values .filter { emojiItem -> - words.fold(true, { prev, word -> + words.fold(true) { prev, word -> prev && emojiItem.keywords.any { keyword -> keyword.contains(word, true) } - }) + } } .sortedBy { it.name }) // and ensure they will not be present twice @@ -97,7 +104,7 @@ class EmojiDataSource @Inject constructor( .toList() } - fun getQuickReactions(): List { + suspend fun getQuickReactions(): List { if (quickReactions.isEmpty()) { listOf( "thumbs-up", // 👍 @@ -109,7 +116,7 @@ class EmojiDataSource @Inject constructor( "rocket", // 🚀 "eyes" // 👀 ) - .mapNotNullTo(quickReactions) { rawData.emojis[it] } + .mapNotNullTo(quickReactions) { rawData.await().emojis[it] } } return quickReactions diff --git a/vector/src/main/java/im/vector/app/features/reactions/widget/ReactionButton.kt b/vector/src/main/java/im/vector/app/features/reactions/widget/ReactionButton.kt index 0f6f1620a8..2b4e9ee5ab 100644 --- a/vector/src/main/java/im/vector/app/features/reactions/widget/ReactionButton.kt +++ b/vector/src/main/java/im/vector/app/features/reactions/widget/ReactionButton.kt @@ -23,9 +23,9 @@ import android.view.View import android.widget.LinearLayout import androidx.core.content.ContextCompat import androidx.core.content.withStyledAttributes +import dagger.hilt.android.AndroidEntryPoint import im.vector.app.EmojiCompatWrapper import im.vector.app.R -import im.vector.app.core.di.HasScreenInjector import im.vector.app.core.utils.DimensionConverter import im.vector.app.core.utils.TextUtils import im.vector.app.databinding.ReactionButtonBinding @@ -35,16 +35,11 @@ import javax.inject.Inject * An animated reaction button. * Displays a String reaction (emoji), with a count, and that can be selected or not (toggle) */ +@AndroidEntryPoint class ReactionButton @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, - defStyleAttr: Int = 0) - : LinearLayout(context, attrs, defStyleAttr), View.OnClickListener, View.OnLongClickListener { - - init { - if (context is HasScreenInjector) { - context.injector().inject(this) - } - } + defStyleAttr: Int = 0) : + LinearLayout(context, attrs, defStyleAttr), View.OnClickListener, View.OnLongClickListener { @Inject lateinit var emojiCompatWrapper: EmojiCompatWrapper diff --git a/vector/src/main/java/im/vector/app/features/room/RequireActiveMembershipViewModel.kt b/vector/src/main/java/im/vector/app/features/room/RequireActiveMembershipViewModel.kt index c8b0037311..6ad93abe0c 100644 --- a/vector/src/main/java/im/vector/app/features/room/RequireActiveMembershipViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/room/RequireActiveMembershipViewModel.kt @@ -16,20 +16,24 @@ package im.vector.app.features.room -import com.airbnb.mvrx.ActivityViewModelContext -import com.airbnb.mvrx.FragmentViewModelContext -import com.airbnb.mvrx.MvRxViewModelFactory -import com.airbnb.mvrx.ViewModelContext -import com.jakewharton.rxrelay2.BehaviorRelay +import com.airbnb.mvrx.MavericksViewModelFactory import dagger.assisted.Assisted -import dagger.assisted.AssistedInject import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import im.vector.app.R +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider -import io.reactivex.Observable -import io.reactivex.schedulers.Schedulers +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.events.model.EventType @@ -37,8 +41,8 @@ import org.matrix.android.sdk.api.session.room.Room import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.util.Optional -import org.matrix.android.sdk.rx.rx -import org.matrix.android.sdk.rx.unwrap +import org.matrix.android.sdk.flow.flow +import org.matrix.android.sdk.flow.unwrap /** * This ViewModel observe a room summary and notify when the room is left @@ -46,48 +50,41 @@ import org.matrix.android.sdk.rx.unwrap class RequireActiveMembershipViewModel @AssistedInject constructor( @Assisted initialState: RequireActiveMembershipViewState, private val stringProvider: StringProvider, - private val session: Session) - : VectorViewModel(initialState) { + private val session: Session) : + VectorViewModel(initialState) { @AssistedFactory - interface Factory { - fun create(initialState: RequireActiveMembershipViewState): RequireActiveMembershipViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: RequireActiveMembershipViewState): RequireActiveMembershipViewModel } - companion object : MvRxViewModelFactory { + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() - @JvmStatic - override fun create(viewModelContext: ViewModelContext, state: RequireActiveMembershipViewState): RequireActiveMembershipViewModel? { - 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") - } - } - - private val roomIdObservable = BehaviorRelay.createDefault(Optional.from(initialState.roomId)) + private val roomIdFlow = MutableStateFlow(Optional.from(initialState.roomId)) init { observeRoomSummary() } private fun observeRoomSummary() { - roomIdObservable + roomIdFlow .unwrap() - .switchMap { roomId -> - val room = session.getRoom(roomId) ?: return@switchMap Observable.just(Optional.empty()) - room.rx() + .flatMapLatest { roomId -> + val room = session.getRoom(roomId) ?: return@flatMapLatest flow { + val emptyResult = Optional.empty() + emit(emptyResult) + } + room.flow() .liveRoomSummary() .unwrap() - .observeOn(Schedulers.computation()) + .flowOn(Dispatchers.Default) .map { mapToLeftViewEvent(room, it) } } .unwrap() - .subscribe { event -> + .onEach { event -> _viewEvents.post(event) } - .disposeOnClear() + .launchIn(viewModelScope) } private fun mapToLeftViewEvent(room: Room, roomSummary: RoomSummary): Optional { @@ -128,7 +125,7 @@ class RequireActiveMembershipViewModel @AssistedInject constructor( setState { copy(roomId = action.roomId) } - roomIdObservable.accept(Optional.from(action.roomId)) + roomIdFlow.tryEmit(Optional.from(action.roomId)) } }.exhaustive } diff --git a/vector/src/main/java/im/vector/app/features/room/RequireActiveMembershipViewState.kt b/vector/src/main/java/im/vector/app/features/room/RequireActiveMembershipViewState.kt index 2ca68b5e8b..7a5363100f 100644 --- a/vector/src/main/java/im/vector/app/features/room/RequireActiveMembershipViewState.kt +++ b/vector/src/main/java/im/vector/app/features/room/RequireActiveMembershipViewState.kt @@ -16,15 +16,16 @@ package im.vector.app.features.room -import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MavericksState +import im.vector.app.features.home.room.detail.RoomDetailArgs import im.vector.app.features.roommemberprofile.RoomMemberProfileArgs import im.vector.app.features.roomprofile.RoomProfileArgs data class RequireActiveMembershipViewState( val roomId: String? = null -) : MvRxState { +) : MavericksState { - // No constructor for RoomDetailArgs because of intent for Shortcut + constructor(args: RoomDetailArgs) : this(roomId = args.roomId) constructor(args: RoomProfileArgs) : this(roomId = args.roomId) diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/ExplicitTermFilter.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/ExplicitTermFilter.kt index 0d1f55485c..e184e77557 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/ExplicitTermFilter.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/ExplicitTermFilter.kt @@ -39,8 +39,8 @@ class ExplicitTermFilter @Inject constructor( } fun isValid(str: String): Boolean { - return explicitContentRegex.matches(str.replace("\n", " ")).not() + return explicitContentRegex.matches(str.replace("\n", " ")).not() && // Special treatment for "18+" since word boundaries does not work here - && str.contains("18+").not() + str.contains("18+").not() } } diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/PublicRoomsController.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/PublicRoomsController.kt index 32c87bffd4..a350bff0bb 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/PublicRoomsController.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/PublicRoomsController.kt @@ -61,8 +61,8 @@ class PublicRoomsController @Inject constructor(private val stringProvider: Stri unknownRoomItem?.addTo(this) - if ((viewState.hasMore && viewState.asyncPublicRoomsRequest is Success) - || viewState.asyncPublicRoomsRequest is Incomplete) { + if ((viewState.hasMore && viewState.asyncPublicRoomsRequest is Success) || + viewState.asyncPublicRoomsRequest is Incomplete) { loadingItem { // Change id to avoid list to scroll automatically when first results are displayed if (publicRooms.isEmpty()) { diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/PublicRoomsFragment.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/PublicRoomsFragment.kt index 8214b26fea..b61583df55 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/PublicRoomsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/PublicRoomsFragment.kt @@ -22,6 +22,7 @@ import android.view.LayoutInflater import android.view.MenuItem import android.view.View import android.view.ViewGroup +import androidx.lifecycle.lifecycleScope import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.withState import com.jakewharton.rxbinding3.appcompat.queryTextChanges @@ -37,7 +38,7 @@ import im.vector.app.databinding.FragmentPublicRoomsBinding import im.vector.app.features.permalink.NavigationInterceptor import im.vector.app.features.permalink.PermalinkHandler import io.reactivex.rxkotlin.subscribeBy - +import kotlinx.coroutines.launch import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoom import timber.log.Timber @@ -125,20 +126,20 @@ class PublicRoomsFragment @Inject constructor( } override fun onUnknownRoomClicked(roomIdOrAlias: String) { - val permalink = session.permalinkService().createPermalink(roomIdOrAlias) - permalinkHandler - .launch(requireContext(), permalink, object : NavigationInterceptor { - override fun navToRoom(roomId: String?, eventId: String?, deepLink: Uri?): Boolean { - requireActivity().finish() - return false - } - }) - .subscribe { isSuccessful -> - if (!isSuccessful) { - requireContext().toast(R.string.room_error_not_found) - } - } - .disposeOnDestroyView() + viewLifecycleOwner.lifecycleScope.launch { + val permalink = session.permalinkService().createPermalink(roomIdOrAlias) + val isHandled = permalinkHandler + .launch(requireContext(), permalink, object : NavigationInterceptor { + override fun navToRoom(roomId: String?, eventId: String?, deepLink: Uri?): Boolean { + requireActivity().finish() + return false + } + }) + + if (!isHandled) { + requireContext().toast(R.string.room_error_not_found) + } + } } override fun onPublicRoomClicked(publicRoom: PublicRoom, joinState: JoinState) { diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/PublicRoomsViewState.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/PublicRoomsViewState.kt index fdab72caba..3b9995a13d 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/PublicRoomsViewState.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/PublicRoomsViewState.kt @@ -17,7 +17,7 @@ package im.vector.app.features.roomdirectory import com.airbnb.mvrx.Async -import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.Uninitialized import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoom @@ -36,4 +36,4 @@ data class PublicRoomsViewState( // keys are room alias or roomId val changeMembershipStates: Map = emptyMap(), val roomDirectoryData: RoomDirectoryData = RoomDirectoryData() -) : MvRxState +) : MavericksState diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryActivity.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryActivity.kt index 9a63e81a2f..7ad8d0ce49 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryActivity.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryActivity.kt @@ -21,19 +21,22 @@ import android.content.Intent import android.os.Bundle import com.airbnb.mvrx.viewModel import com.airbnb.mvrx.withState +import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R -import im.vector.app.core.di.ScreenComponent import im.vector.app.core.extensions.addFragment import im.vector.app.core.extensions.addFragmentToBackstack import im.vector.app.core.extensions.popBackstack import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.databinding.ActivitySimpleBinding -import im.vector.app.features.roomdirectory.createroom.CreateRoomFragment +import im.vector.app.features.matrixto.MatrixToBottomSheet +import im.vector.app.features.navigation.Navigator import im.vector.app.features.roomdirectory.createroom.CreateRoomArgs +import im.vector.app.features.roomdirectory.createroom.CreateRoomFragment import im.vector.app.features.roomdirectory.picker.RoomDirectoryPickerFragment import javax.inject.Inject -class RoomDirectoryActivity : VectorBaseActivity() { +@AndroidEntryPoint +class RoomDirectoryActivity : VectorBaseActivity(), MatrixToBottomSheet.InteractionListener { @Inject lateinit var roomDirectoryViewModelFactory: RoomDirectoryViewModel.Factory private val roomDirectoryViewModel: RoomDirectoryViewModel by viewModel() @@ -43,10 +46,6 @@ class RoomDirectoryActivity : VectorBaseActivity() { override fun getCoordinatorLayout() = views.coordinatorLayout - override fun injectWith(injector: ScreenComponent) { - injector.inject(this) - } - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) sharedActionViewModel = viewModelProvider.get(RoomDirectorySharedActionViewModel::class.java) @@ -84,6 +83,14 @@ class RoomDirectoryActivity : VectorBaseActivity() { } } + override fun mxToBottomSheetNavigateToRoom(roomId: String) { + navigator.openRoom(this, roomId) + } + + override fun mxToBottomSheetSwitchToSpace(spaceId: String) { + navigator.switchToSpace(this, spaceId, Navigator.PostSwitchSpaceAction.None) + } + companion object { private const val INITIAL_FILTER = "INITIAL_FILTER" diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryViewModel.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryViewModel.kt index dc1cbfc58d..844266e668 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryViewModel.kt @@ -16,21 +16,21 @@ package im.vector.app.features.roomdirectory -import androidx.lifecycle.viewModelScope -import com.airbnb.mvrx.ActivityViewModelContext import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Loading -import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.Success -import com.airbnb.mvrx.ViewModelContext import com.airbnb.mvrx.appendAt import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.platform.VectorViewModel import im.vector.app.features.settings.VectorPreferences import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.Session @@ -38,7 +38,7 @@ import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsFilter import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsParams import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams -import org.matrix.android.sdk.rx.rx +import org.matrix.android.sdk.flow.flow import timber.log.Timber class RoomDirectoryViewModel @AssistedInject constructor( @@ -49,18 +49,12 @@ class RoomDirectoryViewModel @AssistedInject constructor( ) : VectorViewModel(initialState) { @AssistedFactory - interface Factory { - fun create(initialState: PublicRoomsViewState): RoomDirectoryViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: PublicRoomsViewState): RoomDirectoryViewModel } - companion object : MvRxViewModelFactory { + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() { private const val PUBLIC_ROOMS_LIMIT = 20 - - @JvmStatic - override fun create(viewModelContext: ViewModelContext, state: PublicRoomsViewState): RoomDirectoryViewModel? { - val activity: RoomDirectoryActivity = (viewModelContext as ActivityViewModelContext).activity() - return activity.roomDirectoryViewModelFactory.create(state) - } } private val showAllRooms = vectorPreferences.showAllPublicRooms() @@ -80,28 +74,24 @@ class RoomDirectoryViewModel @AssistedInject constructor( memberships = listOf(Membership.JOIN) } session - .rx() + .flow() .liveRoomSummaries(queryParams) - .subscribe { list -> - val joinedRoomIds = list - ?.map { it.roomId } - ?.toSet() - .orEmpty() - - setState { - copy(joinedRoomsIds = joinedRoomIds) - } + .map { roomSummaries -> + roomSummaries + .map { it.roomId } + .toSet() + } + .setOnEach { + copy(joinedRoomsIds = it) } - .disposeOnClear() } private fun observeMembershipChanges() { - session.rx() + session.flow() .liveRoomChangeMembershipState() - .subscribe { - setState { copy(changeMembershipStates = it) } + .setOnEach { + copy(changeMembershipStates = it) } - .disposeOnClear() } override fun handle(action: RoomDirectoryAction) { diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomActivity.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomActivity.kt index 4afda8a0e9..eeb7d217c0 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomActivity.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomActivity.kt @@ -20,8 +20,8 @@ import android.content.Context import android.content.Intent import android.os.Bundle import com.google.android.material.appbar.MaterialToolbar +import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R -import im.vector.app.core.di.ScreenComponent import im.vector.app.core.extensions.addFragment import im.vector.app.core.platform.ToolbarConfigurable import im.vector.app.core.platform.VectorBaseActivity @@ -32,6 +32,7 @@ import im.vector.app.features.roomdirectory.RoomDirectorySharedActionViewModel /** * Simple container for [CreateRoomFragment] */ +@AndroidEntryPoint class CreateRoomActivity : VectorBaseActivity(), ToolbarConfigurable { private lateinit var sharedActionViewModel: RoomDirectorySharedActionViewModel @@ -44,10 +45,6 @@ class CreateRoomActivity : VectorBaseActivity(), ToolbarC configureToolbar(toolbar) } - override fun injectWith(injector: ScreenComponent) { - injector.inject(this) - } - override fun initUiAndData() { if (isFirstCreation()) { addFragment( diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomController.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomController.kt index 2676096b6b..7dd9cb4e00 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomController.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomController.kt @@ -29,6 +29,8 @@ import im.vector.app.features.form.formEditTextItem import im.vector.app.features.form.formEditableAvatarItem import im.vector.app.features.form.formSubmitButtonItem import im.vector.app.features.form.formSwitchItem +import org.matrix.android.sdk.api.MatrixConstants +import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.room.failure.CreateRoomFailure import org.matrix.android.sdk.api.session.room.model.RoomJoinRules import javax.inject.Inject @@ -140,6 +142,7 @@ class CreateRoomController @Inject constructor( value(viewState.aliasLocalPart) suffixText(":" + viewState.homeServerName) prefixText("#") + maxLength(MatrixConstants.maxAliasLocalPartLength(viewState.homeServerName)) hint(host.stringProvider.getString(R.string.room_alias_address_hint)) errorMessage( host.roomAliasErrorFormatter.format( @@ -165,7 +168,8 @@ class CreateRoomController @Inject constructor( host.stringProvider.getString(R.string.create_room_encryption_description) } ) - switchChecked(viewState.isEncrypted) + + switchChecked(viewState.isEncrypted ?: viewState.defaultEncrypted[viewState.roomJoinRules].orFalse()) listener { value -> host.listener?.setIsEncrypted(value) diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomFragment.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomFragment.kt index 70f041bd69..c61da211a4 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomFragment.kt @@ -59,7 +59,6 @@ data class CreateRoomArgs( class CreateRoomFragment @Inject constructor( private val createRoomController: CreateRoomController, private val createSpaceController: CreateSubSpaceController, - val createRoomViewModelFactory: CreateRoomViewModel.Factory, colorProvider: ColorProvider ) : VectorBaseFragment(), CreateRoomController.Listener, @@ -163,8 +162,9 @@ class CreateRoomFragment @Inject constructor( } override fun selectVisibility() = withState(viewModel) { state -> - - val allowed = if (state.supportsRestricted) { + // If restricted is supported and the user is in the context of a parent space + // then show restricted option. + val allowed = if (state.supportsRestricted && state.parentSpaceId != null) { listOf(RoomJoinRules.INVITE, RoomJoinRules.PUBLIC, RoomJoinRules.RESTRICTED) } else { listOf(RoomJoinRules.INVITE, RoomJoinRules.PUBLIC) diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewModel.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewModel.kt index 9a9812933b..e0ffdc7a52 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewModel.kt @@ -17,17 +17,16 @@ package im.vector.app.features.roomdirectory.createroom import androidx.core.net.toFile -import androidx.lifecycle.viewModelScope import com.airbnb.mvrx.Fail -import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.Loading -import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized -import com.airbnb.mvrx.ViewModelContext import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import im.vector.app.features.raw.wellknown.getElementWellknown @@ -36,6 +35,7 @@ import im.vector.app.features.settings.VectorPreferences import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.matrix.android.sdk.api.MatrixPatterns.getDomain +import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.raw.RawService import org.matrix.android.sdk.api.session.Session @@ -55,14 +55,16 @@ import timber.log.Timber class CreateRoomViewModel @AssistedInject constructor(@Assisted private val initialState: CreateRoomViewState, private val session: Session, private val rawService: RawService, - private val vectorPreferences: VectorPreferences + vectorPreferences: VectorPreferences ) : VectorViewModel(initialState) { @AssistedFactory - interface Factory { - fun create(initialState: CreateRoomViewState): CreateRoomViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: CreateRoomViewState): CreateRoomViewModel } + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() + init { initHomeServerName() initAdminE2eByDefault() @@ -109,23 +111,18 @@ class CreateRoomViewModel @AssistedInject constructor(@Assisted private val init setState { copy( - isEncrypted = RoomJoinRules.INVITE == roomJoinRules && adminE2EByDefault, - hsAdminHasDisabledE2E = !adminE2EByDefault + hsAdminHasDisabledE2E = !adminE2EByDefault, + defaultEncrypted = mapOf( + RoomJoinRules.INVITE to adminE2EByDefault, + RoomJoinRules.PUBLIC to false, + RoomJoinRules.RESTRICTED to adminE2EByDefault + ) + ) } } } - companion object : MvRxViewModelFactory { - - @JvmStatic - override fun create(viewModelContext: ViewModelContext, state: CreateRoomViewState): CreateRoomViewModel? { - val fragment: CreateRoomFragment = (viewModelContext as FragmentViewModelContext).fragment() - - return fragment.createRoomViewModelFactory.create(state) - } - } - override fun handle(action: CreateRoomAction) { when (action) { is CreateRoomAction.SetAvatar -> setAvatar(action) @@ -286,7 +283,12 @@ class CreateRoomViewModel @AssistedInject constructor(@Assisted private val init disableFederation = state.disableFederation // Encryption - if (state.isEncrypted) { + val shouldEncrypt = when (state.roomJoinRules) { + // we ignore the isEncrypted for public room as the switch is hidden in this case + RoomJoinRules.PUBLIC -> false + else -> state.isEncrypted ?: state.defaultEncrypted[state.roomJoinRules].orFalse() + } + if (shouldEncrypt) { enableEncryption() } } diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewState.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewState.kt index db56a19904..389d365875 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewState.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewState.kt @@ -18,7 +18,7 @@ package im.vector.app.features.roomdirectory.createroom import android.net.Uri import com.airbnb.mvrx.Async -import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.Uninitialized import org.matrix.android.sdk.api.session.room.model.RoomJoinRules import org.matrix.android.sdk.api.session.room.model.RoomSummary @@ -28,7 +28,8 @@ data class CreateRoomViewState( val roomName: String = "", val roomTopic: String = "", val roomJoinRules: RoomJoinRules = RoomJoinRules.INVITE, - val isEncrypted: Boolean = false, + val isEncrypted: Boolean? = null, + val defaultEncrypted: Map = emptyMap(), val showAdvanced: Boolean = false, val disableFederation: Boolean = false, val homeServerName: String = "", @@ -39,7 +40,7 @@ data class CreateRoomViewState( val supportsRestricted: Boolean = false, val aliasLocalPart: String? = null, val isSubSpace: Boolean = false -) : MvRxState { +) : MavericksState { constructor(args: CreateRoomArgs) : this( roomName = args.initialName, @@ -50,8 +51,8 @@ data class CreateRoomViewState( /** * Return true if there is not important input from user */ - fun isEmpty() = avatarUri == null - && roomName.isEmpty() - && roomTopic.isEmpty() - && aliasLocalPart.isNullOrEmpty() + fun isEmpty() = avatarUri == null && + roomName.isEmpty() && + roomTopic.isEmpty() && + aliasLocalPart.isNullOrEmpty() } diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateSubSpaceController.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateSubSpaceController.kt index 6d292c85da..26ea2f30a3 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateSubSpaceController.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateSubSpaceController.kt @@ -28,6 +28,7 @@ import im.vector.app.features.form.formEditTextItem import im.vector.app.features.form.formEditableSquareAvatarItem import im.vector.app.features.form.formMultiLineEditTextItem import im.vector.app.features.form.formSubmitButtonItem +import org.matrix.android.sdk.api.MatrixConstants import org.matrix.android.sdk.api.session.room.failure.CreateRoomFailure import org.matrix.android.sdk.api.session.room.model.RoomJoinRules import javax.inject.Inject @@ -81,6 +82,7 @@ class CreateSubSpaceController @Inject constructor( hint(host.stringProvider.getString(R.string.create_space_alias_hint)) suffixText(":" + data.homeServerName) prefixText("#") + maxLength(MatrixConstants.maxAliasLocalPartLength(data.homeServerName)) errorMessage( host.roomAliasErrorFormatter.format( (((data.asyncCreateRoomRequest as? Fail)?.error) as? CreateRoomFailure.AliasError)?.aliasError) diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerController.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerController.kt index d8db38609c..08e044630d 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerController.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerController.kt @@ -160,8 +160,8 @@ class RoomDirectoryPickerController @Inject constructor( } private fun getErrorMessage(error: Throwable): String { - return if (error is Failure.ServerError - && error.httpCode == HttpsURLConnection.HTTP_INTERNAL_ERROR /* 500 */) { + return if (error is Failure.ServerError && + error.httpCode == HttpsURLConnection.HTTP_INTERNAL_ERROR /* 500 */) { stringProvider.getString(R.string.directory_add_a_new_server_error) } else { errorFormatter.toHumanReadable(error) diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerFragment.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerFragment.kt index a32a3a897f..2707b87c1f 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerFragment.kt @@ -39,8 +39,7 @@ import im.vector.app.features.roomdirectory.RoomDirectoryViewModel import timber.log.Timber import javax.inject.Inject -class RoomDirectoryPickerFragment @Inject constructor(val roomDirectoryPickerViewModelFactory: RoomDirectoryPickerViewModel.Factory, - private val roomDirectoryPickerController: RoomDirectoryPickerController +class RoomDirectoryPickerFragment @Inject constructor(private val roomDirectoryPickerController: RoomDirectoryPickerController ) : VectorBaseFragment(), OnBackPressed, RoomDirectoryPickerController.Callback { diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerViewModel.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerViewModel.kt index 2558715834..a5673e78a2 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerViewModel.kt @@ -16,18 +16,17 @@ package im.vector.app.features.roomdirectory.picker -import androidx.lifecycle.viewModelScope import com.airbnb.mvrx.Fail -import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.Loading -import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized -import com.airbnb.mvrx.ViewModelContext import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import im.vector.app.R +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel @@ -46,18 +45,11 @@ class RoomDirectoryPickerViewModel @AssistedInject constructor( ) : VectorViewModel(initialState) { @AssistedFactory - interface Factory { - fun create(initialState: RoomDirectoryPickerViewState): RoomDirectoryPickerViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: RoomDirectoryPickerViewState): RoomDirectoryPickerViewModel } - companion object : MvRxViewModelFactory { - - @JvmStatic - override fun create(viewModelContext: ViewModelContext, state: RoomDirectoryPickerViewState): RoomDirectoryPickerViewModel? { - val fragment: RoomDirectoryPickerFragment = (viewModelContext as FragmentViewModelContext).fragment() - return fragment.roomDirectoryPickerViewModelFactory.create(state) - } - } + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() init { observeAndCompute() @@ -66,7 +58,7 @@ class RoomDirectoryPickerViewModel @AssistedInject constructor( } private fun observeAndCompute() { - selectSubscribe( + onEach( RoomDirectoryPickerViewState::asyncThirdPartyRequest, RoomDirectoryPickerViewState::customHomeservers ) { async, custom -> diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerViewState.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerViewState.kt index 5cdee862ab..56aa807e41 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerViewState.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerViewState.kt @@ -17,7 +17,7 @@ package im.vector.app.features.roomdirectory.picker import com.airbnb.mvrx.Async -import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.Uninitialized import im.vector.app.features.roomdirectory.RoomDirectoryServer import org.matrix.android.sdk.api.session.room.model.thirdparty.ThirdPartyProtocol @@ -30,4 +30,4 @@ data class RoomDirectoryPickerViewState( val addServerAsync: Async = Uninitialized, // computed val directories: List = emptyList() -) : MvRxState +) : MavericksState diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewActivity.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewActivity.kt index 0121d5d795..86bfdb79cd 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewActivity.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewActivity.kt @@ -20,6 +20,7 @@ import android.content.Context import android.content.Intent import android.os.Parcelable import com.google.android.material.appbar.MaterialToolbar +import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R import im.vector.app.core.extensions.addFragment import im.vector.app.core.platform.ToolbarConfigurable @@ -51,6 +52,7 @@ data class RoomPreviewData( get() = MatrixItem.RoomItem(roomId, roomName ?: roomAlias, avatarUrl) } +@AndroidEntryPoint class RoomPreviewActivity : VectorBaseActivity(), ToolbarConfigurable { companion object { diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewNoPreviewFragment.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewNoPreviewFragment.kt index ef70a31a00..52617e2f1d 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewNoPreviewFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewNoPreviewFragment.kt @@ -51,7 +51,6 @@ import javax.inject.Inject * Note: this Fragment is also used for world readable room for the moment */ class RoomPreviewNoPreviewFragment @Inject constructor( - val roomPreviewViewModelFactory: RoomPreviewViewModel.Factory, private val avatarRenderer: AvatarRenderer ) : VectorBaseFragment() { diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewViewModel.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewViewModel.kt index a559b0554d..7b012f4fac 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewViewModel.kt @@ -16,20 +16,21 @@ package im.vector.app.features.roomdirectory.roompreview -import androidx.lifecycle.viewModelScope -import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.Loading -import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.Success -import com.airbnb.mvrx.ViewModelContext import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel import im.vector.app.features.roomdirectory.JoinState import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.query.QueryStringValue @@ -40,26 +41,19 @@ import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.peeking.PeekResult import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams -import org.matrix.android.sdk.rx.rx +import org.matrix.android.sdk.flow.flow import timber.log.Timber class RoomPreviewViewModel @AssistedInject constructor(@Assisted private val initialState: RoomPreviewViewState, - private val session: Session) - : VectorViewModel(initialState) { + private val session: Session) : + VectorViewModel(initialState) { @AssistedFactory - interface Factory { - fun create(initialState: RoomPreviewViewState): RoomPreviewViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: RoomPreviewViewState): RoomPreviewViewModel } - companion object : MvRxViewModelFactory { - - @JvmStatic - override fun create(viewModelContext: ViewModelContext, state: RoomPreviewViewState): RoomPreviewViewModel? { - val fragment: RoomPreviewNoPreviewFragment = (viewModelContext as FragmentViewModelContext).fragment() - return fragment.roomPreviewViewModelFactory.create(state) - } - } + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() init { // Observe joined room (from the sync) @@ -165,9 +159,9 @@ class RoomPreviewViewModel @AssistedInject constructor(@Assisted private val ini excludeType = null } session - .rx() + .flow() .liveRoomSummaries(queryParams) - .subscribe { list -> + .onEach { list -> val isRoomJoined = list.any { it.membership == Membership.JOIN } @@ -180,13 +174,13 @@ class RoomPreviewViewModel @AssistedInject constructor(@Assisted private val ini setState { copy(roomJoinState = JoinState.JOINED) } } } - .disposeOnClear() + .launchIn(viewModelScope) } private fun observeMembershipChanges() { - session.rx() + session.flow() .liveRoomChangeMembershipState() - .subscribe { + .onEach { val changeMembership = it[initialState.roomId] ?: ChangeMembershipState.Unknown val joinState = when (changeMembership) { is ChangeMembershipState.Joining -> JoinState.JOINING @@ -198,7 +192,7 @@ class RoomPreviewViewModel @AssistedInject constructor(@Assisted private val ini setState { copy(roomJoinState = joinState) } } } - .disposeOnClear() + .launchIn(viewModelScope) } override fun handle(action: RoomPreviewAction) { diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewViewState.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewViewState.kt index 75286ea24f..8488dd7267 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewViewState.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewViewState.kt @@ -17,7 +17,7 @@ package im.vector.app.features.roomdirectory.roompreview import com.airbnb.mvrx.Async -import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.Uninitialized import im.vector.app.features.roomdirectory.JoinState import org.matrix.android.sdk.api.session.permalinks.PermalinkData @@ -48,7 +48,7 @@ data class RoomPreviewViewState( val fromEmailInvite: PermalinkData.RoomEmailInviteLink? = null, // used only if it's an email invite val isEmailBoundToAccount: Boolean = false -) : MvRxState { +) : MavericksState { constructor(args: RoomPreviewData) : this( roomId = args.roomId, @@ -62,7 +62,7 @@ data class RoomPreviewViewState( roomType = args.roomType ) - fun matrixItem() : MatrixItem { + fun matrixItem(): MatrixItem { return if (roomType == RoomType.SPACE) MatrixItem.SpaceItem(roomId, roomName ?: roomAlias, avatarUrl) else MatrixItem.RoomItem(roomId, roomName ?: roomAlias, avatarUrl) } diff --git a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileActivity.kt b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileActivity.kt index 37ad2741ca..c563f6b855 100644 --- a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileActivity.kt +++ b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileActivity.kt @@ -20,53 +20,40 @@ package im.vector.app.features.roommemberprofile import android.content.Context import android.content.Intent import android.widget.Toast -import com.google.android.material.appbar.MaterialToolbar -import com.airbnb.mvrx.MvRx +import com.airbnb.mvrx.Mavericks import com.airbnb.mvrx.viewModel +import com.google.android.material.appbar.MaterialToolbar +import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R -import im.vector.app.core.di.ScreenComponent import im.vector.app.core.extensions.addFragment import im.vector.app.core.platform.ToolbarConfigurable import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.databinding.ActivitySimpleBinding import im.vector.app.features.room.RequireActiveMembershipViewEvents import im.vector.app.features.room.RequireActiveMembershipViewModel -import im.vector.app.features.room.RequireActiveMembershipViewState -import javax.inject.Inject +@AndroidEntryPoint class RoomMemberProfileActivity : VectorBaseActivity(), - ToolbarConfigurable, - RequireActiveMembershipViewModel.Factory { + ToolbarConfigurable { companion object { fun newIntent(context: Context, args: RoomMemberProfileArgs): Intent { return Intent(context, RoomMemberProfileActivity::class.java).apply { - putExtra(MvRx.KEY_ARG, args) + putExtra(Mavericks.KEY_ARG, args) } } } private val requireActiveMembershipViewModel: RequireActiveMembershipViewModel by viewModel() - @Inject - lateinit var requireActiveMembershipViewModelFactory: RequireActiveMembershipViewModel.Factory - - override fun create(initialState: RequireActiveMembershipViewState): RequireActiveMembershipViewModel { - return requireActiveMembershipViewModelFactory.create(initialState) - } - - override fun injectWith(injector: ScreenComponent) { - injector.inject(this) - } - override fun getBinding(): ActivitySimpleBinding { return ActivitySimpleBinding.inflate(layoutInflater) } override fun initUiAndData() { if (isFirstCreation()) { - val fragmentArgs: RoomMemberProfileArgs = intent?.extras?.getParcelable(MvRx.KEY_ARG) ?: return + val fragmentArgs: RoomMemberProfileArgs = intent?.extras?.getParcelable(Mavericks.KEY_ARG) ?: return addFragment(R.id.simpleFragmentContainer, RoomMemberProfileFragment::class.java, fragmentArgs) } diff --git a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileFragment.kt b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileFragment.kt index d8b4f249da..48823714f5 100644 --- a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileFragment.kt @@ -47,6 +47,7 @@ import im.vector.app.databinding.DialogShareQrCodeBinding import im.vector.app.databinding.FragmentMatrixProfileBinding import im.vector.app.databinding.ViewStubRoomMemberProfileHeaderBinding import im.vector.app.features.crypto.verification.VerificationBottomSheet +import im.vector.app.features.displayname.getBestName import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.detail.RoomDetailPendingAction import im.vector.app.features.home.room.detail.RoomDetailPendingActionStore @@ -65,7 +66,6 @@ data class RoomMemberProfileArgs( ) : Parcelable class RoomMemberProfileFragment @Inject constructor( - val viewModelFactory: RoomMemberProfileViewModel.Factory, private val roomMemberProfileController: RoomMemberProfileController, private val avatarRenderer: AvatarRenderer, private val roomDetailPendingActionStore: RoomDetailPendingActionStore diff --git a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileViewModel.kt b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileViewModel.kt index 4b57bdc6aa..5b07b101e7 100644 --- a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileViewModel.kt @@ -17,25 +17,27 @@ package im.vector.app.features.roommemberprofile -import androidx.lifecycle.viewModelScope import com.airbnb.mvrx.Fail -import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.Loading -import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized -import com.airbnb.mvrx.ViewModelContext import dagger.assisted.Assisted -import dagger.assisted.AssistedInject import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import im.vector.app.R +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory 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.powerlevel.PowerLevelsObservableFactory -import io.reactivex.Observable -import io.reactivex.functions.BiFunction +import im.vector.app.features.displayname.getBestName +import im.vector.app.features.powerlevel.PowerLevelsFlowFactory import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.matrix.android.sdk.api.query.QueryStringValue @@ -46,35 +48,26 @@ import org.matrix.android.sdk.api.session.profile.ProfileService import org.matrix.android.sdk.api.session.room.Room import org.matrix.android.sdk.api.session.room.members.roomMemberQueryParams 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.RoomSummary import org.matrix.android.sdk.api.session.room.model.RoomType import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper import org.matrix.android.sdk.api.session.room.powerlevels.Role import org.matrix.android.sdk.api.util.MatrixItem import org.matrix.android.sdk.api.util.toMatrixItem import org.matrix.android.sdk.api.util.toOptional -import org.matrix.android.sdk.rx.rx -import org.matrix.android.sdk.rx.unwrap +import org.matrix.android.sdk.flow.flow +import org.matrix.android.sdk.flow.unwrap class RoomMemberProfileViewModel @AssistedInject constructor(@Assisted private val initialState: RoomMemberProfileViewState, private val stringProvider: StringProvider, - private val session: Session) - : VectorViewModel(initialState) { + private val session: Session) : + VectorViewModel(initialState) { @AssistedFactory - interface Factory { - fun create(initialState: RoomMemberProfileViewState): RoomMemberProfileViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: RoomMemberProfileViewState): RoomMemberProfileViewModel } - companion object : MvRxViewModelFactory { - - @JvmStatic - override fun create(viewModelContext: ViewModelContext, state: RoomMemberProfileViewState): RoomMemberProfileViewModel? { - val fragment: RoomMemberProfileFragment = (viewModelContext as FragmentViewModelContext).fragment() - return fragment.viewModelFactory.create(state) - } - } + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() private val room = if (initialState.roomId != null) { session.getRoom(initialState.roomId) @@ -108,7 +101,7 @@ class RoomMemberProfileViewModel @AssistedInject constructor(@Assisted private v } } - session.rx().liveUserCryptoDevices(initialState.userId) + session.flow().liveUserCryptoDevices(initialState.userId) .map { Pair( it.fold(true, { prev, dev -> prev && dev.isVerified }), @@ -122,14 +115,14 @@ class RoomMemberProfileViewModel @AssistedInject constructor(@Assisted private v ) } - session.rx().liveCrossSigningInfo(initialState.userId) + session.flow().liveCrossSigningInfo(initialState.userId) .execute { copy(userMXCrossSigningInfo = it.invoke()?.getOrNull()) } } private fun observeIgnoredState() { - session.rx().liveIgnoredUsers() + session.flow().liveIgnoredUsers() .map { ignored -> ignored.find { it.userId == initialState.userId @@ -246,7 +239,7 @@ class RoomMemberProfileViewModel @AssistedInject constructor(@Assisted private v val queryParams = roomMemberQueryParams { this.userId = QueryStringValue.Equals(initialState.userId, QueryStringValue.Case.SENSITIVE) } - room.rx().liveRoomMembers(queryParams) + room.flow().liveRoomMembers(queryParams) .map { it.firstOrNull().toOptional() } .unwrap() .execute { @@ -286,11 +279,11 @@ class RoomMemberProfileViewModel @AssistedInject constructor(@Assisted private v } private fun observeRoomSummaryAndPowerLevels(room: Room) { - val roomSummaryLive = room.rx().liveRoomSummary().unwrap() - val powerLevelsContentLive = PowerLevelsObservableFactory(room).createObservable() + val roomSummaryLive = room.flow().liveRoomSummary().unwrap() + val powerLevelsContentLive = PowerLevelsFlowFactory(room).createFlow() powerLevelsContentLive - .subscribe { + .onEach { val powerLevelsHelper = PowerLevelsHelper(it) val permissions = ActionPermissions( canKick = powerLevelsHelper.isUserAbleToKick(session.myUserId), @@ -298,30 +291,26 @@ class RoomMemberProfileViewModel @AssistedInject constructor(@Assisted private v canInvite = powerLevelsHelper.isUserAbleToInvite(session.myUserId), canEditPowerLevel = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_POWER_LEVELS) ) - setState { copy(powerLevelsContent = it, actionPermissions = permissions) } - } - .disposeOnClear() + setState { + copy(powerLevelsContent = it, actionPermissions = permissions) + } + }.launchIn(viewModelScope) roomSummaryLive.execute { copy(isRoomEncrypted = it.invoke()?.isEncrypted == true) } - Observable - .combineLatest( - roomSummaryLive, - powerLevelsContentLive, - BiFunction { roomSummary, powerLevelsContent -> - val roomName = roomSummary.toMatrixItem().getBestName() - val powerLevelsHelper = PowerLevelsHelper(powerLevelsContent) - when (val userPowerLevel = powerLevelsHelper.getUserRole(initialState.userId)) { - Role.Admin -> stringProvider.getString(R.string.room_member_power_level_admin_in, roomName) - Role.Moderator -> stringProvider.getString(R.string.room_member_power_level_moderator_in, roomName) - Role.Default -> stringProvider.getString(R.string.room_member_power_level_default_in, roomName) - is Role.Custom -> stringProvider.getString(R.string.room_member_power_level_custom_in, userPowerLevel.value, roomName) - } - } - ).execute { - copy(userPowerLevelString = it) - } + roomSummaryLive.combine(powerLevelsContentLive) { roomSummary, powerLevelsContent -> + val roomName = roomSummary.toMatrixItem().getBestName() + val powerLevelsHelper = PowerLevelsHelper(powerLevelsContent) + when (val userPowerLevel = powerLevelsHelper.getUserRole(initialState.userId)) { + Role.Admin -> stringProvider.getString(R.string.room_member_power_level_admin_in, roomName) + Role.Moderator -> stringProvider.getString(R.string.room_member_power_level_moderator_in, roomName) + Role.Default -> stringProvider.getString(R.string.room_member_power_level_default_in, roomName) + is Role.Custom -> stringProvider.getString(R.string.room_member_power_level_custom_in, userPowerLevel.value, roomName) + } + }.execute { + copy(userPowerLevelString = it) + } } private fun handleIgnoreAction() = withState { state -> diff --git a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileViewState.kt b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileViewState.kt index 5c2751f0dc..a4730153c2 100644 --- a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileViewState.kt +++ b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileViewState.kt @@ -18,7 +18,7 @@ package im.vector.app.features.roommemberprofile import com.airbnb.mvrx.Async -import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.Uninitialized import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo import org.matrix.android.sdk.api.session.room.model.Membership @@ -42,7 +42,7 @@ data class RoomMemberProfileViewState( val asyncMembership: Async = Uninitialized, val hasReadReceipt: Boolean = false, val actionPermissions: ActionPermissions = ActionPermissions() -) : MvRxState { +) : MavericksState { constructor(args: RoomMemberProfileArgs) : this(userId = args.userId, roomId = args.roomId) } diff --git a/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceListBottomSheet.kt b/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceListBottomSheet.kt index f0cdb1cdd1..f83ac8f19d 100644 --- a/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceListBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceListBottomSheet.kt @@ -24,20 +24,20 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment -import com.airbnb.mvrx.MvRx +import com.airbnb.mvrx.Mavericks import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState +import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R -import im.vector.app.core.di.ScreenComponent import im.vector.app.core.extensions.commitTransaction import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment import im.vector.app.databinding.BottomSheetWithFragmentsBinding import im.vector.app.features.crypto.verification.VerificationBottomSheet import kotlinx.parcelize.Parcelize -import javax.inject.Inject import kotlin.reflect.KClass +@AndroidEntryPoint class DeviceListBottomSheet : VectorBaseBottomSheetDialogFragment() { @@ -47,12 +47,6 @@ class DeviceListBottomSheet : private val viewModel: DeviceListBottomSheetViewModel by fragmentViewModel(DeviceListBottomSheetViewModel::class) - @Inject lateinit var viewModelFactory: DeviceListBottomSheetViewModel.Factory - - override fun injectWith(injector: ScreenComponent) { - injector.inject(this) - } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) viewModel.observeViewEvents { @@ -122,7 +116,7 @@ class DeviceListBottomSheet : companion object { fun newInstance(userId: String, allowDeviceAction: Boolean = true): DeviceListBottomSheet { val args = Bundle() - args.putParcelable(MvRx.KEY_ARG, Args(userId, allowDeviceAction)) + args.putParcelable(Mavericks.KEY_ARG, Args(userId, allowDeviceAction)) return DeviceListBottomSheet().apply { arguments = args } } } diff --git a/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceListBottomSheetViewModel.kt b/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceListBottomSheetViewModel.kt index 7d31a5c811..d2491237ca 100644 --- a/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceListBottomSheetViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceListBottomSheetViewModel.kt @@ -17,15 +17,17 @@ package im.vector.app.features.roommemberprofile.devices import com.airbnb.mvrx.Async -import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.Loading -import com.airbnb.mvrx.MvRxState -import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.MavericksState +import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.ViewModelContext import dagger.assisted.Assisted -import dagger.assisted.AssistedInject import dagger.assisted.AssistedFactory -import im.vector.app.core.di.HasScreenInjector +import dagger.assisted.AssistedInject +import dagger.hilt.EntryPoints +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.SingletonEntryPoint +import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import org.matrix.android.sdk.api.session.Session @@ -33,36 +35,55 @@ import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod import org.matrix.android.sdk.api.util.MatrixItem import org.matrix.android.sdk.api.util.toMatrixItem +import org.matrix.android.sdk.flow.flow import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo -import org.matrix.android.sdk.rx.rx data class DeviceListViewState( + val userId: String, + val allowDeviceAction: Boolean, val userItem: MatrixItem? = null, val isMine: Boolean = false, val memberCrossSigningKey: MXCrossSigningInfo? = null, val cryptoDevices: Async> = Loading(), val selectedDevice: CryptoDeviceInfo? = null -) : MvRxState +) : MavericksState class DeviceListBottomSheetViewModel @AssistedInject constructor(@Assisted private val initialState: DeviceListViewState, - @Assisted private val args: DeviceListBottomSheet.Args, - private val session: Session) - : VectorViewModel(initialState) { + private val session: Session) : + VectorViewModel(initialState) { @AssistedFactory - interface Factory { - fun create(initialState: DeviceListViewState, args: DeviceListBottomSheet.Args): DeviceListBottomSheetViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: DeviceListViewState): DeviceListBottomSheetViewModel + } + + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() { + + override fun initialState(viewModelContext: ViewModelContext): DeviceListViewState? { + val args = viewModelContext.args() + val userId = args.userId + val session = EntryPoints.get(viewModelContext.app(), SingletonEntryPoint::class.java).activeSessionHolder().getActiveSession() + return session.getUser(userId)?.toMatrixItem()?.let { + DeviceListViewState( + userId = userId, + allowDeviceAction = args.allowDeviceAction, + userItem = it, + isMine = userId == session.myUserId + ) + } ?: return super.initialState(viewModelContext) + } } init { - session.rx().liveUserCryptoDevices(args.userId) + + session.flow().liveUserCryptoDevices(initialState.userId) .execute { copy(cryptoDevices = it).also { refreshSelectedId() } } - session.rx().liveCrossSigningInfo(args.userId) + session.flow().liveCrossSigningInfo(initialState.userId) .execute { copy(memberCrossSigningKey = it.invoke()?.getOrNull()) } @@ -89,7 +110,7 @@ class DeviceListBottomSheetViewModel @AssistedInject constructor(@Assisted priva } private fun selectDevice(action: DeviceListAction.SelectDevice) { - if (!args.allowDeviceAction) return + if (!initialState.allowDeviceAction) return setState { copy(selectedDevice = action.device) } @@ -102,29 +123,9 @@ class DeviceListBottomSheetViewModel @AssistedInject constructor(@Assisted priva } private fun manuallyVerify(action: DeviceListAction.ManuallyVerify) { - if (!args.allowDeviceAction) return - session.cryptoService().verificationService().beginKeyVerification(VerificationMethod.SAS, args.userId, action.deviceId, null)?.let { txID -> - _viewEvents.post(DeviceListBottomSheetViewEvents.Verify(args.userId, txID)) - } - } - - companion object : MvRxViewModelFactory { - @JvmStatic - override fun create(viewModelContext: ViewModelContext, state: DeviceListViewState): DeviceListBottomSheetViewModel? { - val fragment: DeviceListBottomSheet = (viewModelContext as FragmentViewModelContext).fragment() - val args = viewModelContext.args() - return fragment.viewModelFactory.create(state, args) - } - - override fun initialState(viewModelContext: ViewModelContext): DeviceListViewState? { - val userId = viewModelContext.args().userId - val session = (viewModelContext.activity as HasScreenInjector).injector().activeSessionHolder().getActiveSession() - return session.getUser(userId)?.toMatrixItem()?.let { - DeviceListViewState( - userItem = it, - isMine = userId == session.myUserId - ) - } ?: return super.initialState(viewModelContext) + if (!initialState.allowDeviceAction) return + session.cryptoService().verificationService().beginKeyVerification(VerificationMethod.SAS, initialState.userId, action.deviceId, null)?.let { txID -> + _viewEvents.post(DeviceListBottomSheetViewEvents.Verify(initialState.userId, txID)) } } } diff --git a/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceListEpoxyController.kt b/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceListEpoxyController.kt index ca2141ccd2..0325cb132e 100644 --- a/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceListEpoxyController.kt +++ b/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceListEpoxyController.kt @@ -39,8 +39,8 @@ import javax.inject.Inject class DeviceListEpoxyController @Inject constructor(private val stringProvider: StringProvider, private val colorProvider: ColorProvider, private val dimensionConverter: DimensionConverter, - private val vectorPreferences: VectorPreferences) - : TypedEpoxyController() { + private val vectorPreferences: VectorPreferences) : + TypedEpoxyController() { interface InteractionListener { fun onDeviceSelected(device: CryptoDeviceInfo) diff --git a/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceListFragment.kt b/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceListFragment.kt index fd770ca530..dbf1f19595 100644 --- a/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceListFragment.kt @@ -27,7 +27,6 @@ import im.vector.app.core.extensions.configureWith import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.utils.DimensionConverter import im.vector.app.databinding.BottomSheetGenericListBinding - import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo import javax.inject.Inject diff --git a/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceTrustInfoActionFragment.kt b/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceTrustInfoActionFragment.kt index 65dfa75b65..f82ecb6ddf 100644 --- a/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceTrustInfoActionFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceTrustInfoActionFragment.kt @@ -27,7 +27,6 @@ import im.vector.app.core.extensions.configureWith import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.utils.DimensionConverter import im.vector.app.databinding.BottomSheetGenericListBinding - import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo import javax.inject.Inject diff --git a/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceTrustInfoEpoxyController.kt b/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceTrustInfoEpoxyController.kt index b8711372af..bce219a711 100644 --- a/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceTrustInfoEpoxyController.kt +++ b/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceTrustInfoEpoxyController.kt @@ -34,8 +34,8 @@ import javax.inject.Inject class DeviceTrustInfoEpoxyController @Inject constructor(private val stringProvider: StringProvider, private val colorProvider: ColorProvider, private val dimensionConverter: DimensionConverter, - private val vectorPreferences: VectorPreferences) - : TypedEpoxyController() { + private val vectorPreferences: VectorPreferences) : + TypedEpoxyController() { interface InteractionListener { fun onVerifyManually(device: CryptoDeviceInfo) diff --git a/vector/src/main/java/im/vector/app/features/roommemberprofile/powerlevel/EditPowerLevelDialogs.kt b/vector/src/main/java/im/vector/app/features/roommemberprofile/powerlevel/EditPowerLevelDialogs.kt index 9cd4bb87bd..e6b898c2b9 100644 --- a/vector/src/main/java/im/vector/app/features/roommemberprofile/powerlevel/EditPowerLevelDialogs.kt +++ b/vector/src/main/java/im/vector/app/features/roommemberprofile/powerlevel/EditPowerLevelDialogs.kt @@ -25,7 +25,6 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder import im.vector.app.R import im.vector.app.core.extensions.hideKeyboard import im.vector.app.databinding.DialogEditPowerLevelBinding - import org.matrix.android.sdk.api.session.room.powerlevels.Role object EditPowerLevelDialogs { diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileActivity.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileActivity.kt index d28878283f..fdb639e7d6 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileActivity.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileActivity.kt @@ -20,11 +20,11 @@ package im.vector.app.features.roomprofile import android.content.Context import android.content.Intent import android.widget.Toast -import com.google.android.material.appbar.MaterialToolbar -import com.airbnb.mvrx.MvRx +import com.airbnb.mvrx.Mavericks import com.airbnb.mvrx.viewModel +import com.google.android.material.appbar.MaterialToolbar +import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R -import im.vector.app.core.di.ScreenComponent import im.vector.app.core.extensions.addFragment import im.vector.app.core.extensions.addFragmentToBackstack import im.vector.app.core.extensions.exhaustive @@ -34,20 +34,19 @@ import im.vector.app.databinding.ActivitySimpleBinding import im.vector.app.features.home.room.detail.RoomDetailPendingActionStore import im.vector.app.features.room.RequireActiveMembershipViewEvents import im.vector.app.features.room.RequireActiveMembershipViewModel -import im.vector.app.features.room.RequireActiveMembershipViewState +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.settings.RoomSettingsFragment -import im.vector.app.features.roomprofile.alias.RoomAliasFragment 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.uploads.RoomUploadsFragment import javax.inject.Inject +@AndroidEntryPoint class RoomProfileActivity : VectorBaseActivity(), - ToolbarConfigurable, - RequireActiveMembershipViewModel.Factory { + ToolbarConfigurable { companion object { @@ -60,7 +59,7 @@ class RoomProfileActivity : fun newIntent(context: Context, roomId: String, directAccess: Int?): Intent { val roomProfileArgs = RoomProfileArgs(roomId) return Intent(context, RoomProfileActivity::class.java).apply { - putExtra(MvRx.KEY_ARG, roomProfileArgs) + putExtra(Mavericks.KEY_ARG, roomProfileArgs) putExtra(EXTRA_DIRECT_ACCESS, directAccess) } } @@ -71,27 +70,16 @@ class RoomProfileActivity : private val requireActiveMembershipViewModel: RequireActiveMembershipViewModel by viewModel() - @Inject - lateinit var requireActiveMembershipViewModelFactory: RequireActiveMembershipViewModel.Factory - @Inject lateinit var roomDetailPendingActionStore: RoomDetailPendingActionStore - override fun create(initialState: RequireActiveMembershipViewState): RequireActiveMembershipViewModel { - return requireActiveMembershipViewModelFactory.create(initialState) - } - - override fun injectWith(injector: ScreenComponent) { - injector.inject(this) - } - override fun getBinding(): ActivitySimpleBinding { return ActivitySimpleBinding.inflate(layoutInflater) } override fun initUiAndData() { sharedActionViewModel = viewModelProvider.get(RoomProfileSharedActionViewModel::class.java) - roomProfileArgs = intent?.extras?.getParcelable(MvRx.KEY_ARG) ?: return + roomProfileArgs = intent?.extras?.getParcelable(Mavericks.KEY_ARG) ?: return if (isFirstCreation()) { when (intent?.extras?.getInt(EXTRA_DIRECT_ACCESS, EXTRA_DIRECT_ACCESS_ROOM_ROOT)) { EXTRA_DIRECT_ACCESS_ROOM_SETTINGS -> { diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileController.kt index be026894b6..b237faa17d 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileController.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileController.kt @@ -94,11 +94,11 @@ class RoomProfileController @Inject constructor( // Upgrade warning val roomVersion = data.roomCreateContent()?.roomVersion - if (data.canUpgradeRoom - && !data.isTombstoned - && roomVersion != null - && data.isUsingUnstableRoomVersion - && data.recommendedRoomVersion != null) { + if (data.canUpgradeRoom && + !data.isTombstoned && + roomVersion != null && + data.isUsingUnstableRoomVersion && + data.recommendedRoomVersion != null) { genericFooterItem { id("version_warning") text(host.stringProvider.getString(R.string.room_using_unstable_room_version, roomVersion)) diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt index 4b37d038b5..23234f8bbd 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt @@ -67,7 +67,6 @@ class RoomProfileFragment @Inject constructor( private val roomProfileController: RoomProfileController, private val avatarRenderer: AvatarRenderer, private val roomDetailPendingActionStore: RoomDetailPendingActionStore, - val roomProfileViewModelFactory: RoomProfileViewModel.Factory ) : VectorBaseFragment(), RoomProfileController.Callback { @@ -219,6 +218,8 @@ class RoomProfileFragment @Inject constructor( avatarRenderer.render(matrixItem, views.matrixProfileToolbarAvatarImageView) headerViews.roomProfileDecorationImageView.render(it.roomEncryptionTrustLevel) views.matrixProfileDecorationToolbarAvatarImageView.render(it.roomEncryptionTrustLevel) + headerViews.roomProfilePresenceImageView.render(it.isDirect, it.directUserPresence) + headerViews.roomProfilePublicImageView.isVisible = it.isPublic && !it.isDirect } } roomProfileController.setData(state) diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewModel.kt index d35e8f3ad5..472ddfc6b9 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewModel.kt @@ -17,19 +17,18 @@ package im.vector.app.features.roomprofile -import androidx.lifecycle.viewModelScope -import com.airbnb.mvrx.FragmentViewModelContext -import com.airbnb.mvrx.MvRxViewModelFactory -import com.airbnb.mvrx.ViewModelContext +import com.airbnb.mvrx.MavericksViewModelFactory import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import im.vector.app.R +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider import im.vector.app.features.home.ShortcutCreator -import im.vector.app.features.powerlevel.PowerLevelsObservableFactory +import im.vector.app.features.powerlevel.PowerLevelsFlowFactory import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.matrix.android.sdk.api.query.QueryStringValue @@ -41,10 +40,10 @@ import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper import org.matrix.android.sdk.api.session.room.state.isPublic -import org.matrix.android.sdk.rx.RxRoom -import org.matrix.android.sdk.rx.mapOptional -import org.matrix.android.sdk.rx.rx -import org.matrix.android.sdk.rx.unwrap +import org.matrix.android.sdk.flow.FlowRoom +import org.matrix.android.sdk.flow.flow +import org.matrix.android.sdk.flow.mapOptional +import org.matrix.android.sdk.flow.unwrap class RoomProfileViewModel @AssistedInject constructor( @Assisted private val initialState: RoomProfileViewState, @@ -54,31 +53,24 @@ class RoomProfileViewModel @AssistedInject constructor( ) : VectorViewModel(initialState) { @AssistedFactory - interface Factory { - fun create(initialState: RoomProfileViewState): RoomProfileViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: RoomProfileViewState): RoomProfileViewModel } - companion object : MvRxViewModelFactory { - - @JvmStatic - override fun create(viewModelContext: ViewModelContext, state: RoomProfileViewState): RoomProfileViewModel? { - val fragment: RoomProfileFragment = (viewModelContext as FragmentViewModelContext).fragment() - return fragment.roomProfileViewModelFactory.create(state) - } - } + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() private val room = session.getRoom(initialState.roomId)!! init { - val rxRoom = room.rx() - observeRoomSummary(rxRoom) - observeRoomCreateContent(rxRoom) - observeBannedRoomMembers(rxRoom) + val flowRoom = room.flow() + observeRoomSummary(flowRoom) + observeRoomCreateContent(flowRoom) + observeBannedRoomMembers(flowRoom) observePermissions() } - private fun observeRoomCreateContent(rxRoom: RxRoom) { - rxRoom.liveStateEvent(EventType.STATE_ROOM_CREATE, QueryStringValue.NoCondition) + private fun observeRoomCreateContent(flowRoom: FlowRoom) { + flowRoom.liveStateEvent(EventType.STATE_ROOM_CREATE, QueryStringValue.NoCondition) .mapOptional { it.content.toModel() } .unwrap() .execute { async -> @@ -93,32 +85,31 @@ class RoomProfileViewModel @AssistedInject constructor( } } - private fun observeRoomSummary(rxRoom: RxRoom) { - rxRoom.liveRoomSummary() + private fun observeRoomSummary(flowRoom: FlowRoom) { + flowRoom.liveRoomSummary() .unwrap() .execute { copy(roomSummary = it) } } - private fun observeBannedRoomMembers(rxRoom: RxRoom) { - rxRoom.liveRoomMembers(roomMemberQueryParams { memberships = listOf(Membership.BAN) }) + private fun observeBannedRoomMembers(flowRoom: FlowRoom) { + flowRoom.liveRoomMembers(roomMemberQueryParams { memberships = listOf(Membership.BAN) }) .execute { copy(bannedMembership = it) } } private fun observePermissions() { - PowerLevelsObservableFactory(room) - .createObservable() - .subscribe { + PowerLevelsFlowFactory(room) + .createFlow() + .setOnEach { val powerLevelsHelper = PowerLevelsHelper(it) val permissions = RoomProfileViewState.ActionPermissions( canEnableEncryption = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_ENCRYPTION) ) - setState { copy(actionPermissions = permissions) } + copy(actionPermissions = permissions) } - .disposeOnClear() } override fun handle(action: RoomProfileAction) { diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewState.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewState.kt index 999b6540bd..14b415c53a 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewState.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewState.kt @@ -18,7 +18,7 @@ package im.vector.app.features.roomprofile import com.airbnb.mvrx.Async -import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.Uninitialized import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary @@ -35,7 +35,7 @@ data class RoomProfileViewState( val recommendedRoomVersion: String? = null, val canUpgradeRoom: Boolean = false, val isTombstoned: Boolean = false -) : MvRxState { +) : MavericksState { constructor(args: RoomProfileArgs) : this(roomId = args.roomId) diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasController.kt index a14bb61606..a1c252d356 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasController.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasController.kt @@ -36,6 +36,7 @@ import im.vector.app.features.discovery.settingsInfoItem import im.vector.app.features.form.formEditTextItem import im.vector.app.features.form.formSwitchItem import im.vector.app.features.roomdirectory.createroom.RoomAliasErrorFormatter +import org.matrix.android.sdk.api.MatrixConstants import org.matrix.android.sdk.api.session.room.alias.RoomAliasError import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility import org.matrix.android.sdk.api.session.room.model.RoomType @@ -174,6 +175,7 @@ class RoomAliasController @Inject constructor( formEditTextItem { id("publishManuallyEdit") value(data.publishManuallyState.value) + maxLength(MatrixConstants.ALIAS_MAX_LENGTH) hint(host.stringProvider.getString(R.string.room_alias_address_hint)) inputType(InputType.TYPE_CLASS_TEXT) onTextChange { text -> @@ -253,6 +255,7 @@ class RoomAliasController @Inject constructor( value(data.newLocalAliasState.value) suffixText(":" + data.homeServerName) prefixText("#") + maxLength(MatrixConstants.maxAliasLocalPartLength(data.homeServerName)) hint(host.stringProvider.getString(R.string.room_alias_address_hint)) errorMessage(host.roomAliasErrorFormatter.format((data.newLocalAliasState.asyncRequest as? Fail)?.error as? RoomAliasError)) onTextChange { value -> diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasFragment.kt index 36dbf7bf8c..e281c0f84d 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasFragment.kt @@ -38,14 +38,12 @@ import im.vector.app.features.roomprofile.RoomProfileArgs import im.vector.app.features.roomprofile.alias.detail.RoomAliasBottomSheet import im.vector.app.features.roomprofile.alias.detail.RoomAliasBottomSheetSharedAction import im.vector.app.features.roomprofile.alias.detail.RoomAliasBottomSheetSharedActionViewModel - import org.matrix.android.sdk.api.session.room.alias.RoomAliasError import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility import org.matrix.android.sdk.api.util.toMatrixItem import javax.inject.Inject class RoomAliasFragment @Inject constructor( - val viewModelFactory: RoomAliasViewModel.Factory, private val controller: RoomAliasController, private val avatarRenderer: AvatarRenderer ) : diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasViewModel.kt index aa9981997c..19f600e5de 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasViewModel.kt @@ -16,20 +16,21 @@ package im.vector.app.features.roomprofile.alias -import androidx.lifecycle.viewModelScope import com.airbnb.mvrx.Fail -import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.Loading -import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized -import com.airbnb.mvrx.ViewModelContext import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel -import im.vector.app.features.powerlevel.PowerLevelsObservableFactory +import im.vector.app.features.powerlevel.PowerLevelsFlowFactory +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import org.matrix.android.sdk.api.MatrixPatterns.getDomain import org.matrix.android.sdk.api.query.QueryStringValue @@ -38,27 +39,20 @@ 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.RoomCanonicalAliasContent import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper -import org.matrix.android.sdk.rx.mapOptional -import org.matrix.android.sdk.rx.rx -import org.matrix.android.sdk.rx.unwrap +import org.matrix.android.sdk.flow.flow +import org.matrix.android.sdk.flow.mapOptional +import org.matrix.android.sdk.flow.unwrap class RoomAliasViewModel @AssistedInject constructor(@Assisted initialState: RoomAliasViewState, - private val session: Session) - : VectorViewModel(initialState) { + private val session: Session) : + VectorViewModel(initialState) { @AssistedFactory - interface Factory { - fun create(initialState: RoomAliasViewState): RoomAliasViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: RoomAliasViewState): RoomAliasViewModel } - companion object : MvRxViewModelFactory { - - @JvmStatic - override fun create(viewModelContext: ViewModelContext, state: RoomAliasViewState): RoomAliasViewModel? { - val fragment: RoomAliasFragment = (viewModelContext as FragmentViewModelContext).fragment() - return fragment.viewModelFactory.create(state) - } - } + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() private val room = session.getRoom(initialState.roomId)!! @@ -128,7 +122,7 @@ class RoomAliasViewModel @AssistedInject constructor(@Assisted initialState: Roo } private fun observeRoomSummary() { - room.rx().liveRoomSummary() + room.flow().liveRoomSummary() .unwrap() .execute { async -> copy( @@ -138,9 +132,9 @@ class RoomAliasViewModel @AssistedInject constructor(@Assisted initialState: Roo } private fun observePowerLevel() { - PowerLevelsObservableFactory(room) - .createObservable() - .subscribe { + PowerLevelsFlowFactory(room) + .createFlow() + .onEach { val powerLevelsHelper = PowerLevelsHelper(it) val permissions = RoomAliasViewState.ActionPermissions( canChangeCanonicalAlias = powerLevelsHelper.isUserAllowedToSend( @@ -163,27 +157,23 @@ class RoomAliasViewModel @AssistedInject constructor(@Assisted initialState: Roo publishManuallyState = newPublishManuallyState ) } - } - .disposeOnClear() + }.launchIn(viewModelScope) } /** * We do not want to use the fallback avatar url, which can be the other user avatar, or the current user avatar. */ private fun observeRoomCanonicalAlias() { - room.rx() + room.flow() .liveStateEvent(EventType.STATE_ROOM_CANONICAL_ALIAS, QueryStringValue.NoCondition) .mapOptional { it.content.toModel() } .unwrap() - .subscribe { - setState { - copy( - canonicalAlias = it.canonicalAlias, - alternativeAliases = it.alternativeAliases.orEmpty().sorted() - ) - } + .setOnEach { + copy( + canonicalAlias = it.canonicalAlias, + alternativeAliases = it.alternativeAliases.orEmpty().sorted() + ) } - .disposeOnClear() } override fun handle(action: RoomAliasAction) { diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasViewState.kt b/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasViewState.kt index f6341f4f64..aabdb7530f 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasViewState.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasViewState.kt @@ -17,7 +17,7 @@ package im.vector.app.features.roomprofile.alias import com.airbnb.mvrx.Async -import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.Uninitialized import im.vector.app.features.roomprofile.RoomProfileArgs import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility @@ -35,7 +35,7 @@ data class RoomAliasViewState( val publishManuallyState: AddAliasState = AddAliasState.Hidden, val localAliases: Async> = Uninitialized, val newLocalAliasState: AddAliasState = AddAliasState.Closed -) : MvRxState { +) : MavericksState { constructor(args: RoomProfileArgs) : this(roomId = args.roomId) diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/alias/detail/RoomAliasBottomSheet.kt b/vector/src/main/java/im/vector/app/features/roomprofile/alias/detail/RoomAliasBottomSheet.kt index e65efd4936..56dbcbfba4 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/alias/detail/RoomAliasBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/alias/detail/RoomAliasBottomSheet.kt @@ -24,7 +24,7 @@ import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState -import im.vector.app.core.di.ScreenComponent +import dagger.hilt.android.AndroidEntryPoint import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.configureWith import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment @@ -44,23 +44,19 @@ data class RoomAliasBottomSheetArgs( /** * Bottom sheet fragment that shows room alias information with list of contextual actions */ +@AndroidEntryPoint class RoomAliasBottomSheet : VectorBaseBottomSheetDialogFragment(), RoomAliasBottomSheetController.Listener { private lateinit var sharedActionViewModel: RoomAliasBottomSheetSharedActionViewModel @Inject lateinit var sharedViewPool: RecyclerView.RecycledViewPool - @Inject lateinit var roomAliasBottomSheetViewModelFactory: RoomAliasBottomSheetViewModel.Factory @Inject lateinit var controller: RoomAliasBottomSheetController private val viewModel: RoomAliasBottomSheetViewModel by fragmentViewModel(RoomAliasBottomSheetViewModel::class) override val showExpanded = true - override fun injectWith(injector: ScreenComponent) { - injector.inject(this) - } - override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): BottomSheetGenericListBinding { return BottomSheetGenericListBinding.inflate(inflater, container, false) } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/alias/detail/RoomAliasBottomSheetSharedAction.kt b/vector/src/main/java/im/vector/app/features/roomprofile/alias/detail/RoomAliasBottomSheetSharedAction.kt index 13909c401f..d7cb923603 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/alias/detail/RoomAliasBottomSheetSharedAction.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/alias/detail/RoomAliasBottomSheetSharedAction.kt @@ -24,8 +24,8 @@ import im.vector.app.core.platform.VectorSharedAction sealed class RoomAliasBottomSheetSharedAction( @StringRes val titleRes: Int, @DrawableRes val iconResId: Int = 0, - val destructive: Boolean = false) - : VectorSharedAction { + val destructive: Boolean = false) : + VectorSharedAction { data class ShareAlias(val matrixTo: String) : RoomAliasBottomSheetSharedAction( R.string.share, diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/alias/detail/RoomAliasBottomSheetState.kt b/vector/src/main/java/im/vector/app/features/roomprofile/alias/detail/RoomAliasBottomSheetState.kt index a61075cef6..1accc85e45 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/alias/detail/RoomAliasBottomSheetState.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/alias/detail/RoomAliasBottomSheetState.kt @@ -16,7 +16,7 @@ package im.vector.app.features.roomprofile.alias.detail -import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MavericksState data class RoomAliasBottomSheetState( val alias: String, @@ -25,7 +25,7 @@ data class RoomAliasBottomSheetState( val isMainAlias: Boolean, val isLocal: Boolean, val canEditCanonicalAlias: Boolean -) : MvRxState { +) : MavericksState { constructor(args: RoomAliasBottomSheetArgs) : this( alias = args.alias, diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/alias/detail/RoomAliasBottomSheetViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/alias/detail/RoomAliasBottomSheetViewModel.kt index e762b52025..0efef6ad8c 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/alias/detail/RoomAliasBottomSheetViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/alias/detail/RoomAliasBottomSheetViewModel.kt @@ -15,12 +15,12 @@ */ package im.vector.app.features.roomprofile.alias.detail -import com.airbnb.mvrx.FragmentViewModelContext -import com.airbnb.mvrx.MvRxViewModelFactory -import com.airbnb.mvrx.ViewModelContext +import com.airbnb.mvrx.MavericksViewModelFactory import dagger.assisted.Assisted -import dagger.assisted.AssistedInject import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.platform.EmptyAction import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel @@ -32,18 +32,11 @@ class RoomAliasBottomSheetViewModel @AssistedInject constructor( ) : VectorViewModel(initialState) { @AssistedFactory - interface Factory { - fun create(initialState: RoomAliasBottomSheetState): RoomAliasBottomSheetViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: RoomAliasBottomSheetState): RoomAliasBottomSheetViewModel } - companion object : MvRxViewModelFactory { - - @JvmStatic - override fun create(viewModelContext: ViewModelContext, state: RoomAliasBottomSheetState): RoomAliasBottomSheetViewModel? { - val fragment: RoomAliasBottomSheet = (viewModelContext as FragmentViewModelContext).fragment() - return fragment.roomAliasBottomSheetViewModelFactory.create(state) - } - } + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() init { setState { diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListFragment.kt index 807954438f..c9fc889242 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListFragment.kt @@ -34,13 +34,11 @@ import im.vector.app.core.utils.toast import im.vector.app.databinding.FragmentRoomSettingGenericBinding import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.roomprofile.RoomProfileArgs - import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary import org.matrix.android.sdk.api.util.toMatrixItem import javax.inject.Inject class RoomBannedMemberListFragment @Inject constructor( - val viewModelFactory: RoomBannedMemberListViewModel.Factory, private val roomMemberListController: RoomBannedMemberListController, private val avatarRenderer: AvatarRenderer ) : VectorBaseFragment(), diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListViewModel.kt index 1b64261fcb..d7efc2fb79 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListViewModel.kt @@ -16,18 +16,17 @@ package im.vector.app.features.roomprofile.banned -import androidx.lifecycle.viewModelScope -import com.airbnb.mvrx.FragmentViewModelContext -import com.airbnb.mvrx.MvRxViewModelFactory -import com.airbnb.mvrx.ViewModelContext +import com.airbnb.mvrx.MavericksViewModelFactory import dagger.assisted.Assisted -import dagger.assisted.AssistedInject import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import im.vector.app.R +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider -import im.vector.app.features.powerlevel.PowerLevelsObservableFactory +import im.vector.app.features.powerlevel.PowerLevelsFlowFactory import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.matrix.android.sdk.api.query.QueryStringValue @@ -39,55 +38,46 @@ import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.RoomMemberContent import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper -import org.matrix.android.sdk.rx.rx -import org.matrix.android.sdk.rx.unwrap +import org.matrix.android.sdk.flow.flow +import org.matrix.android.sdk.flow.unwrap class RoomBannedMemberListViewModel @AssistedInject constructor(@Assisted initialState: RoomBannedMemberListViewState, private val stringProvider: StringProvider, - private val session: Session) - : VectorViewModel(initialState) { + private val session: Session) : + VectorViewModel(initialState) { @AssistedFactory - interface Factory { - fun create(initialState: RoomBannedMemberListViewState): RoomBannedMemberListViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: RoomBannedMemberListViewState): RoomBannedMemberListViewModel } private val room = session.getRoom(initialState.roomId)!! init { - val rxRoom = room.rx() - room.rx().liveRoomSummary() + room.flow().liveRoomSummary() .unwrap() .execute { async -> copy(roomSummary = async) } - rxRoom.liveRoomMembers(roomMemberQueryParams { memberships = listOf(Membership.BAN) }) + room.flow().liveRoomMembers(roomMemberQueryParams { memberships = listOf(Membership.BAN) }) .execute { copy( bannedMemberSummaries = it ) } - val powerLevelsContentLive = PowerLevelsObservableFactory(room).createObservable() + val powerLevelsContentLive = PowerLevelsFlowFactory(room).createFlow() powerLevelsContentLive - .subscribe { + .setOnEach { val powerLevelsHelper = PowerLevelsHelper(it) - setState { copy(canUserBan = powerLevelsHelper.isUserAbleToBan(session.myUserId)) } + copy(canUserBan = powerLevelsHelper.isUserAbleToBan(session.myUserId)) } - .disposeOnClear() } - companion object : MvRxViewModelFactory { - - @JvmStatic - override fun create(viewModelContext: ViewModelContext, state: RoomBannedMemberListViewState): RoomBannedMemberListViewModel? { - val fragment: RoomBannedMemberListFragment = (viewModelContext as FragmentViewModelContext).fragment() - return fragment.viewModelFactory.create(state) - } - } + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() override fun handle(action: RoomBannedMemberListAction) { when (action) { diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListViewState.kt b/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListViewState.kt index 2861b30222..e36de58f97 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListViewState.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListViewState.kt @@ -17,7 +17,7 @@ package im.vector.app.features.roomprofile.banned import com.airbnb.mvrx.Async -import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.Uninitialized import im.vector.app.features.roomprofile.RoomProfileArgs import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary @@ -30,7 +30,7 @@ data class RoomBannedMemberListViewState( val filter: String = "", val onGoingModerationAction: List = emptyList(), val canUserBan: Boolean = false -) : MvRxState { +) : MavericksState { constructor(args: RoomProfileArgs) : this(roomId = args.roomId) } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListController.kt index 8399c9e238..ed0b88c8d3 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListController.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListController.kt @@ -17,14 +17,19 @@ package im.vector.app.features.roomprofile.members import com.airbnb.epoxy.TypedEpoxyController +import im.vector.app.R import im.vector.app.core.epoxy.dividerItem import im.vector.app.core.epoxy.profiles.buildProfileSection import im.vector.app.core.epoxy.profiles.profileMatrixItem +import im.vector.app.core.epoxy.profiles.profileMatrixItemWithPowerLevelWithPresence import im.vector.app.core.extensions.join +import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.StringProvider import im.vector.app.features.home.AvatarRenderer +import me.gujun.android.span.span import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.presence.model.UserPresence import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary import org.matrix.android.sdk.api.session.room.model.RoomThirdPartyInviteContent import org.matrix.android.sdk.api.util.MatrixItem @@ -34,6 +39,7 @@ import javax.inject.Inject class RoomMemberListController @Inject constructor( private val avatarRenderer: AvatarRenderer, private val stringProvider: StringProvider, + private val colorProvider: ColorProvider, private val roomMemberSummaryFilter: RoomMemberSummaryFilter ) : TypedEpoxyController() { @@ -84,17 +90,10 @@ class RoomMemberListController @Inject constructor( buildProfileSection( stringProvider.getString(powerLevelCategory.titleRes) ) + filteredRoomMemberList.join( each = { _, roomMember -> - profileMatrixItem { - id(roomMember.userId) - matrixItem(roomMember.toMatrixItem()) - avatarRenderer(host.avatarRenderer) - userEncryptionTrustLevel(data.trustLevelMap.invoke()?.get(roomMember.userId)) - clickListener { - host.callback?.onRoomMemberClicked(roomMember) - } - } + buildPresence(roomMember, powerLevelCategory, host, data, roomMember.userPresence) }, between = { _, roomMemberBefore -> dividerItem { @@ -123,6 +122,33 @@ class RoomMemberListController @Inject constructor( } } + private fun buildPresence(roomMember: RoomMemberSummary, + powerLevelCategory: RoomMemberListCategories, + host: RoomMemberListController, + data: RoomMemberListViewState, + userPresence: UserPresence? + ) { + val powerLabel = stringProvider.getString(powerLevelCategory.titleRes) + + profileMatrixItemWithPowerLevelWithPresence { + id(roomMember.userId) + matrixItem(roomMember.toMatrixItem()) + avatarRenderer(host.avatarRenderer) + userEncryptionTrustLevel(data.trustLevelMap.invoke()?.get(roomMember.userId)) + clickListener { + host.callback?.onRoomMemberClicked(roomMember) + } + userPresence(userPresence) + powerLevelLabel( + span { + span(powerLabel) { + textColor = host.colorProvider.getColorFromAttribute(R.attr.vctr_content_secondary) + } + } + ) + } + } + private fun buildThreePidInvites(data: RoomMemberListViewState) { val host = this data.threePidInvites() diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListFragment.kt index b8d0842c22..8840f61600 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListFragment.kt @@ -34,7 +34,6 @@ import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.FragmentRoomMemberListBinding import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.roomprofile.RoomProfileArgs - import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary @@ -43,20 +42,14 @@ import org.matrix.android.sdk.api.util.toMatrixItem import javax.inject.Inject class RoomMemberListFragment @Inject constructor( - val viewModelFactory: RoomMemberListViewModel.Factory, private val roomMemberListController: RoomMemberListController, private val avatarRenderer: AvatarRenderer ) : VectorBaseFragment(), - RoomMemberListController.Callback, - RoomMemberListViewModel.Factory { + RoomMemberListController.Callback { private val viewModel: RoomMemberListViewModel by fragmentViewModel() private val roomProfileArgs: RoomProfileArgs by args() - override fun create(initialState: RoomMemberListViewState): RoomMemberListViewModel { - return viewModelFactory.create(initialState) - } - override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentRoomMemberListBinding { return FragmentRoomMemberListBinding.inflate(inflater, container, false) } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewModel.kt index 5c6ff48403..adf5a31f2a 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewModel.kt @@ -16,20 +16,25 @@ package im.vector.app.features.roomprofile.members -import androidx.lifecycle.viewModelScope -import com.airbnb.mvrx.ActivityViewModelContext -import com.airbnb.mvrx.FragmentViewModelContext -import com.airbnb.mvrx.MvRxViewModelFactory -import com.airbnb.mvrx.ViewModelContext +import androidx.lifecycle.asFlow +import com.airbnb.mvrx.MavericksViewModelFactory import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel -import im.vector.app.features.powerlevel.PowerLevelsObservableFactory -import io.reactivex.Observable -import io.reactivex.android.schedulers.AndroidSchedulers +import im.vector.app.features.powerlevel.PowerLevelsFlowFactory +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel import org.matrix.android.sdk.api.extensions.orFalse @@ -43,33 +48,22 @@ import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper import org.matrix.android.sdk.api.session.room.powerlevels.Role -import org.matrix.android.sdk.rx.asObservable -import org.matrix.android.sdk.rx.mapOptional -import org.matrix.android.sdk.rx.rx -import org.matrix.android.sdk.rx.unwrap +import org.matrix.android.sdk.flow.flow +import org.matrix.android.sdk.flow.mapOptional +import org.matrix.android.sdk.flow.unwrap import timber.log.Timber class RoomMemberListViewModel @AssistedInject constructor(@Assisted initialState: RoomMemberListViewState, private val roomMemberSummaryComparator: RoomMemberSummaryComparator, - private val session: Session) - : VectorViewModel(initialState) { + private val session: Session) : + VectorViewModel(initialState) { @AssistedFactory - interface Factory { - fun create(initialState: RoomMemberListViewState): RoomMemberListViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: RoomMemberListViewState): RoomMemberListViewModel } - companion object : MvRxViewModelFactory { - - @JvmStatic - override fun create(viewModelContext: ViewModelContext, state: RoomMemberListViewState): RoomMemberListViewModel? { - 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") - } - } + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() private val room = session.getRoom(initialState.roomId)!! @@ -86,28 +80,26 @@ class RoomMemberListViewModel @AssistedInject constructor(@Assisted initialState memberships = Membership.activeMemberships() } - Observable - .combineLatest, PowerLevelsContent, RoomMemberSummaries>( - room.rx().liveRoomMembers(roomMemberQueryParams), - room.rx() - .liveStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.NoCondition) - .mapOptional { it.content.toModel() } - .unwrap(), - { roomMembers, powerLevelsContent -> - buildRoomMemberSummaries(powerLevelsContent, roomMembers) - } - ) + combine( + room.flow().liveRoomMembers(roomMemberQueryParams), + room.flow() + .liveStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.NoCondition) + .mapOptional { it.content.toModel() } + .unwrap() + ) { roomMembers, powerLevelsContent -> + buildRoomMemberSummaries(powerLevelsContent, roomMembers) + } .execute { async -> copy(roomMemberSummaries = async) } if (room.isEncrypted()) { - room.rx().liveRoomMembers(roomMemberQueryParams) - .observeOn(AndroidSchedulers.mainThread()) - .switchMap { membersSummary -> + room.flow().liveRoomMembers(roomMemberQueryParams) + .flowOn(Dispatchers.Main) + .flatMapLatest { membersSummary -> session.cryptoService().getLiveCryptoDeviceInfo(membersSummary.map { it.userId }) - .asObservable() - .doOnError { Timber.e(it) } + .asFlow() + .catch { Timber.e(it) } .map { deviceList -> // If any key change, emit the userIds list deviceList.groupBy { it.userId }.mapValues { @@ -129,8 +121,8 @@ class RoomMemberListViewModel @AssistedInject constructor(@Assisted initialState } private fun observePowerLevel() { - PowerLevelsObservableFactory(room).createObservable() - .subscribe { + PowerLevelsFlowFactory(room).createFlow() + .onEach { val permissions = ActionPermissions( canInvite = PowerLevelsHelper(it).isUserAbleToInvite(session.myUserId), canRevokeThreePidInvite = PowerLevelsHelper(it).isUserAllowedToSend( @@ -142,12 +134,11 @@ class RoomMemberListViewModel @AssistedInject constructor(@Assisted initialState setState { copy(actionsPermissions = permissions) } - } - .disposeOnClear() + }.launchIn(viewModelScope) } private fun observeRoomSummary() { - room.rx().liveRoomSummary() + room.flow().liveRoomSummary() .unwrap() .execute { async -> copy(roomSummary = async) @@ -155,7 +146,7 @@ class RoomMemberListViewModel @AssistedInject constructor(@Assisted initialState } private fun observeThirdPartyInvites() { - room.rx().liveStateEvents(setOf(EventType.STATE_ROOM_THIRD_PARTY_INVITE)) + room.flow().liveStateEvents(setOf(EventType.STATE_ROOM_THIRD_PARTY_INVITE)) .execute { async -> copy(threePidInvites = async) } @@ -192,7 +183,7 @@ class RoomMemberListViewModel @AssistedInject constructor(@Assisted initialState override fun handle(action: RoomMemberListAction) { when (action) { is RoomMemberListAction.RevokeThreePidInvite -> handleRevokeThreePidInvite(action) - is RoomMemberListAction.FilterMemberList -> handleFilterMemberList(action) + is RoomMemberListAction.FilterMemberList -> handleFilterMemberList(action) }.exhaustive } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewState.kt b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewState.kt index 63d07cc4dd..d736260f10 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewState.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewState.kt @@ -18,7 +18,7 @@ package im.vector.app.features.roomprofile.members import androidx.annotation.StringRes import com.airbnb.mvrx.Async -import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.Uninitialized import im.vector.app.R import im.vector.app.core.platform.GenericIdArgs @@ -36,7 +36,7 @@ data class RoomMemberListViewState( val threePidInvites: Async> = Uninitialized, val trustLevelMap: Async> = Uninitialized, val actionsPermissions: ActionPermissions = ActionPermissions() -) : MvRxState { +) : MavericksState { constructor(args: RoomProfileArgs) : this(roomId = args.roomId) diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberSummaryFilter.kt b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberSummaryFilter.kt index bd9fb7b941..449b8663d6 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberSummaryFilter.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberSummaryFilter.kt @@ -31,9 +31,9 @@ class RoomMemberSummaryFilter @Inject constructor() : Predicate(initialState) { @AssistedFactory - interface Factory { - fun create(initialState: RoomNotificationSettingsViewState): RoomNotificationSettingsViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: RoomNotificationSettingsViewState): RoomNotificationSettingsViewModel } - companion object : MvRxViewModelFactory { - - @JvmStatic - override fun create(viewModelContext: ViewModelContext, state: RoomNotificationSettingsViewState): RoomNotificationSettingsViewModel { - val fragmentModelContext = (viewModelContext as FragmentViewModelContext) - return if (fragmentModelContext.fragment is RoomNotificationSettingsFragment) { - val fragment: RoomNotificationSettingsFragment = fragmentModelContext.fragment() - fragment.viewModelFactory.create(state) - } else { - val fragment: RoomListQuickActionsBottomSheet = fragmentModelContext.fragment() - fragment.roomNotificationSettingsViewModelFactory.create(state) - } - } - } + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() private val room = session.getRoom(initialState.roomId)!! @@ -64,7 +49,7 @@ class RoomNotificationSettingsViewModel @AssistedInject constructor( } private fun observeSummary() { - room.rx().liveRoomSummary() + room.flow().liveRoomSummary() .unwrap() .execute { async -> copy(roomSummary = async) @@ -72,7 +57,7 @@ class RoomNotificationSettingsViewModel @AssistedInject constructor( } private fun observeNotificationState() { - room.rx() + room.flow() .liveNotificationState() .execute { copy(notificationState = it) diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/notifications/RoomNotificationSettingsViewState.kt b/vector/src/main/java/im/vector/app/features/roomprofile/notifications/RoomNotificationSettingsViewState.kt index 72e61fba70..832bb5036e 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/notifications/RoomNotificationSettingsViewState.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/notifications/RoomNotificationSettingsViewState.kt @@ -17,7 +17,7 @@ package im.vector.app.features.roomprofile.notifications import com.airbnb.mvrx.Async -import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized import im.vector.app.features.home.room.list.actions.RoomListActionsArgs @@ -30,7 +30,7 @@ data class RoomNotificationSettingsViewState( val roomSummary: Async = Uninitialized, val isLoading: Boolean = false, val notificationState: Async = Uninitialized -) : MvRxState { +) : MavericksState { constructor(args: RoomProfileArgs) : this(roomId = args.roomId) constructor(args: RoomListActionsArgs) : this(roomId = args.roomId) } @@ -40,8 +40,8 @@ data class RoomNotificationSettingsViewState( */ val RoomNotificationSettingsViewState.notificationStateMapped: Async get() { - if ((roomSummary()?.isEncrypted == true && notificationState() == RoomNotificationState.MENTIONS_ONLY) - || notificationState() == RoomNotificationState.ALL_MESSAGES) { + if ((roomSummary()?.isEncrypted == true && notificationState() == RoomNotificationState.MENTIONS_ONLY) || + notificationState() == RoomNotificationState.ALL_MESSAGES) { /** if in an encrypted room, mentions notifications are not supported so show "All Messages" as selected. * Also in the new settings there is no notion of notifications without sound so it maps to noisy also */ @@ -49,6 +49,7 @@ val RoomNotificationSettingsViewState.notificationStateMapped: Async(initialState) { + private val session: Session) : + VectorViewModel(initialState) { @AssistedFactory - interface Factory { - fun create(initialState: RoomPermissionsViewState): RoomPermissionsViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: RoomPermissionsViewState): RoomPermissionsViewModel } - companion object : MvRxViewModelFactory { - - @JvmStatic - override fun create(viewModelContext: ViewModelContext, state: RoomPermissionsViewState): RoomPermissionsViewModel? { - val fragment: RoomPermissionsFragment = (viewModelContext as FragmentViewModelContext).fragment() - return fragment.viewModelFactory.create(state) - } - } + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() private val room = session.getRoom(initialState.roomId)!! @@ -62,7 +56,7 @@ class RoomPermissionsViewModel @AssistedInject constructor(@Assisted initialStat } private fun observeRoomSummary() { - room.rx().liveRoomSummary() + room.flow().liveRoomSummary() .unwrap() .execute { async -> copy( @@ -72,9 +66,9 @@ class RoomPermissionsViewModel @AssistedInject constructor(@Assisted initialStat } private fun observePowerLevel() { - PowerLevelsObservableFactory(room) - .createObservable() - .subscribe { powerLevelContent -> + PowerLevelsFlowFactory(room) + .createFlow() + .onEach { powerLevelContent -> val powerLevelsHelper = PowerLevelsHelper(powerLevelContent) val permissions = RoomPermissionsViewState.ActionPermissions( canChangePowerLevels = powerLevelsHelper.isUserAllowedToSend( @@ -89,8 +83,7 @@ class RoomPermissionsViewModel @AssistedInject constructor(@Assisted initialStat currentPowerLevelsContent = Success(powerLevelContent) ) } - } - .disposeOnClear() + }.launchIn(viewModelScope) } override fun handle(action: RoomPermissionsAction) { diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/permissions/RoomPermissionsViewState.kt b/vector/src/main/java/im/vector/app/features/roomprofile/permissions/RoomPermissionsViewState.kt index ce38ab87e5..9a5ac4c19f 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/permissions/RoomPermissionsViewState.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/permissions/RoomPermissionsViewState.kt @@ -17,7 +17,7 @@ package im.vector.app.features.roomprofile.permissions import com.airbnb.mvrx.Async -import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.Uninitialized import im.vector.app.features.roomprofile.RoomProfileArgs import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent @@ -30,7 +30,7 @@ data class RoomPermissionsViewState( val showAdvancedPermissions: Boolean = false, val currentPowerLevelsContent: Async = Uninitialized, val isLoading: Boolean = false -) : MvRxState { +) : MavericksState { constructor(args: RoomProfileArgs) : this(roomId = args.roomId) diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsFragment.kt index b7821c056c..ce059881b8 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsFragment.kt @@ -52,7 +52,6 @@ import java.util.UUID import javax.inject.Inject class RoomSettingsFragment @Inject constructor( - val viewModelFactory: RoomSettingsViewModel.Factory, private val controller: RoomSettingsController, colorProvider: ColorProvider, private val avatarRenderer: AvatarRenderer @@ -60,8 +59,7 @@ class RoomSettingsFragment @Inject constructor( VectorBaseFragment(), RoomSettingsController.Callback, OnBackPressed, - GalleryOrCameraDialogHelper.Listener, - RoomSettingsViewModel.Factory { + GalleryOrCameraDialogHelper.Listener { private val viewModel: RoomSettingsViewModel by fragmentViewModel() private lateinit var roomProfileSharedActionViewModel: RoomProfileSharedActionViewModel @@ -77,10 +75,6 @@ class RoomSettingsFragment @Inject constructor( override fun getMenuRes() = R.menu.vector_room_settings - override fun create(initialState: RoomSettingsViewState): RoomSettingsViewModel { - return viewModelFactory.create(initialState) - } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) roomProfileSharedActionViewModel = activityViewModelProvider.get(RoomProfileSharedActionViewModel::class.java) diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewModel.kt index e872a04d80..1e3cd053b1 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewModel.kt @@ -17,19 +17,20 @@ package im.vector.app.features.roomprofile.settings import androidx.core.net.toFile -import com.airbnb.mvrx.ActivityViewModelContext -import com.airbnb.mvrx.FragmentViewModelContext -import com.airbnb.mvrx.MvRxViewModelFactory -import com.airbnb.mvrx.ViewModelContext +import com.airbnb.mvrx.MavericksViewModelFactory import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel -import im.vector.app.features.powerlevel.PowerLevelsObservableFactory +import im.vector.app.features.powerlevel.PowerLevelsFlowFactory import im.vector.app.features.settings.VectorPreferences -import io.reactivex.Completable -import io.reactivex.Observable +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.Session @@ -41,31 +42,21 @@ import org.matrix.android.sdk.api.session.room.model.RoomGuestAccessContent import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibilityContent import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesContent import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper -import org.matrix.android.sdk.rx.mapOptional -import org.matrix.android.sdk.rx.rx -import org.matrix.android.sdk.rx.unwrap +import org.matrix.android.sdk.flow.flow +import org.matrix.android.sdk.flow.mapOptional +import org.matrix.android.sdk.flow.unwrap class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: RoomSettingsViewState, private val vectorPreferences: VectorPreferences, - private val session: Session) - : VectorViewModel(initialState) { + private val session: Session) : + VectorViewModel(initialState) { @AssistedFactory - interface Factory { - fun create(initialState: RoomSettingsViewState): RoomSettingsViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: RoomSettingsViewState): RoomSettingsViewModel } - companion object : MvRxViewModelFactory { - - @JvmStatic - override fun create(viewModelContext: ViewModelContext, state: RoomSettingsViewState): RoomSettingsViewModel? { - 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") - } - } + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() private val room = session.getRoom(initialState.roomId)!! @@ -97,7 +88,7 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: } private fun observeState() { - selectSubscribe( + onEach( RoomSettingsViewState::avatarAction, RoomSettingsViewState::newName, RoomSettingsViewState::newTopic, @@ -112,18 +103,18 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: val summary = asyncSummary() setState { copy( - showSaveAction = avatarAction !is RoomSettingsViewState.AvatarAction.None - || summary?.name != newName - || summary?.topic != newTopic - || (newHistoryVisibility != null && newHistoryVisibility != currentHistoryVisibility) - || newJoinRule.hasChanged() + showSaveAction = avatarAction !is RoomSettingsViewState.AvatarAction.None || + summary?.name != newName || + summary?.topic != newTopic || + (newHistoryVisibility != null && newHistoryVisibility != currentHistoryVisibility) || + newJoinRule.hasChanged() ) } } } private fun observeRoomSummary() { - room.rx().liveRoomSummary() + room.flow().liveRoomSummary() .unwrap() .execute { async -> val roomSummary = async.invoke() @@ -134,10 +125,10 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: ) } - val powerLevelsContentLive = PowerLevelsObservableFactory(room).createObservable() + val powerLevelsContentLive = PowerLevelsFlowFactory(room).createFlow() powerLevelsContentLive - .subscribe { + .onEach { val powerLevelsHelper = PowerLevelsHelper(it) val permissions = RoomSettingsViewState.ActionPermissions( canChangeAvatar = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_AVATAR), @@ -146,68 +137,62 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: canChangeHistoryVisibility = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_HISTORY_VISIBILITY), canChangeJoinRule = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, - EventType.STATE_ROOM_JOIN_RULES) - && powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, - EventType.STATE_ROOM_GUEST_ACCESS), + EventType.STATE_ROOM_JOIN_RULES) && + powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, + EventType.STATE_ROOM_GUEST_ACCESS), canAddChildren = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_SPACE_CHILD) ) - setState { copy(actionPermissions = permissions) } - } - .disposeOnClear() + setState { + copy(actionPermissions = permissions) + } + }.launchIn(viewModelScope) } private fun observeRoomHistoryVisibility() { - room.rx() + room.flow() .liveStateEvent(EventType.STATE_ROOM_HISTORY_VISIBILITY, QueryStringValue.NoCondition) .mapOptional { it.content.toModel() } .unwrap() - .subscribe { - it.historyVisibility?.let { - setState { copy(currentHistoryVisibility = it) } - } + .mapNotNull { it.historyVisibility } + .setOnEach { + copy(currentHistoryVisibility = it) } - .disposeOnClear() } private fun observeJoinRule() { - room.rx() + room.flow() .liveStateEvent(EventType.STATE_ROOM_JOIN_RULES, QueryStringValue.NoCondition) .mapOptional { it.content.toModel() } .unwrap() - .subscribe { - it.joinRules?.let { - setState { copy(currentRoomJoinRules = it) } - } + .mapNotNull { it.joinRules } + .setOnEach { + copy(currentRoomJoinRules = it) } - .disposeOnClear() } private fun observeGuestAccess() { - room.rx() + room.flow() .liveStateEvent(EventType.STATE_ROOM_GUEST_ACCESS, QueryStringValue.NoCondition) .mapOptional { it.content.toModel() } .unwrap() - .subscribe { - it.guestAccess?.let { - setState { copy(currentGuestAccess = it) } - } + .mapNotNull { it.guestAccess } + .setOnEach { + copy(currentGuestAccess = it) } - .disposeOnClear() } /** * We do not want to use the fallback avatar url, which can be the other user avatar, or the current user avatar. */ private fun observeRoomAvatar() { - room.rx() + room.flow() .liveStateEvent(EventType.STATE_ROOM_AVATAR, QueryStringValue.NoCondition) .mapOptional { it.content.toModel() } .unwrap() - .subscribe { - setState { copy(currentRoomAvatarUrl = it.avatarUrl) } + .setOnEach { + copy(currentRoomAvatarUrl = it.avatarUrl) } - .disposeOnClear() } override fun handle(action: RoomSettingsAction) { @@ -261,61 +246,57 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: } private fun saveSettings() = withState { state -> - postLoading(true) - - val operationList = mutableListOf() + val operationList = mutableListOf Unit>() val summary = state.roomSummary.invoke() when (val avatarAction = state.avatarAction) { RoomSettingsViewState.AvatarAction.None -> Unit RoomSettingsViewState.AvatarAction.DeleteAvatar -> { - operationList.add(room.rx().deleteAvatar()) + operationList.add { room.deleteAvatar() } } is RoomSettingsViewState.AvatarAction.UpdateAvatar -> { - operationList.add(room.rx().updateAvatar(avatarAction.newAvatarUri, avatarAction.newAvatarFileName)) + operationList.add { room.updateAvatar(avatarAction.newAvatarUri, avatarAction.newAvatarFileName) } } } if (summary?.name != state.newName) { - operationList.add(room.rx().updateName(state.newName ?: "")) + operationList.add { room.updateName(state.newName ?: "") } } if (summary?.topic != state.newTopic) { - operationList.add(room.rx().updateTopic(state.newTopic ?: "")) + operationList.add { room.updateTopic(state.newTopic ?: "") } } if (state.newHistoryVisibility != null) { - operationList.add(room.rx().updateHistoryReadability(state.newHistoryVisibility)) + operationList.add { room.updateHistoryReadability(state.newHistoryVisibility) } } if (state.newRoomJoinRules.hasChanged()) { - operationList.add(room.rx().updateJoinRule(state.newRoomJoinRules.newJoinRules, state.newRoomJoinRules.newGuestAccess)) + operationList.add { room.updateJoinRule(state.newRoomJoinRules.newJoinRules, state.newRoomJoinRules.newGuestAccess) } + } + viewModelScope.launch { + updateLoadingState(isLoading = true) + try { + for (operation in operationList) { + operation.invoke() + } + setState { + deletePendingAvatar(this) + copy( + avatarAction = RoomSettingsViewState.AvatarAction.None, + newHistoryVisibility = null, + newRoomJoinRules = RoomSettingsViewState.NewJoinRule() + ) + } + _viewEvents.post(RoomSettingsViewEvents.Success) + } catch (failure: Throwable) { + _viewEvents.post(RoomSettingsViewEvents.Failure(failure)) + } finally { + updateLoadingState(isLoading = false) + } } - - Observable - .fromIterable(operationList) - .concatMapCompletable { it } - .subscribe( - { - postLoading(false) - setState { - deletePendingAvatar(this) - copy( - avatarAction = RoomSettingsViewState.AvatarAction.None, - newHistoryVisibility = null, - newRoomJoinRules = RoomSettingsViewState.NewJoinRule() - ) - } - _viewEvents.post(RoomSettingsViewEvents.Success) - }, - { - postLoading(false) - _viewEvents.post(RoomSettingsViewEvents.Failure(it)) - } - ) - .disposeOnClear() } - private fun postLoading(isLoading: Boolean) { + private fun updateLoadingState(isLoading: Boolean) { setState { copy(isLoading = isLoading) } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewState.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewState.kt index 403836b268..122e0034c6 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewState.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewState.kt @@ -18,7 +18,7 @@ package im.vector.app.features.roomprofile.settings import android.net.Uri import com.airbnb.mvrx.Async -import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.Uninitialized import im.vector.app.R import im.vector.app.core.resources.StringProvider @@ -46,7 +46,7 @@ data class RoomSettingsViewState( val actionPermissions: ActionPermissions = ActionPermissions(), val supportsRestricted: Boolean = false, val canUpgradeToRestricted: Boolean = false -) : MvRxState { +) : MavericksState { constructor(args: RoomProfileArgs) : this(roomId = args.roomId) diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/historyvisibility/RoomHistoryVisibilityBottomSheet.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/historyvisibility/RoomHistoryVisibilityBottomSheet.kt index 4089139b78..c63cf918c8 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/historyvisibility/RoomHistoryVisibilityBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/historyvisibility/RoomHistoryVisibilityBottomSheet.kt @@ -21,7 +21,7 @@ import android.os.Parcelable import android.view.View import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState -import im.vector.app.core.di.ScreenComponent +import dagger.hilt.android.AndroidEntryPoint import im.vector.app.core.ui.bottomsheet.BottomSheetGeneric import im.vector.app.core.ui.bottomsheet.BottomSheetGenericController import kotlinx.parcelize.Parcelize @@ -33,16 +33,13 @@ data class RoomHistoryVisibilityBottomSheetArgs( val currentRoomHistoryVisibility: RoomHistoryVisibility ) : Parcelable +@AndroidEntryPoint class RoomHistoryVisibilityBottomSheet : BottomSheetGeneric() { private lateinit var roomHistoryVisibilitySharedActionViewModel: RoomHistoryVisibilitySharedActionViewModel @Inject lateinit var controller: RoomHistoryVisibilityController private val viewModel: RoomHistoryVisibilityViewModel by fragmentViewModel(RoomHistoryVisibilityViewModel::class) - override fun injectWith(injector: ScreenComponent) { - injector.inject(this) - } - override fun getController(): BottomSheetGenericController = controller override fun onViewCreated(view: View, savedInstanceState: Bundle?) { diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/historyvisibility/RoomHistoryVisibilitySharedActionViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/historyvisibility/RoomHistoryVisibilitySharedActionViewModel.kt index 4f1bdca194..c4f4892984 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/historyvisibility/RoomHistoryVisibilitySharedActionViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/historyvisibility/RoomHistoryVisibilitySharedActionViewModel.kt @@ -19,5 +19,5 @@ package im.vector.app.features.roomprofile.settings.historyvisibility import im.vector.app.core.platform.VectorSharedActionViewModel import javax.inject.Inject -class RoomHistoryVisibilitySharedActionViewModel @Inject constructor() - : VectorSharedActionViewModel() +class RoomHistoryVisibilitySharedActionViewModel @Inject constructor() : + VectorSharedActionViewModel() diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/historyvisibility/RoomHistoryVisibilityViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/historyvisibility/RoomHistoryVisibilityViewModel.kt index c2a8ae967f..5580156918 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/historyvisibility/RoomHistoryVisibilityViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/historyvisibility/RoomHistoryVisibilityViewModel.kt @@ -18,5 +18,5 @@ package im.vector.app.features.roomprofile.settings.historyvisibility import im.vector.app.core.ui.bottomsheet.BottomSheetGenericViewModel -class RoomHistoryVisibilityViewModel(initialState: RoomHistoryVisibilityState) - : BottomSheetGenericViewModel(initialState) +class RoomHistoryVisibilityViewModel(initialState: RoomHistoryVisibilityState) : + BottomSheetGenericViewModel(initialState) diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleActivity.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleActivity.kt index 21c39ad49d..bb8db019c3 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleActivity.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleActivity.kt @@ -22,13 +22,13 @@ import android.os.Bundle import androidx.core.view.isVisible import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Loading -import com.airbnb.mvrx.MvRx +import com.airbnb.mvrx.Mavericks import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.viewModel import com.airbnb.mvrx.withState +import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R -import im.vector.app.core.di.ScreenComponent import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.extensions.addFragment import im.vector.app.core.extensions.commitTransaction @@ -44,29 +44,20 @@ import im.vector.app.features.roomprofile.settings.joinrule.advanced.RoomJoinRul import im.vector.app.features.roomprofile.settings.joinrule.advanced.RoomJoinRuleChooseRestrictedViewModel import javax.inject.Inject -class RoomJoinRuleActivity : VectorBaseActivity(), - RoomJoinRuleChooseRestrictedViewModel.Factory { +@AndroidEntryPoint +class RoomJoinRuleActivity : VectorBaseActivity() { override fun getBinding() = ActivitySimpleBinding.inflate(layoutInflater) private lateinit var roomProfileArgs: RoomProfileArgs - @Inject - lateinit var allowListViewModelFactory: RoomJoinRuleChooseRestrictedViewModel.Factory - @Inject lateinit var errorFormatter: ErrorFormatter val viewModel: RoomJoinRuleChooseRestrictedViewModel by viewModel() - override fun create(initialState: RoomJoinRuleChooseRestrictedState) = allowListViewModelFactory.create(initialState) - - override fun injectWith(injector: ScreenComponent) { - injector.inject(this) - } - override fun initUiAndData() { - roomProfileArgs = intent?.extras?.getParcelable(MvRx.KEY_ARG) ?: return + roomProfileArgs = intent?.extras?.getParcelable(Mavericks.KEY_ARG) ?: return if (isFirstCreation()) { addFragment( R.id.simpleFragmentContainer, @@ -78,7 +69,7 @@ class RoomJoinRuleActivity : VectorBaseActivity(), override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - viewModel.selectSubscribe(this, RoomJoinRuleChooseRestrictedState::updatingStatus) { + viewModel.onEach(RoomJoinRuleChooseRestrictedState::updatingStatus) { when (it) { Uninitialized -> { // nop @@ -142,7 +133,7 @@ class RoomJoinRuleActivity : VectorBaseActivity(), fun newIntent(context: Context, roomId: String): Intent { val roomProfileArgs = RoomProfileArgs(roomId) return Intent(context, RoomJoinRuleActivity::class.java).apply { - putExtra(MvRx.KEY_ARG, roomProfileArgs) + putExtra(Mavericks.KEY_ARG, roomProfileArgs) } } } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleBottomSheet.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleBottomSheet.kt index f0f8193cc5..4185c2031b 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleBottomSheet.kt @@ -21,7 +21,7 @@ import android.os.Parcelable import android.view.View import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState -import im.vector.app.core.di.ScreenComponent +import dagger.hilt.android.AndroidEntryPoint import im.vector.app.core.ui.bottomsheet.BottomSheetGeneric import im.vector.app.core.ui.bottomsheet.BottomSheetGenericController import kotlinx.parcelize.Parcelize @@ -44,16 +44,13 @@ data class RoomJoinRuleBottomSheetArgs( val parentSpaceName: String? ) : Parcelable +@AndroidEntryPoint class RoomJoinRuleBottomSheet : BottomSheetGeneric() { private lateinit var roomJoinRuleSharedActionViewModel: RoomJoinRuleSharedActionViewModel @Inject lateinit var controller: RoomJoinRuleController private val viewModel: RoomJoinRuleViewModel by fragmentViewModel(RoomJoinRuleViewModel::class) - override fun injectWith(injector: ScreenComponent) { - injector.inject(this) - } - override fun getController(): BottomSheetGenericController = controller override fun onViewCreated(view: View, savedInstanceState: Bundle?) { diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleSharedActionViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleSharedActionViewModel.kt index a7df2cb475..971d71e0df 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleSharedActionViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleSharedActionViewModel.kt @@ -19,5 +19,5 @@ package im.vector.app.features.roomprofile.settings.joinrule import im.vector.app.core.platform.VectorSharedActionViewModel import javax.inject.Inject -class RoomJoinRuleSharedActionViewModel @Inject constructor() - : VectorSharedActionViewModel() +class RoomJoinRuleSharedActionViewModel @Inject constructor() : + VectorSharedActionViewModel() diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleViewModel.kt index 4305bfa72d..1ff374bf5b 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleViewModel.kt @@ -18,5 +18,5 @@ package im.vector.app.features.roomprofile.settings.joinrule import im.vector.app.core.ui.bottomsheet.BottomSheetGenericViewModel -class RoomJoinRuleViewModel(initialState: RoomJoinRuleState) - : BottomSheetGenericViewModel(initialState) +class RoomJoinRuleViewModel(initialState: RoomJoinRuleState) : + BottomSheetGenericViewModel(initialState) diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/SpaceJoinRuleItem.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/SpaceJoinRuleItem.kt index 9110f9b32e..382ef1c545 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/SpaceJoinRuleItem.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/SpaceJoinRuleItem.kt @@ -28,6 +28,7 @@ import im.vector.app.core.epoxy.ClickListener import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.epoxy.onClick +import im.vector.app.core.extensions.setAttributeTintedImageResource import im.vector.app.core.utils.DebouncedClickListener import im.vector.app.features.home.AvatarRenderer import org.matrix.android.sdk.api.util.MatrixItem @@ -57,7 +58,7 @@ abstract class SpaceJoinRuleItem : VectorEpoxyModel() holder.upgradeRequiredButton.setOnClickListener(DebouncedClickListener(listener)) if (selected) { - holder.radioImage.setImageDrawable(ContextCompat.getDrawable(holder.view.context, R.drawable.ic_radio_on)) + holder.radioImage.setAttributeTintedImageResource(R.drawable.ic_radio_on, R.attr.colorPrimary) holder.radioImage.contentDescription = holder.view.context.getString(R.string.a11y_checked) } else { holder.radioImage.setImageDrawable(ContextCompat.getDrawable(holder.view.context, R.drawable.ic_radio_off)) diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/advanced/RoomJoinRuleChooseRestrictedState.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/advanced/RoomJoinRuleChooseRestrictedState.kt index 8a107ce8f1..157f53b56e 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/advanced/RoomJoinRuleChooseRestrictedState.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/advanced/RoomJoinRuleChooseRestrictedState.kt @@ -17,7 +17,7 @@ package im.vector.app.features.roomprofile.settings.joinrule.advanced import com.airbnb.mvrx.Async -import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.Uninitialized import im.vector.app.features.roomprofile.RoomProfileArgs import im.vector.app.features.roomprofile.settings.joinrule.JoinRulesOptionSupport @@ -45,6 +45,6 @@ data class RoomJoinRuleChooseRestrictedState( val restrictedSupportedByThisVersion: Boolean = false, val restrictedVersionNeeded: String? = null, val didSwitchToReplacementRoom: Boolean = false -) : MvRxState { +) : MavericksState { constructor(args: RoomProfileArgs) : this(roomId = args.roomId) } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/advanced/RoomJoinRuleChooseRestrictedViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/advanced/RoomJoinRuleChooseRestrictedViewModel.kt index eaa435f853..4bd7568ccd 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/advanced/RoomJoinRuleChooseRestrictedViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/advanced/RoomJoinRuleChooseRestrictedViewModel.kt @@ -18,23 +18,22 @@ package im.vector.app.features.roomprofile.settings.joinrule.advanced import android.graphics.Typeface import androidx.core.text.toSpannable -import androidx.lifecycle.viewModelScope -import com.airbnb.mvrx.ActivityViewModelContext import com.airbnb.mvrx.Fail -import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.Loading -import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized -import com.airbnb.mvrx.ViewModelContext import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import im.vector.app.R +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider import im.vector.app.core.utils.styleMatchingText +import im.vector.app.features.displayname.getBestName import im.vector.app.features.roomprofile.settings.joinrule.toOption import im.vector.app.features.settings.VectorPreferences import kotlinx.coroutines.launch @@ -78,9 +77,9 @@ class RoomJoinRuleChooseRestrictedViewModel @AssistedInject constructor( val unknownAllowedOrRooms = mutableListOf() initialAllowList.orEmpty().forEach { entry -> val summary = entry.roomId?.let { session.getRoomSummary(it) } - if (summary == null // it's not known by me - || summary.roomType != RoomType.SPACE // it's not a space - || !roomSummary.flattenParentIds.contains(summary.roomId) // it's not a parent space + if (summary == null || // it's not known by me + summary.roomType != RoomType.SPACE || // it's not a space + !roomSummary.flattenParentIds.contains(summary.roomId) // it's not a parent space ) { (summary?.toMatrixItem() ?: entry.roomId?.let { MatrixItem.RoomItem(it, null, null) })?.let { unknownAllowedOrRooms.add(it) @@ -104,8 +103,8 @@ class RoomJoinRuleChooseRestrictedViewModel @AssistedInject constructor( // server is not really checking that, just to be sure let's check val restrictedSupportedByThisVersion = homeServerCapabilities .isFeatureSupported(HomeServerCapabilities.ROOM_CAP_RESTRICTED, room.getRoomVersion()) - if (safeRule == RoomJoinRules.RESTRICTED - && !restrictedSupportedByThisVersion) { + if (safeRule == RoomJoinRules.RESTRICTED && + !restrictedSupportedByThisVersion) { safeRule = RoomJoinRules.INVITE } @@ -174,8 +173,8 @@ class RoomJoinRuleChooseRestrictedViewModel @AssistedInject constructor( } @AssistedFactory - interface Factory { - fun create(initialState: RoomJoinRuleChooseRestrictedState): RoomJoinRuleChooseRestrictedViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: RoomJoinRuleChooseRestrictedState): RoomJoinRuleChooseRestrictedViewModel } override fun handle(action: RoomJoinRuleChooseRestrictedActions) { @@ -390,15 +389,5 @@ class RoomJoinRuleChooseRestrictedViewModel @AssistedInject constructor( } } - companion object : MvRxViewModelFactory { - - override fun create(viewModelContext: ViewModelContext, state: RoomJoinRuleChooseRestrictedState) - : RoomJoinRuleChooseRestrictedViewModel? { - 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") - } - } + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsFragment.kt index 2141b6bf27..3716d9682c 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsFragment.kt @@ -38,16 +38,13 @@ import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.notifications.NotificationUtils import im.vector.app.features.roomprofile.RoomProfileArgs import kotlinx.coroutines.launch - import org.matrix.android.sdk.api.util.toMatrixItem import javax.inject.Inject class RoomUploadsFragment @Inject constructor( - private val viewModelFactory: RoomUploadsViewModel.Factory, private val avatarRenderer: AvatarRenderer, private val notificationUtils: NotificationUtils -) : VectorBaseFragment(), - RoomUploadsViewModel.Factory by viewModelFactory { +) : VectorBaseFragment() { private val roomProfileArgs: RoomProfileArgs by args() diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsViewModel.kt index 1d6b056816..92ff33395e 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsViewModel.kt @@ -16,24 +16,22 @@ package im.vector.app.features.roomprofile.uploads -import androidx.lifecycle.viewModelScope -import com.airbnb.mvrx.ActivityViewModelContext import com.airbnb.mvrx.Fail -import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.Loading -import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.Success -import com.airbnb.mvrx.ViewModelContext import dagger.assisted.Assisted -import dagger.assisted.AssistedInject import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import kotlinx.coroutines.launch import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.room.model.message.MessageType -import org.matrix.android.sdk.rx.rx -import org.matrix.android.sdk.rx.unwrap +import org.matrix.android.sdk.flow.flow +import org.matrix.android.sdk.flow.unwrap class RoomUploadsViewModel @AssistedInject constructor( @Assisted initialState: RoomUploadsViewState, @@ -41,21 +39,11 @@ class RoomUploadsViewModel @AssistedInject constructor( ) : VectorViewModel(initialState) { @AssistedFactory - interface Factory { - fun create(initialState: RoomUploadsViewState): RoomUploadsViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: RoomUploadsViewState): RoomUploadsViewModel } - companion object : MvRxViewModelFactory { - - @JvmStatic - override fun create(viewModelContext: ViewModelContext, state: RoomUploadsViewState): RoomUploadsViewModel? { - 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") - } - } + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() private val room = session.getRoom(initialState.roomId)!! @@ -66,7 +54,7 @@ class RoomUploadsViewModel @AssistedInject constructor( } private fun observeRoomSummary() { - room.rx().liveRoomSummary() + room.flow().liveRoomSummary() .unwrap() .execute { async -> copy(roomSummary = async) @@ -91,8 +79,8 @@ class RoomUploadsViewModel @AssistedInject constructor( val groupedUploadEvents = result.uploadEvents .groupBy { - it.contentWithAttachmentContent.msgType == MessageType.MSGTYPE_IMAGE - || it.contentWithAttachmentContent.msgType == MessageType.MSGTYPE_VIDEO + it.contentWithAttachmentContent.msgType == MessageType.MSGTYPE_IMAGE || + it.contentWithAttachmentContent.msgType == MessageType.MSGTYPE_VIDEO } setState { diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsViewState.kt b/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsViewState.kt index b85e4f04de..a2b5aa99df 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsViewState.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsViewState.kt @@ -17,7 +17,7 @@ package im.vector.app.features.roomprofile.uploads import com.airbnb.mvrx.Async -import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.Uninitialized import im.vector.app.features.roomprofile.RoomProfileArgs import org.matrix.android.sdk.api.session.room.model.RoomSummary @@ -33,7 +33,7 @@ data class RoomUploadsViewState( val asyncEventsRequest: Async = Uninitialized, // True if more result are available server side val hasMore: Boolean = true -) : MvRxState { +) : MavericksState { constructor(args: RoomProfileArgs) : this(roomId = args.roomId) } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/uploads/files/RoomUploadsFilesFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/uploads/files/RoomUploadsFilesFragment.kt index 972c606867..1739378761 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/uploads/files/RoomUploadsFilesFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/uploads/files/RoomUploadsFilesFragment.kt @@ -35,7 +35,6 @@ import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.FragmentGenericStateViewRecyclerBinding import im.vector.app.features.roomprofile.uploads.RoomUploadsAction import im.vector.app.features.roomprofile.uploads.RoomUploadsViewModel - import org.matrix.android.sdk.api.session.room.uploads.UploadEvent import javax.inject.Inject diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/uploads/media/RoomUploadsMediaFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/uploads/media/RoomUploadsMediaFragment.kt index a7f7dbfe98..eb4337cffa 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/uploads/media/RoomUploadsMediaFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/uploads/media/RoomUploadsMediaFragment.kt @@ -46,7 +46,6 @@ import im.vector.app.features.roomprofile.uploads.RoomUploadsAction import im.vector.app.features.roomprofile.uploads.RoomUploadsFragment import im.vector.app.features.roomprofile.uploads.RoomUploadsViewModel import im.vector.app.features.roomprofile.uploads.RoomUploadsViewState - import org.matrix.android.sdk.api.session.room.model.message.MessageImageContent import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent import org.matrix.android.sdk.api.session.room.model.message.getFileUrl diff --git a/vector/src/main/java/im/vector/app/features/session/SessionListener.kt b/vector/src/main/java/im/vector/app/features/session/SessionListener.kt index d07e26d82d..c1ee0b527e 100644 --- a/vector/src/main/java/im/vector/app/features/session/SessionListener.kt +++ b/vector/src/main/java/im/vector/app/features/session/SessionListener.kt @@ -20,8 +20,8 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import im.vector.app.core.extensions.postLiveEvent import im.vector.app.core.utils.LiveEvent -import kotlinx.coroutines.cancelChildren import im.vector.app.features.call.vectorCallService +import kotlinx.coroutines.cancelChildren import kotlinx.coroutines.launch import org.matrix.android.sdk.api.failure.GlobalError import org.matrix.android.sdk.api.session.Session diff --git a/vector/src/main/java/im/vector/app/features/settings/SecretsSynchronisationInfo.kt b/vector/src/main/java/im/vector/app/features/settings/SecretsSynchronisationInfo.kt new file mode 100644 index 0000000000..5afcb77587 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/SecretsSynchronisationInfo.kt @@ -0,0 +1,71 @@ +/* + * 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.settings + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME +import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME +import org.matrix.android.sdk.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME +import org.matrix.android.sdk.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME +import org.matrix.android.sdk.flow.flow +import org.matrix.android.sdk.rx.SecretsSynchronisationInfo + +data class SecretsSynchronisationInfo( + val isBackupSetup: Boolean, + val isCrossSigningEnabled: Boolean, + val isCrossSigningTrusted: Boolean, + val allPrivateKeysKnown: Boolean, + val megolmBackupAvailable: Boolean, + val megolmSecretKnown: Boolean, + val isMegolmKeyIn4S: Boolean +) + +fun Session.liveSecretSynchronisationInfo(): Flow { + val sessionFlow = flow() + return combine( + sessionFlow.liveUserAccountData(setOf(MASTER_KEY_SSSS_NAME, USER_SIGNING_KEY_SSSS_NAME, SELF_SIGNING_KEY_SSSS_NAME, KEYBACKUP_SECRET_SSSS_NAME)), + sessionFlow.liveCrossSigningInfo(myUserId), + sessionFlow.liveCrossSigningPrivateKeys() + ) { _, crossSigningInfo, pInfo -> + // first check if 4S is already setup + val is4SSetup = sharedSecretStorageService.isRecoverySetup() + val isCrossSigningEnabled = crossSigningInfo.getOrNull() != null + val isCrossSigningTrusted = crossSigningInfo.getOrNull()?.isTrusted() == true + val allPrivateKeysKnown = pInfo.getOrNull()?.allKnown().orFalse() + + val keysBackupService = cryptoService().keysBackupService() + val currentBackupVersion = keysBackupService.currentBackupVersion + val megolmBackupAvailable = currentBackupVersion != null + val savedBackupKey = keysBackupService.getKeyBackupRecoveryKeyInfo() + + val megolmKeyKnown = savedBackupKey?.version == currentBackupVersion + SecretsSynchronisationInfo( + isBackupSetup = is4SSetup, + isCrossSigningEnabled = isCrossSigningEnabled, + isCrossSigningTrusted = isCrossSigningTrusted, + allPrivateKeysKnown = allPrivateKeysKnown, + megolmBackupAvailable = megolmBackupAvailable, + megolmSecretKnown = megolmKeyKnown, + isMegolmKeyIn4S = sharedSecretStorageService.isMegolmKeyInBackup() + ) + } + .distinctUntilChanged() +} diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorDataStore.kt b/vector/src/main/java/im/vector/app/features/settings/VectorDataStore.kt new file mode 100644 index 0000000000..74b3794b2c --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/VectorDataStore.kt @@ -0,0 +1,47 @@ +/* + * 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.settings + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.intPreferencesKey +import androidx.datastore.preferences.preferencesDataStore +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +private val Context.dataStore: DataStore by preferencesDataStore(name = "vector_settings") + +class VectorDataStore @Inject constructor( + private val context: Context +) { + + private val pushCounter = intPreferencesKey("push_counter") + + val pushCounterFlow: Flow = context.dataStore.data.map { preferences -> + preferences[pushCounter] ?: 0 + } + + suspend fun incrementPushCounter() { + context.dataStore.edit { settings -> + val currentCounterValue = settings[pushCounter] ?: 0 + settings[pushCounter] = currentCounterValue + 1 + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt index 259c3662fc..07cd9d6dac 100755 --- a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt @@ -44,6 +44,7 @@ class VectorPreferences @Inject constructor(private val context: Context) { const val SETTINGS_IDENTITY_SERVER_PREFERENCE_KEY = "SETTINGS_IDENTITY_SERVER_PREFERENCE_KEY" const val SETTINGS_APP_TERM_CONDITIONS_PREFERENCE_KEY = "SETTINGS_APP_TERM_CONDITIONS_PREFERENCE_KEY" const val SETTINGS_PRIVACY_POLICY_PREFERENCE_KEY = "SETTINGS_PRIVACY_POLICY_PREFERENCE_KEY" + const val SETTINGS_DISCOVERY_PREFERENCE_KEY = "SETTINGS_DISCOVERY_PREFERENCE_KEY" const val SETTINGS_NOTIFICATION_ADVANCED_PREFERENCE_KEY = "SETTINGS_NOTIFICATION_ADVANCED_PREFERENCE_KEY" const val SETTINGS_THIRD_PARTY_NOTICES_PREFERENCE_KEY = "SETTINGS_THIRD_PARTY_NOTICES_PREFERENCE_KEY" @@ -117,6 +118,7 @@ class VectorPreferences @Inject constructor(private val context: Context) { // notifications const val SETTINGS_ENABLE_ALL_NOTIF_PREFERENCE_KEY = "SETTINGS_ENABLE_ALL_NOTIF_PREFERENCE_KEY" const val SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY = "SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY" + const val SETTINGS_EMAIL_NOTIFICATION_CATEGORY_PREFERENCE_KEY = "SETTINGS_EMAIL_NOTIFICATION_CATEGORY_PREFERENCE_KEY" // public static final String SETTINGS_TURN_SCREEN_ON_PREFERENCE_KEY = "SETTINGS_TURN_SCREEN_ON_PREFERENCE_KEY"; const val SETTINGS_SYSTEM_CALL_NOTIFICATION_PREFERENCE_KEY = "SETTINGS_SYSTEM_CALL_NOTIFICATION_PREFERENCE_KEY" @@ -159,6 +161,7 @@ class VectorPreferences @Inject constructor(private val context: Context) { private const val SETTINGS_LABS_SHOW_HIDDEN_EVENTS_PREFERENCE_KEY = "SETTINGS_LABS_SHOW_HIDDEN_EVENTS_PREFERENCE_KEY" private const val SETTINGS_LABS_ENABLE_SWIPE_TO_REPLY = "SETTINGS_LABS_ENABLE_SWIPE_TO_REPLY" private const val SETTINGS_DEVELOPER_MODE_FAIL_FAST_PREFERENCE_KEY = "SETTINGS_DEVELOPER_MODE_FAIL_FAST_PREFERENCE_KEY" + private const val SETTINGS_DEVELOPER_MODE_SHOW_INFO_ON_SCREEN_KEY = "SETTINGS_DEVELOPER_MODE_SHOW_INFO_ON_SCREEN_KEY" // SETTINGS_LABS_HIDE_TECHNICAL_E2E_ERRORS private const val SETTINGS_LABS_SHOW_COMPLETE_HISTORY_IN_ENCRYPTED_ROOM = "SETTINGS_LABS_SHOW_COMPLETE_HISTORY_IN_ENCRYPTED_ROOM" @@ -312,6 +315,10 @@ class VectorPreferences @Inject constructor(private val context: Context) { return defaultPrefs.getBoolean(SETTINGS_DEVELOPER_MODE_PREFERENCE_KEY, false) } + fun developerShowDebugInfo(): Boolean { + return developerMode() && defaultPrefs.getBoolean(SETTINGS_DEVELOPER_MODE_SHOW_INFO_ON_SCREEN_KEY, false) + } + fun shouldShowHiddenEvents(): Boolean { return developerMode() && defaultPrefs.getBoolean(SETTINGS_LABS_SHOW_HIDDEN_EVENTS_PREFERENCE_KEY, false) } @@ -926,8 +933,8 @@ class VectorPreferences @Inject constructor(private val context: Context) { * Return true if Pin code is disabled, or if user set the settings to see full notification content */ fun useCompleteNotificationFormat(): Boolean { - return !useFlagPinCode() - || defaultPrefs.getBoolean(SETTINGS_SECURITY_USE_COMPLETE_NOTIFICATIONS_FLAG, true) + return !useFlagPinCode() || + defaultPrefs.getBoolean(SETTINGS_SECURITY_USE_COMPLETE_NOTIFICATIONS_FLAG, true) } fun backgroundSyncTimeOut(): Int { diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsActivity.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsActivity.kt index 4546313198..27fbacc362 100755 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsActivity.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsActivity.kt @@ -15,29 +15,37 @@ */ package im.vector.app.features.settings +import android.app.Activity import android.content.Context import android.content.Intent +import android.os.Bundle +import android.os.Parcelable +import android.util.Log import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat +import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R -import im.vector.app.core.di.ScreenComponent import im.vector.app.core.extensions.replaceFragment import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.databinding.ActivityVectorSettingsBinding import im.vector.app.features.discovery.DiscoverySettingsFragment +import im.vector.app.features.navigation.SettingsActivityPayload import im.vector.app.features.settings.devices.VectorSettingsDevicesFragment import im.vector.app.features.settings.notifications.VectorSettingsNotificationPreferenceFragment - +import im.vector.app.features.settings.threepids.ThreePidsSettingsFragment import org.matrix.android.sdk.api.failure.GlobalError import org.matrix.android.sdk.api.session.Session import timber.log.Timber import javax.inject.Inject +private const val KEY_ACTIVITY_PAYLOAD = "settings-activity-payload" + /** * Displays the client settings. */ +@AndroidEntryPoint class VectorSettingsActivity : VectorBaseActivity(), PreferenceFragmentCompat.OnPreferenceStartFragmentCallback, FragmentManager.OnBackStackChangedListener, @@ -55,36 +63,33 @@ class VectorSettingsActivity : VectorBaseActivity @Inject lateinit var session: Session - override fun injectWith(injector: ScreenComponent) { - injector.inject(this) - } - override fun initUiAndData() { configureToolbar(views.settingsToolbar) if (isFirstCreation()) { // display the fragment - when (intent.getIntExtra(EXTRA_DIRECT_ACCESS, EXTRA_DIRECT_ACCESS_ROOT)) { - EXTRA_DIRECT_ACCESS_GENERAL -> + + when (val payload = readPayload(SettingsActivityPayload.Root)) { + SettingsActivityPayload.General -> replaceFragment(R.id.vector_settings_page, VectorSettingsGeneralFragment::class.java, null, FRAGMENT_TAG) - EXTRA_DIRECT_ACCESS_ADVANCED_SETTINGS -> + SettingsActivityPayload.AdvancedSettings -> replaceFragment(R.id.vector_settings_page, VectorSettingsAdvancedSettingsFragment::class.java, null, FRAGMENT_TAG) - EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY -> + SettingsActivityPayload.SecurityPrivacy -> replaceFragment(R.id.vector_settings_page, VectorSettingsSecurityPrivacyFragment::class.java, null, FRAGMENT_TAG) - EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY_MANAGE_SESSIONS -> + SettingsActivityPayload.SecurityPrivacyManageSessions -> replaceFragment(R.id.vector_settings_page, VectorSettingsDevicesFragment::class.java, null, FRAGMENT_TAG) - EXTRA_DIRECT_ACCESS_NOTIFICATIONS -> { + SettingsActivityPayload.Notifications -> { requestHighlightPreferenceKeyOnResume(VectorPreferences.SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY) replaceFragment(R.id.vector_settings_page, VectorSettingsNotificationPreferenceFragment::class.java, null, FRAGMENT_TAG) } - EXTRA_DIRECT_ACCESS_DISCOVERY_SETTINGS -> { - replaceFragment(R.id.vector_settings_page, DiscoverySettingsFragment::class.java, null, FRAGMENT_TAG) + is SettingsActivityPayload.DiscoverySettings -> { + Log.e("!!!", "SettingsActivityPayload.DiscoverySettings : $payload") + replaceFragment(R.id.vector_settings_page, DiscoverySettingsFragment::class.java, payload, FRAGMENT_TAG) } - - else -> + else -> replaceFragment(R.id.vector_settings_page, VectorSettingsRootFragment::class.java, null, FRAGMENT_TAG) } } @@ -136,6 +141,10 @@ class VectorSettingsActivity : VectorBaseActivity return keyToHighlight } + override fun navigateToEmailAndPhoneNumbers() { + navigateTo(ThreePidsSettingsFragment::class.java) + } + override fun handleInvalidToken(globalError: GlobalError.InvalidToken) { if (ignoreInvalidTokenError) { Timber.w("Ignoring invalid token global error") @@ -144,19 +153,31 @@ class VectorSettingsActivity : VectorBaseActivity } } - fun navigateTo(fragmentClass: Class) { + fun navigateTo(fragmentClass: Class, arguments: Bundle? = null) { supportFragmentManager.beginTransaction() .setCustomAnimations(R.anim.right_in, R.anim.fade_out, R.anim.fade_in, R.anim.right_out) - .replace(R.id.vector_settings_page, fragmentClass, null) + .replace(R.id.vector_settings_page, fragmentClass, arguments) .addToBackStack(null) .commit() } companion object { - fun getIntent(context: Context, directAccess: Int) = Intent(context, VectorSettingsActivity::class.java) - .apply { putExtra(EXTRA_DIRECT_ACCESS, directAccess) } + fun getIntent(context: Context, directAccess: Int) = Companion.getIntent(context, when (directAccess) { + EXTRA_DIRECT_ACCESS_ROOT -> SettingsActivityPayload.Root + EXTRA_DIRECT_ACCESS_ADVANCED_SETTINGS -> SettingsActivityPayload.AdvancedSettings + EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY -> SettingsActivityPayload.SecurityPrivacy + EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY_MANAGE_SESSIONS -> SettingsActivityPayload.SecurityPrivacyManageSessions + EXTRA_DIRECT_ACCESS_GENERAL -> SettingsActivityPayload.General + EXTRA_DIRECT_ACCESS_NOTIFICATIONS -> SettingsActivityPayload.Notifications + EXTRA_DIRECT_ACCESS_DISCOVERY_SETTINGS -> SettingsActivityPayload.DiscoverySettings() + else -> { + Timber.w("Unknown directAccess: $directAccess defaulting to Root") + SettingsActivityPayload.Root + } + }) - private const val EXTRA_DIRECT_ACCESS = "EXTRA_DIRECT_ACCESS" + fun getIntent(context: Context, payload: SettingsActivityPayload) = Intent(context, VectorSettingsActivity::class.java) + .applyPayload(payload) const val EXTRA_DIRECT_ACCESS_ROOT = 0 const val EXTRA_DIRECT_ACCESS_ADVANCED_SETTINGS = 1 @@ -169,3 +190,11 @@ class VectorSettingsActivity : VectorBaseActivity private const val FRAGMENT_TAG = "VectorSettingsPreferencesFragment" } } + +private fun Activity.readPayload(default: T): T { + return intent.getParcelableExtra(KEY_ACTIVITY_PAYLOAD) ?: default +} + +private fun Intent.applyPayload(payload: T): Intent { + return putExtra(KEY_ACTIVITY_PAYLOAD, payload) +} diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsAdvancedSettingsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsAdvancedSettingsFragment.kt index 80c8dfa77d..68ce4e691c 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsAdvancedSettingsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsAdvancedSettingsFragment.kt @@ -20,6 +20,7 @@ import androidx.preference.Preference import androidx.preference.SeekBarPreference import im.vector.app.R import im.vector.app.core.platform.VectorBaseActivity +import im.vector.app.core.preference.VectorPreferenceCategory import im.vector.app.core.preference.VectorSwitchPreference import im.vector.app.features.rageshake.RageShake @@ -72,7 +73,7 @@ class VectorSettingsAdvancedSettingsFragment : VectorSettingsBaseFragment() { true } } else { - findPreference("SETTINGS_RAGE_SHAKE_CATEGORY_KEY")!!.isVisible = false + findPreference("SETTINGS_RAGE_SHAKE_CATEGORY_KEY")!!.isVisible = false } } } diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsBaseFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsBaseFragment.kt index efbd1cd1b4..bffabf2e93 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsBaseFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsBaseFragment.kt @@ -23,10 +23,8 @@ import androidx.annotation.CallSuper import androidx.preference.PreferenceFragmentCompat import com.google.android.material.dialog.MaterialAlertDialogBuilder import im.vector.app.R -import im.vector.app.core.di.DaggerScreenComponent -import im.vector.app.core.di.HasScreenInjector -import im.vector.app.core.di.ScreenComponent import im.vector.app.core.error.ErrorFormatter +import im.vector.app.core.extensions.singletonEntryPoint import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.utils.toast import io.reactivex.disposables.CompositeDisposable @@ -34,7 +32,7 @@ import io.reactivex.disposables.Disposable import org.matrix.android.sdk.api.session.Session import timber.log.Timber -abstract class VectorSettingsBaseFragment : PreferenceFragmentCompat(), HasScreenInjector { +abstract class VectorSettingsBaseFragment : PreferenceFragmentCompat() { val vectorActivity: VectorBaseActivity<*> by lazy { activity as VectorBaseActivity<*> @@ -45,7 +43,6 @@ abstract class VectorSettingsBaseFragment : PreferenceFragmentCompat(), HasScree // members protected lateinit var session: Session protected lateinit var errorFormatter: ErrorFormatter - private lateinit var screenComponent: ScreenComponent abstract val preferenceXmlRes: Int @@ -56,17 +53,10 @@ abstract class VectorSettingsBaseFragment : PreferenceFragmentCompat(), HasScree } override fun onAttach(context: Context) { - screenComponent = DaggerScreenComponent.factory().create(vectorActivity.getVectorComponent(), vectorActivity) + val singletonEntryPoint = context.singletonEntryPoint() super.onAttach(context) - session = screenComponent.activeSessionHolder().getActiveSession() - errorFormatter = screenComponent.errorFormatter() - injectWith(injector()) - } - - protected open fun injectWith(injector: ScreenComponent) = Unit - - override fun injector(): ScreenComponent { - return screenComponent + session = singletonEntryPoint.activeSessionHolder().getActiveSession() + errorFormatter = singletonEntryPoint.errorFormatter() } override fun onResume() { diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsFragmentInteractionListener.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsFragmentInteractionListener.kt index b815ce653d..ddfcc93287 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsFragmentInteractionListener.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsFragmentInteractionListener.kt @@ -20,4 +20,6 @@ interface VectorSettingsFragmentInteractionListener { fun requestHighlightPreferenceKeyOnResume(key: String?) fun requestedKeyToHighlight(): String? + + fun navigateToEmailAndPhoneNumbers() } diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsGeneralFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsGeneralFragment.kt index f40079c615..8d950b4e32 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsGeneralFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsGeneralFragment.kt @@ -38,6 +38,7 @@ import im.vector.app.R import im.vector.app.core.dialogs.GalleryOrCameraDialogHelper import im.vector.app.core.extensions.hideKeyboard import im.vector.app.core.extensions.hidePassword +import im.vector.app.core.extensions.toMvRxBundle import im.vector.app.core.intent.getFilenameFromUri import im.vector.app.core.platform.SimpleTextWatcher import im.vector.app.core.preference.UserAvatarPreference @@ -50,16 +51,22 @@ import im.vector.app.core.utils.toast import im.vector.app.databinding.DialogChangePasswordBinding import im.vector.app.features.MainActivity import im.vector.app.features.MainActivityArgs +import im.vector.app.features.discovery.DiscoverySettingsFragment +import im.vector.app.features.navigation.SettingsActivityPayload import im.vector.app.features.workers.signout.SignOutUiWorker -import io.reactivex.android.schedulers.AndroidSchedulers import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.distinctUntilChangedBy +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.matrix.android.sdk.api.failure.isInvalidPassword import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerConfig import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerService -import org.matrix.android.sdk.rx.rx -import org.matrix.android.sdk.rx.unwrap +import org.matrix.android.sdk.flow.flow +import org.matrix.android.sdk.flow.unwrap import java.io.File import java.util.UUID import javax.inject.Inject @@ -118,29 +125,29 @@ class VectorSettingsGeneralFragment @Inject constructor( } private fun observeUserAvatar() { - session.rx() + session.flow() .liveUser(session.myUserId) .unwrap() - .distinctUntilChanged { user -> user.avatarUrl } - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { mUserAvatarPreference.refreshAvatar(it) } - .disposeOnDestroyView() + .distinctUntilChangedBy { user -> user.avatarUrl } + .onEach { + mUserAvatarPreference.refreshAvatar(it) + } + .launchIn(viewLifecycleOwner.lifecycleScope) } private fun observeUserDisplayName() { - session.rx() + session.flow() .liveUser(session.myUserId) .unwrap() .map { it.displayName ?: "" } .distinctUntilChanged() - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { displayName -> + .onEach { displayName -> mDisplayNamePreference.let { it.summary = displayName it.text = displayName } } - .disposeOnDestroyView() + .launchIn(viewLifecycleOwner.lifecycleScope) } override fun bindPref() { @@ -173,6 +180,19 @@ class VectorSettingsGeneralFragment @Inject constructor( mPasswordPreference.isVisible = false } + val openDiscoveryScreenPreferenceClickListener = Preference.OnPreferenceClickListener { + (requireActivity() as VectorSettingsActivity).navigateTo( + DiscoverySettingsFragment::class.java, + SettingsActivityPayload.DiscoverySettings().toMvRxBundle() + ) + true + } + + val discoveryPreference = findPreference(VectorPreferences.SETTINGS_DISCOVERY_PREFERENCE_KEY)!! + discoveryPreference.onPreferenceClickListener = openDiscoveryScreenPreferenceClickListener + + mIdentityServerPreference.onPreferenceClickListener = openDiscoveryScreenPreferenceClickListener + // Advanced settings // user account diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsPreferencesFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsPreferencesFragment.kt index 212147e628..2a3ea799a5 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsPreferencesFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsPreferencesFragment.kt @@ -142,7 +142,7 @@ class VectorSettingsPreferencesFragment @Inject constructor( // Take photo or video updateTakePhotoOrVideoPreferenceSummary() takePhotoOrVideoPreference.onPreferenceClickListener = Preference.OnPreferenceClickListener { - PhotoOrVideoDialog(requireActivity(), vectorPreferences).showForSettings(object: PhotoOrVideoDialog.PhotoOrVideoDialogSettingsListener { + PhotoOrVideoDialog(requireActivity(), vectorPreferences).showForSettings(object : PhotoOrVideoDialog.PhotoOrVideoDialogSettingsListener { override fun onUpdated() { updateTakePhotoOrVideoPreferenceSummary() } diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsSecurityPrivacyFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsSecurityPrivacyFragment.kt index 0075be6e25..b622d8aab4 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsSecurityPrivacyFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsSecurityPrivacyFragment.kt @@ -58,17 +58,20 @@ import im.vector.app.features.pin.PinMode import im.vector.app.features.raw.wellknown.getElementWellknown import im.vector.app.features.raw.wellknown.isE2EByDefault import im.vector.app.features.themes.ThemeUtils -import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.Disposable +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import me.gujun.android.span.span import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.extensions.getFingerprintHumanReadable +import org.matrix.android.sdk.api.raw.RawService import org.matrix.android.sdk.internal.crypto.crosssigning.isVerified import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo import org.matrix.android.sdk.internal.crypto.model.rest.DevicesListResponse import org.matrix.android.sdk.rx.SecretsSynchronisationInfo -import org.matrix.android.sdk.rx.rx import javax.inject.Inject class VectorSettingsSecurityPrivacyFragment @Inject constructor( @@ -77,6 +80,7 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor( private val pinCodeStore: PinCodeStore, private val keysExporter: KeysExporter, private val keysImporter: KeysImporter, + private val rawService: RawService, private val navigator: Navigator ) : VectorSettingsBaseFragment() { @@ -144,19 +148,16 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor( // My device name may have been updated refreshMyDevice() refreshXSigningStatus() - session.rx().liveSecretSynchronisationInfo() - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { + session.liveSecretSynchronisationInfo() + .flowOn(Dispatchers.Main) + .onEach { refresh4SSection(it) refreshXSigningStatus() - }.also { - disposables.add(it) - } + }.launchIn(viewLifecycleOwner.lifecycleScope) lifecycleScope.launchWhenResumed { findPreference(VectorPreferences.SETTINGS_CRYPTOGRAPHY_HS_ADMIN_DISABLED_E2E_DEFAULT)?.isVisible = - vectorActivity.getVectorComponent() - .rawService() + rawService .getElementWellknown(session.sessionParams) ?.isE2EByDefault() == false } diff --git a/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountAction.kt b/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountAction.kt index bf62800d6a..0662afc700 100644 --- a/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountAction.kt +++ b/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountAction.kt @@ -21,7 +21,7 @@ import im.vector.app.core.platform.VectorViewModelAction sealed class DeactivateAccountAction : VectorViewModelAction { data class DeactivateAccount(val eraseAllData: Boolean) : DeactivateAccountAction() - object SsoAuthDone: DeactivateAccountAction() - data class PasswordAuthDone(val password: String): DeactivateAccountAction() - object ReAuthCancelled: DeactivateAccountAction() + object SsoAuthDone : DeactivateAccountAction() + data class PasswordAuthDone(val password: String) : DeactivateAccountAction() + object ReAuthCancelled : DeactivateAccountAction() } diff --git a/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountFragment.kt b/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountFragment.kt index 2cc80bfa23..5729e773b7 100644 --- a/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountFragment.kt @@ -34,12 +34,9 @@ import im.vector.app.features.MainActivityArgs import im.vector.app.features.auth.ReAuthActivity import im.vector.app.features.settings.VectorSettingsActivity import org.matrix.android.sdk.api.auth.data.LoginFlowTypes - import javax.inject.Inject -class DeactivateAccountFragment @Inject constructor( - val viewModelFactory: DeactivateAccountViewModel.Factory -) : VectorBaseFragment() { +class DeactivateAccountFragment @Inject constructor() : VectorBaseFragment() { private val viewModel: DeactivateAccountViewModel by fragmentViewModel() diff --git a/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountViewModel.kt index 80c64220c0..922435047f 100644 --- a/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountViewModel.kt @@ -15,26 +15,25 @@ */ package im.vector.app.features.settings.account.deactivation -import androidx.lifecycle.viewModelScope -import com.airbnb.mvrx.FragmentViewModelContext -import com.airbnb.mvrx.MvRxState -import com.airbnb.mvrx.MvRxViewModelFactory -import com.airbnb.mvrx.ViewModelContext +import com.airbnb.mvrx.MavericksState +import com.airbnb.mvrx.MavericksViewModelFactory import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import im.vector.app.features.auth.ReAuthActivity import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.auth.UIABaseAuth import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor +import org.matrix.android.sdk.api.auth.UserPasswordAuth +import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse import org.matrix.android.sdk.api.failure.isInvalidUIAAuth import org.matrix.android.sdk.api.session.Session -import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse import org.matrix.android.sdk.internal.crypto.crosssigning.fromBase64 import org.matrix.android.sdk.internal.crypto.model.rest.DefaultBaseAuth -import org.matrix.android.sdk.api.auth.UIABaseAuth -import org.matrix.android.sdk.api.auth.UserPasswordAuth import timber.log.Timber import kotlin.coroutines.Continuation import kotlin.coroutines.resume @@ -42,15 +41,15 @@ import kotlin.coroutines.resumeWithException data class DeactivateAccountViewState( val dummy: Boolean = false -) : MvRxState +) : MavericksState class DeactivateAccountViewModel @AssistedInject constructor(@Assisted private val initialState: DeactivateAccountViewState, - private val session: Session) - : VectorViewModel(initialState) { + private val session: Session) : + VectorViewModel(initialState) { @AssistedFactory - interface Factory { - fun create(initialState: DeactivateAccountViewState): DeactivateAccountViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: DeactivateAccountViewState): DeactivateAccountViewModel } var uiaContinuation: Continuation? = null @@ -114,12 +113,5 @@ class DeactivateAccountViewModel @AssistedInject constructor(@Assisted private v } } - companion object : MvRxViewModelFactory { - - @JvmStatic - override fun create(viewModelContext: ViewModelContext, state: DeactivateAccountViewState): DeactivateAccountViewModel? { - val fragment: DeactivateAccountFragment = (viewModelContext as FragmentViewModelContext).fragment() - return fragment.viewModelFactory.create(state) - } - } + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() } diff --git a/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsAction.kt b/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsAction.kt index 735c456ff9..6c2fc7ecc2 100644 --- a/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsAction.kt +++ b/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsAction.kt @@ -19,8 +19,8 @@ package im.vector.app.features.settings.crosssigning import im.vector.app.core.platform.VectorViewModelAction sealed class CrossSigningSettingsAction : VectorViewModelAction { - object InitializeCrossSigning: CrossSigningSettingsAction() - object SsoAuthDone: CrossSigningSettingsAction() - data class PasswordAuthDone(val password: String): CrossSigningSettingsAction() - object ReAuthCancelled: CrossSigningSettingsAction() + object InitializeCrossSigning : CrossSigningSettingsAction() + object SsoAuthDone : CrossSigningSettingsAction() + data class PasswordAuthDone(val password: String) : CrossSigningSettingsAction() + object ReAuthCancelled : CrossSigningSettingsAction() } diff --git a/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsFragment.kt index 17fbc333b5..fa061cdf8d 100644 --- a/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsFragment.kt @@ -35,7 +35,6 @@ import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.FragmentGenericRecyclerBinding import im.vector.app.features.auth.ReAuthActivity import org.matrix.android.sdk.api.auth.data.LoginFlowTypes - import javax.inject.Inject /** @@ -43,7 +42,6 @@ import javax.inject.Inject */ class CrossSigningSettingsFragment @Inject constructor( private val controller: CrossSigningSettingsController, - val viewModelFactory: CrossSigningSettingsViewModel.Factory ) : VectorBaseFragment(), CrossSigningSettingsController.InteractionListener { @@ -56,14 +54,14 @@ class CrossSigningSettingsFragment @Inject constructor( private val reAuthActivityResultLauncher = registerStartForActivityResult { activityResult -> if (activityResult.resultCode == Activity.RESULT_OK) { when (activityResult.data?.extras?.getString(ReAuthActivity.RESULT_FLOW_TYPE)) { - LoginFlowTypes.SSO -> { + LoginFlowTypes.SSO -> { viewModel.handle(CrossSigningSettingsAction.SsoAuthDone) } LoginFlowTypes.PASSWORD -> { val password = activityResult.data?.extras?.getString(ReAuthActivity.RESULT_VALUE) ?: "" viewModel.handle(CrossSigningSettingsAction.PasswordAuthDone(password)) } - else -> { + else -> { viewModel.handle(CrossSigningSettingsAction.ReAuthCancelled) } } @@ -79,7 +77,7 @@ class CrossSigningSettingsFragment @Inject constructor( setupRecyclerView() viewModel.observeViewEvents { event -> when (event) { - is CrossSigningSettingsViewEvents.Failure -> { + is CrossSigningSettingsViewEvents.Failure -> { MaterialAlertDialogBuilder(requireContext()) .setTitle(R.string.dialog_title_error) .setMessage(errorFormatter.toHumanReadable(event.throwable)) @@ -87,7 +85,7 @@ class CrossSigningSettingsFragment @Inject constructor( .show() Unit } - is CrossSigningSettingsViewEvents.RequestReAuth -> { + is CrossSigningSettingsViewEvents.RequestReAuth -> { ReAuthActivity.newIntent(requireContext(), event.registrationFlowResponse, event.lastErrorCode, @@ -99,7 +97,7 @@ class CrossSigningSettingsFragment @Inject constructor( views.waitingView.waitingView.isVisible = true views.waitingView.waitingStatusText.setTextOrHide(event.status) } - CrossSigningSettingsViewEvents.HideModalWaitingView -> { + CrossSigningSettingsViewEvents.HideModalWaitingView -> { views.waitingView.waitingView.isVisible = false } }.exhaustive diff --git a/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewModel.kt index 8bdf97b6ec..644b7f33dd 100644 --- a/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewModel.kt @@ -15,21 +15,20 @@ */ package im.vector.app.features.settings.crosssigning -import androidx.lifecycle.viewModelScope -import com.airbnb.mvrx.FragmentViewModelContext -import com.airbnb.mvrx.MvRxViewModelFactory -import com.airbnb.mvrx.ViewModelContext +import com.airbnb.mvrx.MavericksViewModelFactory import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import im.vector.app.R +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider import im.vector.app.features.auth.ReAuthActivity import im.vector.app.features.login.ReAuthHelper -import io.reactivex.Observable import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch import org.matrix.android.sdk.api.auth.UIABaseAuth import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor @@ -38,14 +37,11 @@ import org.matrix.android.sdk.api.auth.data.LoginFlowTypes import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse import org.matrix.android.sdk.api.auth.registration.nextUncompletedStage import org.matrix.android.sdk.api.session.Session -import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo -import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.flow.flow import org.matrix.android.sdk.internal.crypto.crosssigning.fromBase64 import org.matrix.android.sdk.internal.crypto.crosssigning.isVerified import org.matrix.android.sdk.internal.crypto.model.rest.DefaultBaseAuth -import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo import org.matrix.android.sdk.internal.util.awaitCallback -import org.matrix.android.sdk.rx.rx import timber.log.Timber import kotlin.coroutines.Continuation import kotlin.coroutines.resume @@ -59,13 +55,12 @@ class CrossSigningSettingsViewModel @AssistedInject constructor( ) : VectorViewModel(initialState) { init { - Observable.combineLatest, Optional, Pair, Optional>>( - session.rx().liveMyDevicesInfo(), - session.rx().liveCrossSigningInfo(session.myUserId), - { myDevicesInfo, mxCrossSigningInfo -> - myDevicesInfo to mxCrossSigningInfo - } - ) + combine( + session.flow().liveMyDevicesInfo(), + session.flow().liveCrossSigningInfo(session.myUserId) + ) { myDevicesInfo, mxCrossSigningInfo -> + myDevicesInfo to mxCrossSigningInfo + } .execute { data -> val crossSigningKeys = data.invoke()?.second?.getOrNull() val xSigningIsEnableInAccount = crossSigningKeys != null @@ -85,8 +80,8 @@ class CrossSigningSettingsViewModel @AssistedInject constructor( var pendingAuth: UIABaseAuth? = null @AssistedFactory - interface Factory { - fun create(initialState: CrossSigningSettingsViewState): CrossSigningSettingsViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: CrossSigningSettingsViewState): CrossSigningSettingsViewModel } override fun handle(action: CrossSigningSettingsAction) { @@ -102,8 +97,8 @@ class CrossSigningSettingsViewModel @AssistedInject constructor( errCode: String?, promise: Continuation) { Timber.d("## UIA : initializeCrossSigning UIA") - if (flowResponse.nextUncompletedStage() == LoginFlowTypes.PASSWORD - && reAuthHelper.data != null && errCode == null) { + if (flowResponse.nextUncompletedStage() == LoginFlowTypes.PASSWORD && + reAuthHelper.data != null && errCode == null) { UserPasswordAuth( session = null, user = session.myUserId, @@ -126,7 +121,7 @@ class CrossSigningSettingsViewModel @AssistedInject constructor( } Unit } - is CrossSigningSettingsAction.SsoAuthDone -> { + is CrossSigningSettingsAction.SsoAuthDone -> { Timber.d("## UIA - FallBack success") if (pendingAuth != null) { uiaContinuation?.resume(pendingAuth!!) @@ -134,7 +129,7 @@ class CrossSigningSettingsViewModel @AssistedInject constructor( uiaContinuation?.resumeWithException(IllegalArgumentException()) } } - is CrossSigningSettingsAction.PasswordAuthDone -> { + is CrossSigningSettingsAction.PasswordAuthDone -> { val decryptedPass = session.loadSecureSecret(action.password.fromBase64().inputStream(), ReAuthActivity.DEFAULT_RESULT_KEYSTORE_ALIAS) uiaContinuation?.resume( UserPasswordAuth( @@ -144,7 +139,7 @@ class CrossSigningSettingsViewModel @AssistedInject constructor( ) ) } - CrossSigningSettingsAction.ReAuthCancelled -> { + CrossSigningSettingsAction.ReAuthCancelled -> { Timber.d("## UIA - Reauth cancelled") _viewEvents.post(CrossSigningSettingsViewEvents.HideModalWaitingView) uiaContinuation?.resumeWithException(Exception()) @@ -159,12 +154,5 @@ class CrossSigningSettingsViewModel @AssistedInject constructor( _viewEvents.post(CrossSigningSettingsViewEvents.Failure(Exception(stringProvider.getString(R.string.failed_to_initialize_cross_signing)))) } - companion object : MvRxViewModelFactory { - - @JvmStatic - override fun create(viewModelContext: ViewModelContext, state: CrossSigningSettingsViewState): CrossSigningSettingsViewModel? { - val fragment: CrossSigningSettingsFragment = (viewModelContext as FragmentViewModelContext).fragment() - return fragment.viewModelFactory.create(state) - } - } + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() } diff --git a/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewState.kt b/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewState.kt index 8a371ada68..9e349253ca 100644 --- a/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewState.kt +++ b/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewState.kt @@ -16,7 +16,7 @@ package im.vector.app.features.settings.crosssigning -import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MavericksState import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo data class CrossSigningSettingsViewState( @@ -24,4 +24,4 @@ data class CrossSigningSettingsViewState( val xSigningIsEnableInAccount: Boolean = false, val xSigningKeysAreTrusted: Boolean = false, val xSigningKeyCanSign: Boolean = true -) : MvRxState +) : MavericksState diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/DeviceVerificationInfoBottomSheet.kt b/vector/src/main/java/im/vector/app/features/settings/devices/DeviceVerificationInfoBottomSheet.kt index 9bfd2df0d6..441a344660 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/DeviceVerificationInfoBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/DeviceVerificationInfoBottomSheet.kt @@ -21,11 +21,11 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.core.view.isVisible -import com.airbnb.mvrx.MvRx +import com.airbnb.mvrx.Mavericks import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.parentFragmentViewModel import com.airbnb.mvrx.withState -import im.vector.app.core.di.ScreenComponent +import dagger.hilt.android.AndroidEntryPoint import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.configureWith import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment @@ -39,6 +39,7 @@ data class DeviceVerificationInfoArgs( val deviceId: String ) : Parcelable +@AndroidEntryPoint class DeviceVerificationInfoBottomSheet : VectorBaseBottomSheetDialogFragment(), DeviceVerificationInfoBottomSheetController.Callback { @@ -47,12 +48,6 @@ class DeviceVerificationInfoBottomSheet : private val sharedViewModel: DevicesViewModel by parentFragmentViewModel(DevicesViewModel::class) - @Inject lateinit var deviceVerificationInfoViewModelFactory: DeviceVerificationInfoBottomSheetViewModel.Factory - - override fun injectWith(injector: ScreenComponent) { - injector.inject(this) - } - @Inject lateinit var controller: DeviceVerificationInfoBottomSheetController override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): BottomSheetGenericListWithTitleBinding { @@ -83,7 +78,7 @@ class DeviceVerificationInfoBottomSheet : fun newInstance(userId: String, deviceId: String): DeviceVerificationInfoBottomSheet { val args = Bundle() val parcelableArgs = DeviceVerificationInfoArgs(userId, deviceId) - args.putParcelable(MvRx.KEY_ARG, parcelableArgs) + args.putParcelable(Mavericks.KEY_ARG, parcelableArgs) return DeviceVerificationInfoBottomSheet().apply { arguments = args } } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/DeviceVerificationInfoBottomSheetController.kt b/vector/src/main/java/im/vector/app/features/settings/devices/DeviceVerificationInfoBottomSheetController.kt index d6ef31abf2..c109920cd6 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/DeviceVerificationInfoBottomSheetController.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/DeviceVerificationInfoBottomSheetController.kt @@ -33,8 +33,8 @@ import javax.inject.Inject class DeviceVerificationInfoBottomSheetController @Inject constructor( private val stringProvider: StringProvider, - private val colorProvider: ColorProvider) - : TypedEpoxyController() { + private val colorProvider: ColorProvider) : + TypedEpoxyController() { var callback: Callback? = null diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/DeviceVerificationInfoBottomSheetViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/DeviceVerificationInfoBottomSheetViewModel.kt index ee5b0a6092..3a944b5a71 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/DeviceVerificationInfoBottomSheetViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/DeviceVerificationInfoBottomSheetViewModel.kt @@ -15,30 +15,33 @@ */ package im.vector.app.features.settings.devices -import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.Loading -import com.airbnb.mvrx.MvRxViewModelFactory -import com.airbnb.mvrx.ViewModelContext +import com.airbnb.mvrx.MavericksViewModelFactory import dagger.assisted.Assisted -import dagger.assisted.AssistedInject import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.platform.EmptyAction import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel +import kotlinx.coroutines.flow.map import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.flow.flow import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo -import org.matrix.android.sdk.rx.rx class DeviceVerificationInfoBottomSheetViewModel @AssistedInject constructor(@Assisted initialState: DeviceVerificationInfoBottomSheetViewState, - @Assisted val deviceId: String, val session: Session ) : VectorViewModel(initialState) { @AssistedFactory - interface Factory { - fun create(initialState: DeviceVerificationInfoBottomSheetViewState, deviceId: String): DeviceVerificationInfoBottomSheetViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: DeviceVerificationInfoBottomSheetViewState): DeviceVerificationInfoBottomSheetViewModel } + companion object : MavericksViewModelFactory + by hiltMavericksViewModelFactory() + init { setState { @@ -48,7 +51,7 @@ class DeviceVerificationInfoBottomSheetViewModel @AssistedInject constructor(@As isRecoverySetup = session.sharedSecretStorageService.isRecoverySetup() ) } - session.rx().liveCrossSigningInfo(session.myUserId) + session.flow().liveCrossSigningInfo(session.myUserId) .execute { copy( hasAccountCrossSigning = it.invoke()?.getOrNull() != null, @@ -56,9 +59,9 @@ class DeviceVerificationInfoBottomSheetViewModel @AssistedInject constructor(@As ) } - session.rx().liveUserCryptoDevices(session.myUserId) + session.flow().liveUserCryptoDevices(session.myUserId) .map { list -> - list.firstOrNull { it.deviceId == deviceId } + list.firstOrNull { it.deviceId == initialState.deviceId } } .execute { copy( @@ -67,7 +70,7 @@ class DeviceVerificationInfoBottomSheetViewModel @AssistedInject constructor(@As ) } - session.rx().liveUserCryptoDevices(session.myUserId) + session.flow().liveUserCryptoDevices(session.myUserId) .map { it.size } .execute { copy( @@ -79,26 +82,15 @@ class DeviceVerificationInfoBottomSheetViewModel @AssistedInject constructor(@As copy(deviceInfo = Loading()) } - session.rx().liveMyDevicesInfo() + session.flow().liveMyDevicesInfo() .map { devices -> - devices.firstOrNull { it.deviceId == deviceId } ?: DeviceInfo(deviceId = deviceId) + devices.firstOrNull { it.deviceId == initialState.deviceId } ?: DeviceInfo(deviceId = initialState.deviceId) } .execute { copy(deviceInfo = it) } } - companion object : MvRxViewModelFactory { - - @JvmStatic - override fun create(viewModelContext: ViewModelContext, state: DeviceVerificationInfoBottomSheetViewState) - : DeviceVerificationInfoBottomSheetViewModel? { - val fragment: DeviceVerificationInfoBottomSheet = (viewModelContext as FragmentViewModelContext).fragment() - val args = viewModelContext.args() - return fragment.deviceVerificationInfoViewModelFactory.create(state, args.deviceId) - } - } - override fun handle(action: EmptyAction) { } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/DeviceVerificationInfoBottomSheetViewState.kt b/vector/src/main/java/im/vector/app/features/settings/devices/DeviceVerificationInfoBottomSheetViewState.kt index a736b0442c..32927ca068 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/DeviceVerificationInfoBottomSheetViewState.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/DeviceVerificationInfoBottomSheetViewState.kt @@ -17,12 +17,13 @@ package im.vector.app.features.settings.devices import com.airbnb.mvrx.Async -import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.Uninitialized import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo data class DeviceVerificationInfoBottomSheetViewState( + val deviceId: String, val cryptoDeviceInfo: Async = Uninitialized, val deviceInfo: Async = Uninitialized, val hasAccountCrossSigning: Boolean = false, @@ -30,8 +31,9 @@ data class DeviceVerificationInfoBottomSheetViewState( val isMine: Boolean = false, val hasOtherSessions: Boolean = false, val isRecoverySetup: Boolean = false -) : MvRxState { +) : MavericksState { - val canVerifySession: Boolean - get() = hasOtherSessions || isRecoverySetup + constructor(args: DeviceVerificationInfoArgs) : this(deviceId = args.deviceId) + + val canVerifySession = hasOtherSessions || isRecoverySetup } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/DevicesAction.kt b/vector/src/main/java/im/vector/app/features/settings/devices/DevicesAction.kt index 46a476c270..e402982d97 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/DevicesAction.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/DevicesAction.kt @@ -22,6 +22,7 @@ import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo sealed class DevicesAction : VectorViewModelAction { object Refresh : DevicesAction() data class Delete(val deviceId: String) : DevicesAction() + // data class Password(val password: String) : DevicesAction() data class Rename(val deviceId: String, val newName: String) : DevicesAction() @@ -31,7 +32,7 @@ sealed class DevicesAction : VectorViewModelAction { object CompleteSecurity : DevicesAction() data class MarkAsManuallyVerified(val cryptoDeviceInfo: CryptoDeviceInfo) : DevicesAction() - object SsoAuthDone: DevicesAction() - data class PasswordAuthDone(val password: String): DevicesAction() - object ReAuthCancelled: DevicesAction() + object SsoAuthDone : DevicesAction() + data class PasswordAuthDone(val password: String) : DevicesAction() + object ReAuthCancelled : DevicesAction() } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewEvents.kt b/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewEvents.kt index 8535c698a7..9e1f83582f 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewEvents.kt @@ -18,8 +18,8 @@ package im.vector.app.features.settings.devices import im.vector.app.core.platform.VectorViewEvents -import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse +import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo @@ -28,6 +28,7 @@ import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo */ sealed class DevicesViewEvents : VectorViewEvents { data class Loading(val message: CharSequence? = null) : DevicesViewEvents() + // object HideLoading : DevicesViewEvents() data class Failure(val throwable: Throwable) : DevicesViewEvents() diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt index c48b08e806..e8300a1097 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt @@ -16,49 +16,53 @@ package im.vector.app.features.settings.devices -import androidx.lifecycle.viewModelScope import com.airbnb.mvrx.Async import com.airbnb.mvrx.Fail -import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.Loading -import com.airbnb.mvrx.MvRxState -import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.MavericksState +import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized -import com.airbnb.mvrx.ViewModelContext import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import im.vector.app.R +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider import im.vector.app.features.auth.ReAuthActivity import im.vector.app.features.login.ReAuthHelper -import io.reactivex.Observable import io.reactivex.subjects.PublishSubject import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.sample import kotlinx.coroutines.launch import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.NoOpMatrixCallback +import org.matrix.android.sdk.api.auth.UIABaseAuth import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor +import org.matrix.android.sdk.api.auth.UserPasswordAuth import org.matrix.android.sdk.api.auth.data.LoginFlowTypes +import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse +import org.matrix.android.sdk.api.auth.registration.nextUncompletedStage import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod import org.matrix.android.sdk.api.session.crypto.verification.VerificationService import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState -import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse -import org.matrix.android.sdk.api.auth.registration.nextUncompletedStage +import org.matrix.android.sdk.flow.flow import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustLevel import org.matrix.android.sdk.internal.crypto.crosssigning.fromBase64 import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.internal.crypto.model.rest.DefaultBaseAuth import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo -import org.matrix.android.sdk.api.auth.UIABaseAuth -import org.matrix.android.sdk.api.auth.UserPasswordAuth import org.matrix.android.sdk.internal.util.awaitCallback -import org.matrix.android.sdk.rx.rx import timber.log.Timber import java.util.concurrent.TimeUnit import javax.net.ssl.HttpsURLConnection @@ -75,7 +79,7 @@ data class DevicesViewState( val request: Async = Uninitialized, val hasAccountCrossSigning: Boolean = false, val accountCrossSigningIsTrusted: Boolean = false -) : MvRxState +) : MavericksState data class DeviceFullInfo( val deviceInfo: DeviceInfo, @@ -93,18 +97,11 @@ class DevicesViewModel @AssistedInject constructor( var pendingAuth: UIABaseAuth? = null @AssistedFactory - interface Factory { - fun create(initialState: DevicesViewState): DevicesViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: DevicesViewState): DevicesViewModel } - companion object : MvRxViewModelFactory { - - @JvmStatic - override fun create(viewModelContext: ViewModelContext, state: DevicesViewState): DevicesViewModel? { - val fragment: VectorSettingsDevicesFragment = (viewModelContext as FragmentViewModelContext).fragment() - return fragment.devicesViewModelFactory.create(state) - } - } + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() private val refreshPublisher: PublishSubject = PublishSubject.create() @@ -118,18 +115,17 @@ class DevicesViewModel @AssistedInject constructor( ) } - Observable.combineLatest, List, List>( - session.rx().liveUserCryptoDevices(session.myUserId), - session.rx().liveMyDevicesInfo(), - { cryptoList, infoList -> - infoList - .sortedByDescending { it.lastSeenTs } - .map { deviceInfo -> - val cryptoDeviceInfo = cryptoList.firstOrNull { it.deviceId == deviceInfo.deviceId } - DeviceFullInfo(deviceInfo, cryptoDeviceInfo) - } - } - ) + combine( + session.flow().liveUserCryptoDevices(session.myUserId), + session.flow().liveMyDevicesInfo() + ) { cryptoList, infoList -> + infoList + .sortedByDescending { it.lastSeenTs } + .map { deviceInfo -> + val cryptoDeviceInfo = cryptoList.firstOrNull { it.deviceId == deviceInfo.deviceId } + DeviceFullInfo(deviceInfo, cryptoDeviceInfo) + } + } .distinctUntilChanged() .execute { async -> copy( @@ -137,7 +133,7 @@ class DevicesViewModel @AssistedInject constructor( ) } - session.rx().liveCrossSigningInfo(session.myUserId) + session.flow().liveCrossSigningInfo(session.myUserId) .execute { copy( hasAccountCrossSigning = it.invoke()?.getOrNull() != null, @@ -146,24 +142,24 @@ class DevicesViewModel @AssistedInject constructor( } session.cryptoService().verificationService().addListener(this) -// session.rx().liveMyDeviceInfo() +// session.flow().liveMyDeviceInfo() // .execute { // copy( // devices = it // ) // } - session.rx().liveUserCryptoDevices(session.myUserId) + session.flow().liveUserCryptoDevices(session.myUserId) .map { it.size } .distinctUntilChanged() - .throttleLast(5_000, TimeUnit.MILLISECONDS) - .subscribe { + .sample(5_000) + .onEach { // If we have a new crypto device change, we might want to trigger refresh of device info session.cryptoService().fetchDevicesList(NoOpMatrixCallback()) } - .disposeOnClear() + .launchIn(viewModelScope) -// session.rx().liveUserCryptoDevices(session.myUserId) +// session.flow().liveUserCryptoDevices(session.myUserId) // .execute { // copy( // cryptoDevices = it diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/VectorSettingsDevicesFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/VectorSettingsDevicesFragment.kt index 5af886b265..531e9a944b 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/VectorSettingsDevicesFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/VectorSettingsDevicesFragment.kt @@ -40,7 +40,6 @@ import im.vector.app.databinding.FragmentGenericRecyclerBinding import im.vector.app.features.auth.ReAuthActivity import im.vector.app.features.crypto.verification.VerificationBottomSheet import org.matrix.android.sdk.api.auth.data.LoginFlowTypes - import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo import javax.inject.Inject @@ -48,7 +47,6 @@ import javax.inject.Inject * Display the list of the user's device */ class VectorSettingsDevicesFragment @Inject constructor( - val devicesViewModelFactory: DevicesViewModel.Factory, private val devicesController: DevicesController ) : VectorBaseFragment(), DevicesController.Callback { diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/AccountDataFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/AccountDataFragment.kt index a55032e44a..a586e14d99 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devtools/AccountDataFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devtools/AccountDataFragment.kt @@ -31,14 +31,12 @@ import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.resources.ColorProvider import im.vector.app.core.utils.createJSonViewerStyleProvider import im.vector.app.databinding.FragmentGenericRecyclerBinding - import org.billcarsonfr.jsonviewer.JSonViewerDialog import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent import org.matrix.android.sdk.internal.di.MoshiProvider import javax.inject.Inject class AccountDataFragment @Inject constructor( - val viewModelFactory: AccountDataViewModel.Factory, private val epoxyController: AccountDataEpoxyController, private val colorProvider: ColorProvider ) : VectorBaseFragment(), diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/AccountDataViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/AccountDataViewModel.kt index d50caea579..6289699687 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devtools/AccountDataViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devtools/AccountDataViewModel.kt @@ -16,34 +16,33 @@ package im.vector.app.features.settings.devtools -import androidx.lifecycle.viewModelScope import com.airbnb.mvrx.Async -import com.airbnb.mvrx.FragmentViewModelContext -import com.airbnb.mvrx.MvRxState -import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.MavericksState +import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.Uninitialized -import com.airbnb.mvrx.ViewModelContext import dagger.assisted.Assisted -import dagger.assisted.AssistedInject import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel import kotlinx.coroutines.launch import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent -import org.matrix.android.sdk.rx.rx +import org.matrix.android.sdk.flow.flow data class AccountDataViewState( val accountData: Async> = Uninitialized -) : MvRxState +) : MavericksState class AccountDataViewModel @AssistedInject constructor(@Assisted initialState: AccountDataViewState, - private val session: Session) - : VectorViewModel(initialState) { + private val session: Session) : + VectorViewModel(initialState) { init { - session.rx().liveUserAccountData(emptySet()) + session.flow().liveUserAccountData(emptySet()) .execute { copy(accountData = it) } @@ -62,16 +61,9 @@ class AccountDataViewModel @AssistedInject constructor(@Assisted initialState: A } @AssistedFactory - interface Factory { - fun create(initialState: AccountDataViewState): AccountDataViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: AccountDataViewState): AccountDataViewModel } - companion object : MvRxViewModelFactory { - - @JvmStatic - override fun create(viewModelContext: ViewModelContext, state: AccountDataViewState): AccountDataViewModel? { - val fragment: AccountDataFragment = (viewModelContext as FragmentViewModelContext).fragment() - return fragment.viewModelFactory.create(state) - } - } + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() } diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsPaperTrailFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsPaperTrailFragment.kt index 6a0b13fe87..83740c5018 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsPaperTrailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsPaperTrailFragment.kt @@ -29,13 +29,11 @@ import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.resources.ColorProvider import im.vector.app.core.utils.createJSonViewerStyleProvider import im.vector.app.databinding.FragmentGenericRecyclerBinding - import org.billcarsonfr.jsonviewer.JSonViewerDialog import org.matrix.android.sdk.api.session.events.model.Event import javax.inject.Inject class GossipingEventsPaperTrailFragment @Inject constructor( - val viewModelFactory: GossipingEventsPaperTrailViewModel.Factory, private val epoxyController: GossipingTrailPagedEpoxyController, private val colorProvider: ColorProvider ) : VectorBaseFragment(), diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsPaperTrailViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsPaperTrailViewModel.kt index 325538ee5e..dde032d303 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsPaperTrailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsPaperTrailViewModel.kt @@ -16,31 +16,31 @@ package im.vector.app.features.settings.devtools +import androidx.lifecycle.asFlow import androidx.paging.PagedList import com.airbnb.mvrx.Async -import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.Loading -import com.airbnb.mvrx.MvRxState -import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.MavericksState +import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.Uninitialized -import com.airbnb.mvrx.ViewModelContext import dagger.assisted.Assisted -import dagger.assisted.AssistedInject import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory 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.session.Session import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.rx.asObservable data class GossipingEventsPaperTrailState( val events: Async> = Uninitialized -) : MvRxState +) : MavericksState class GossipingEventsPaperTrailViewModel @AssistedInject constructor(@Assisted initialState: GossipingEventsPaperTrailState, - private val session: Session) - : VectorViewModel(initialState) { + private val session: Session) : + VectorViewModel(initialState) { init { refresh() @@ -50,7 +50,8 @@ class GossipingEventsPaperTrailViewModel @AssistedInject constructor(@Assisted i setState { copy(events = Loading()) } - session.cryptoService().getGossipingEventsTrail().asObservable() + session.cryptoService().getGossipingEventsTrail() + .asFlow() .execute { copy(events = it) } @@ -59,17 +60,9 @@ class GossipingEventsPaperTrailViewModel @AssistedInject constructor(@Assisted i override fun handle(action: EmptyAction) {} @AssistedFactory - interface Factory { - fun create(initialState: GossipingEventsPaperTrailState): GossipingEventsPaperTrailViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: GossipingEventsPaperTrailState): GossipingEventsPaperTrailViewModel } - companion object : MvRxViewModelFactory { - - @JvmStatic - override fun create(viewModelContext: ViewModelContext, state: GossipingEventsPaperTrailState): GossipingEventsPaperTrailViewModel? { - val fragment: GossipingEventsPaperTrailFragment = (viewModelContext as FragmentViewModelContext).fragment() - - return fragment.viewModelFactory.create(state) - } - } + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() } diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/IncomingKeyRequestListFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/IncomingKeyRequestListFragment.kt index 91e770babf..ac4bef9c94 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devtools/IncomingKeyRequestListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devtools/IncomingKeyRequestListFragment.kt @@ -27,11 +27,9 @@ import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.configureWith import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.FragmentGenericRecyclerBinding - import javax.inject.Inject class IncomingKeyRequestListFragment @Inject constructor( - val viewModelFactory: KeyRequestListViewModel.Factory, private val epoxyController: IncomingKeyRequestPagedController ) : VectorBaseFragment() { diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestListViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestListViewModel.kt index c0a791233f..197a72cb05 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestListViewModel.kt @@ -16,17 +16,17 @@ package im.vector.app.features.settings.devtools -import androidx.lifecycle.viewModelScope +import androidx.lifecycle.asFlow import androidx.paging.PagedList import com.airbnb.mvrx.Async -import com.airbnb.mvrx.FragmentViewModelContext -import com.airbnb.mvrx.MvRxState -import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.MavericksState +import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.Uninitialized -import com.airbnb.mvrx.ViewModelContext import dagger.assisted.Assisted -import dagger.assisted.AssistedInject import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.platform.EmptyAction import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel @@ -34,16 +34,15 @@ import kotlinx.coroutines.launch import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.internal.crypto.IncomingRoomKeyRequest import org.matrix.android.sdk.internal.crypto.OutgoingRoomKeyRequest -import org.matrix.android.sdk.rx.asObservable data class KeyRequestListViewState( val incomingRequests: Async> = Uninitialized, val outgoingRoomKeyRequests: Async> = Uninitialized -) : MvRxState +) : MavericksState class KeyRequestListViewModel @AssistedInject constructor(@Assisted initialState: KeyRequestListViewState, - private val session: Session) - : VectorViewModel(initialState) { + private val session: Session) : + VectorViewModel(initialState) { init { refresh() @@ -51,13 +50,13 @@ class KeyRequestListViewModel @AssistedInject constructor(@Assisted initialState fun refresh() { viewModelScope.launch { - session.cryptoService().getOutgoingRoomKeyRequestsPaged().asObservable() + session.cryptoService().getOutgoingRoomKeyRequestsPaged().asFlow() .execute { copy(outgoingRoomKeyRequests = it) } session.cryptoService().getIncomingRoomKeyRequestsPaged() - .asObservable() + .asFlow() .execute { copy(incomingRequests = it) } @@ -67,19 +66,9 @@ class KeyRequestListViewModel @AssistedInject constructor(@Assisted initialState override fun handle(action: EmptyAction) {} @AssistedFactory - interface Factory { - fun create(initialState: KeyRequestListViewState): KeyRequestListViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: KeyRequestListViewState): KeyRequestListViewModel } - companion object : MvRxViewModelFactory { - - @JvmStatic - override fun create(viewModelContext: ViewModelContext, state: KeyRequestListViewState): KeyRequestListViewModel? { - val context = viewModelContext as FragmentViewModelContext - val factory = (context.fragment as? IncomingKeyRequestListFragment)?.viewModelFactory - ?: (context.fragment as? OutgoingKeyRequestListFragment)?.viewModelFactory - - return factory?.create(state) - } - } + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() } diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestViewModel.kt index e7a56ef9df..f480eb2db8 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestViewModel.kt @@ -17,19 +17,18 @@ package im.vector.app.features.settings.devtools import android.net.Uri -import androidx.lifecycle.viewModelScope import com.airbnb.mvrx.Async import com.airbnb.mvrx.Fail -import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.Loading -import com.airbnb.mvrx.MvRxState -import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.MavericksState +import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized -import com.airbnb.mvrx.ViewModelContext import dagger.assisted.Assisted -import dagger.assisted.AssistedInject import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewEvents import im.vector.app.core.platform.VectorViewModel @@ -48,26 +47,19 @@ sealed class KeyRequestEvents : VectorViewEvents { data class KeyRequestViewState( val exporting: Async = Uninitialized -) : MvRxState +) : MavericksState class KeyRequestViewModel @AssistedInject constructor( @Assisted initialState: KeyRequestViewState, - private val session: Session) - : VectorViewModel(initialState) { + private val session: Session) : + VectorViewModel(initialState) { @AssistedFactory - interface Factory { - fun create(initialState: KeyRequestViewState): KeyRequestViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: KeyRequestViewState): KeyRequestViewModel } - companion object : MvRxViewModelFactory { - - @JvmStatic - override fun create(viewModelContext: ViewModelContext, state: KeyRequestViewState): KeyRequestViewModel? { - val fragment: KeyRequestsFragment = (viewModelContext as FragmentViewModelContext).fragment() - return fragment.viewModelFactory.create(state) - } - } + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() override fun handle(action: KeyRequestAction) { when (action) { diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestsFragment.kt index 7bee0e283c..d807fc620a 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestsFragment.kt @@ -38,12 +38,10 @@ import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.utils.selectTxtFileToWrite import im.vector.app.databinding.FragmentDevtoolKeyrequestsBinding - import org.matrix.android.sdk.api.extensions.tryOrNull import javax.inject.Inject -class KeyRequestsFragment @Inject constructor( - val viewModelFactory: KeyRequestViewModel.Factory) : VectorBaseFragment() { +class KeyRequestsFragment @Inject constructor() : VectorBaseFragment() { override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentDevtoolKeyrequestsBinding { return FragmentDevtoolKeyrequestsBinding.inflate(inflater, container, false) diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/OutgoingKeyRequestListFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/OutgoingKeyRequestListFragment.kt index 77cddfff8c..0483d5fb4d 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devtools/OutgoingKeyRequestListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devtools/OutgoingKeyRequestListFragment.kt @@ -27,10 +27,8 @@ import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.configureWith import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.FragmentGenericRecyclerBinding - import javax.inject.Inject class OutgoingKeyRequestListFragment @Inject constructor( - val viewModelFactory: KeyRequestListViewModel.Factory, private val epoxyController: OutgoingKeyRequestPagedController ) : VectorBaseFragment() { diff --git a/vector/src/main/java/im/vector/app/features/settings/homeserver/HomeServerSettingsViewState.kt b/vector/src/main/java/im/vector/app/features/settings/homeserver/HomeServerSettingsViewState.kt index 87ad637ca5..3acd79d768 100644 --- a/vector/src/main/java/im/vector/app/features/settings/homeserver/HomeServerSettingsViewState.kt +++ b/vector/src/main/java/im/vector/app/features/settings/homeserver/HomeServerSettingsViewState.kt @@ -17,7 +17,7 @@ package im.vector.app.features.settings.homeserver import com.airbnb.mvrx.Async -import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.Uninitialized import org.matrix.android.sdk.api.federation.FederationVersion import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities @@ -27,4 +27,4 @@ data class HomeServerSettingsViewState( val homeserverClientServerApiUrl: String = "", val homeServerCapabilities: HomeServerCapabilities = HomeServerCapabilities(), val federationVersion: Async = Uninitialized -) : MvRxState +) : MavericksState diff --git a/vector/src/main/java/im/vector/app/features/settings/homeserver/HomeserverSettingsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/homeserver/HomeserverSettingsFragment.kt index 20541a1ebb..28bce90424 100644 --- a/vector/src/main/java/im/vector/app/features/settings/homeserver/HomeserverSettingsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/homeserver/HomeserverSettingsFragment.kt @@ -34,7 +34,6 @@ import javax.inject.Inject * Display some information about the homeserver */ class HomeserverSettingsFragment @Inject constructor( - val homeserverSettingsViewModelFactory: HomeserverSettingsViewModel.Factory, private val homeserverSettingsController: HomeserverSettingsController ) : VectorBaseFragment(), HomeserverSettingsController.Callback { diff --git a/vector/src/main/java/im/vector/app/features/settings/homeserver/HomeserverSettingsViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/homeserver/HomeserverSettingsViewModel.kt index 623ac37aa4..fab563b49e 100644 --- a/vector/src/main/java/im/vector/app/features/settings/homeserver/HomeserverSettingsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/homeserver/HomeserverSettingsViewModel.kt @@ -16,16 +16,15 @@ package im.vector.app.features.settings.homeserver -import androidx.lifecycle.viewModelScope import com.airbnb.mvrx.Fail -import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.Loading -import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.Success -import com.airbnb.mvrx.ViewModelContext import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel import kotlinx.coroutines.launch @@ -37,18 +36,11 @@ class HomeserverSettingsViewModel @AssistedInject constructor( ) : VectorViewModel(initialState) { @AssistedFactory - interface Factory { - fun create(initialState: HomeServerSettingsViewState): HomeserverSettingsViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: HomeServerSettingsViewState): HomeserverSettingsViewModel } - companion object : MvRxViewModelFactory { - - @JvmStatic - override fun create(viewModelContext: ViewModelContext, state: HomeServerSettingsViewState): HomeserverSettingsViewModel? { - val fragment: HomeserverSettingsFragment = (viewModelContext as FragmentViewModelContext).fragment() - return fragment.homeserverSettingsViewModelFactory.create(state) - } - } + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() init { setState { diff --git a/vector/src/main/java/im/vector/app/features/settings/ignored/IgnoredUsersViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/ignored/IgnoredUsersViewModel.kt index aa00f71542..b2a7b2cbd1 100644 --- a/vector/src/main/java/im/vector/app/features/settings/ignored/IgnoredUsersViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/ignored/IgnoredUsersViewModel.kt @@ -16,59 +16,51 @@ package im.vector.app.features.settings.ignored -import androidx.lifecycle.viewModelScope import com.airbnb.mvrx.Async import com.airbnb.mvrx.Fail -import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.Loading -import com.airbnb.mvrx.MvRxState -import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.MavericksState +import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized -import com.airbnb.mvrx.ViewModelContext import dagger.assisted.Assisted -import dagger.assisted.AssistedInject import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModelAction import kotlinx.coroutines.launch import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.user.model.User -import org.matrix.android.sdk.rx.rx +import org.matrix.android.sdk.flow.flow data class IgnoredUsersViewState( val ignoredUsers: List = emptyList(), val unIgnoreRequest: Async = Uninitialized -) : MvRxState +) : MavericksState sealed class IgnoredUsersAction : VectorViewModelAction { data class UnIgnore(val userId: String) : IgnoredUsersAction() } class IgnoredUsersViewModel @AssistedInject constructor(@Assisted initialState: IgnoredUsersViewState, - private val session: Session) - : VectorViewModel(initialState) { + private val session: Session) : + VectorViewModel(initialState) { @AssistedFactory - interface Factory { - fun create(initialState: IgnoredUsersViewState): IgnoredUsersViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: IgnoredUsersViewState): IgnoredUsersViewModel } - companion object : MvRxViewModelFactory { - - @JvmStatic - override fun create(viewModelContext: ViewModelContext, state: IgnoredUsersViewState): IgnoredUsersViewModel? { - val ignoredUsersFragment: VectorSettingsIgnoredUsersFragment = (viewModelContext as FragmentViewModelContext).fragment() - return ignoredUsersFragment.ignoredUsersViewModelFactory.create(state) - } - } + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() init { observeIgnoredUsers() } private fun observeIgnoredUsers() { - session.rx() + session.flow() .liveIgnoredUsers() .execute { async -> copy( diff --git a/vector/src/main/java/im/vector/app/features/settings/ignored/VectorSettingsIgnoredUsersFragment.kt b/vector/src/main/java/im/vector/app/features/settings/ignored/VectorSettingsIgnoredUsersFragment.kt index fe09008cdd..509014492d 100644 --- a/vector/src/main/java/im/vector/app/features/settings/ignored/VectorSettingsIgnoredUsersFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/ignored/VectorSettingsIgnoredUsersFragment.kt @@ -33,11 +33,9 @@ import im.vector.app.core.extensions.configureWith import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.FragmentGenericRecyclerBinding - import javax.inject.Inject class VectorSettingsIgnoredUsersFragment @Inject constructor( - val ignoredUsersViewModelFactory: IgnoredUsersViewModel.Factory, private val ignoredUsersController: IgnoredUsersController ) : VectorBaseFragment(), IgnoredUsersController.Callback { diff --git a/vector/src/main/java/im/vector/app/features/settings/locale/LocalePickerFragment.kt b/vector/src/main/java/im/vector/app/features/settings/locale/LocalePickerFragment.kt index 45fa6b735f..601574c908 100644 --- a/vector/src/main/java/im/vector/app/features/settings/locale/LocalePickerFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/locale/LocalePickerFragment.kt @@ -30,15 +30,12 @@ import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.restart import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.FragmentLocalePickerBinding - import java.util.Locale import javax.inject.Inject class LocalePickerFragment @Inject constructor( - private val viewModelFactory: LocalePickerViewModel.Factory, private val controller: LocalePickerController ) : VectorBaseFragment(), - LocalePickerViewModel.Factory by viewModelFactory, LocalePickerController.Listener { private val viewModel: LocalePickerViewModel by fragmentViewModel() diff --git a/vector/src/main/java/im/vector/app/features/settings/locale/LocalePickerViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/locale/LocalePickerViewModel.kt index 2e59b0ef7d..d6b35fa4fe 100644 --- a/vector/src/main/java/im/vector/app/features/settings/locale/LocalePickerViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/locale/LocalePickerViewModel.kt @@ -16,15 +16,13 @@ package im.vector.app.features.settings.locale -import androidx.lifecycle.viewModelScope -import com.airbnb.mvrx.ActivityViewModelContext -import com.airbnb.mvrx.FragmentViewModelContext -import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.Success -import com.airbnb.mvrx.ViewModelContext import dagger.assisted.Assisted -import dagger.assisted.AssistedInject import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import im.vector.app.features.configuration.VectorConfiguration @@ -37,8 +35,8 @@ class LocalePickerViewModel @AssistedInject constructor( ) : VectorViewModel(initialState) { @AssistedFactory - interface Factory { - fun create(initialState: LocalePickerViewState): LocalePickerViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: LocalePickerViewState): LocalePickerViewModel } init { @@ -53,17 +51,7 @@ class LocalePickerViewModel @AssistedInject constructor( } } - companion object : MvRxViewModelFactory { - - @JvmStatic - override fun create(viewModelContext: ViewModelContext, state: LocalePickerViewState): LocalePickerViewModel? { - 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") - } - } + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() override fun handle(action: LocalePickerAction) { when (action) { diff --git a/vector/src/main/java/im/vector/app/features/settings/locale/LocalePickerViewState.kt b/vector/src/main/java/im/vector/app/features/settings/locale/LocalePickerViewState.kt index 64c95468f0..8cb5978393 100644 --- a/vector/src/main/java/im/vector/app/features/settings/locale/LocalePickerViewState.kt +++ b/vector/src/main/java/im/vector/app/features/settings/locale/LocalePickerViewState.kt @@ -17,7 +17,7 @@ package im.vector.app.features.settings.locale import com.airbnb.mvrx.Async -import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.Uninitialized import im.vector.app.features.settings.VectorLocale import java.util.Locale @@ -25,4 +25,4 @@ import java.util.Locale data class LocalePickerViewState( val currentLocale: Locale = VectorLocale.applicationLocale, val locales: Async> = Uninitialized -) : MvRxState +) : MavericksState diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/NotificationIndex.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/NotificationIndex.kt index 29d316bb76..793b94db1d 100644 --- a/vector/src/main/java/im/vector/app/features/settings/notifications/NotificationIndex.kt +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/NotificationIndex.kt @@ -44,11 +44,11 @@ val PushRule.notificationIndex: NotificationIndex? get() = */ private fun ruleMatches(rule: PushRule, targetRule: PushRule): Boolean { // Rules match if both are disabled, or if both are enabled and their highlight/sound/notify actions match up. - return (!rule.enabled && !targetRule.enabled) - || (rule.enabled - && targetRule.enabled - && rule.getHighlight() == targetRule.getHighlight() - && rule.getNotificationSound() == targetRule.getNotificationSound() - && rule.shouldNotify() == targetRule.shouldNotify() - && rule.shouldNotNotify() == targetRule.shouldNotNotify()) + return (!rule.enabled && !targetRule.enabled) || + (rule.enabled && + targetRule.enabled && + rule.getHighlight() == targetRule.getHighlight() && + rule.getNotificationSound() == targetRule.getNotificationSound() && + rule.shouldNotify() == targetRule.shouldNotify() && + rule.shouldNotNotify() == targetRule.shouldNotNotify()) } diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsAdvancedNotificationPreferenceFragment.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsAdvancedNotificationPreferenceFragment.kt index 93a788f528..3b4aef929d 100644 --- a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsAdvancedNotificationPreferenceFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsAdvancedNotificationPreferenceFragment.kt @@ -27,8 +27,8 @@ import org.matrix.android.sdk.api.pushrules.RuleIds import org.matrix.android.sdk.api.pushrules.rest.PushRuleAndKind import javax.inject.Inject -class VectorSettingsAdvancedNotificationPreferenceFragment @Inject constructor() - : VectorSettingsBaseFragment() { +class VectorSettingsAdvancedNotificationPreferenceFragment @Inject constructor() : + VectorSettingsBaseFragment() { override var titleRes: Int = R.string.settings_notification_advanced diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsDefaultNotificationPreferenceFragment.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsDefaultNotificationPreferenceFragment.kt index 3fc6293e6b..840e8ccde0 100644 --- a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsDefaultNotificationPreferenceFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsDefaultNotificationPreferenceFragment.kt @@ -20,8 +20,8 @@ import im.vector.app.R import im.vector.app.core.preference.VectorPreferenceCategory import org.matrix.android.sdk.api.pushrules.RuleIds -class VectorSettingsDefaultNotificationPreferenceFragment - : VectorSettingsPushRuleNotificationPreferenceFragment() { +class VectorSettingsDefaultNotificationPreferenceFragment : + VectorSettingsPushRuleNotificationPreferenceFragment() { override var titleRes: Int = R.string.settings_notification_default diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsKeywordAndMentionsNotificationPreferenceFragment.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsKeywordAndMentionsNotificationPreferenceFragment.kt index 59ed727191..fb1a357c30 100644 --- a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsKeywordAndMentionsNotificationPreferenceFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsKeywordAndMentionsNotificationPreferenceFragment.kt @@ -33,8 +33,8 @@ import org.matrix.android.sdk.api.pushrules.RuleKind import org.matrix.android.sdk.api.pushrules.rest.PushRule import org.matrix.android.sdk.api.pushrules.toJson -class VectorSettingsKeywordAndMentionsNotificationPreferenceFragment - : VectorSettingsPushRuleNotificationPreferenceFragment() { +class VectorSettingsKeywordAndMentionsNotificationPreferenceFragment : + VectorSettingsPushRuleNotificationPreferenceFragment() { override var titleRes: Int = R.string.settings_notification_mentions_and_keywords @@ -88,7 +88,7 @@ class VectorSettingsKeywordAndMentionsNotificationPreferenceFragment false } - editKeywordPreference.listener = object: KeywordPreference.Listener { + editKeywordPreference.listener = object : KeywordPreference.Listener { override fun onFocusDidChange(hasFocus: Boolean) { keywordsHasFocus = true } diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt index 40f575c853..b014b3d2dc 100644 --- a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt @@ -23,7 +23,10 @@ import android.media.RingtoneManager import android.net.Uri import android.os.Parcelable import android.widget.Toast +import androidx.lifecycle.LiveData +import androidx.lifecycle.distinctUntilChanged import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.map import androidx.preference.Preference import androidx.preference.SwitchPreference import im.vector.app.R @@ -43,10 +46,15 @@ import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorSettingsBaseFragment import im.vector.app.features.settings.VectorSettingsFragmentInteractionListener import im.vector.app.push.fcm.FcmHelper +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.pushrules.RuleIds import org.matrix.android.sdk.api.pushrules.RuleKind +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.identity.ThreePid +import org.matrix.android.sdk.api.session.pushers.Pusher +import org.matrix.android.sdk.internal.extensions.combineLatest import javax.inject.Inject // Referenced in vector_settings_preferences_root.xml @@ -78,6 +86,21 @@ class VectorSettingsNotificationPreferenceFragment @Inject constructor( (pref as SwitchPreference).isChecked = areNotifEnabledAtAccountLevel } + findPreference(VectorPreferences.SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY)?.let { + it.setTransactionalSwitchChangeListener(lifecycleScope) { isChecked -> + if (isChecked) { + FcmHelper.getFcmToken(requireContext())?.let { + pushManager.registerPusherWithFcmKey(it) + } + } else { + FcmHelper.getFcmToken(requireContext())?.let { + pushManager.unregisterPusher(it) + session.refreshPushers() + } + } + } + } + findPreference(VectorPreferences.SETTINGS_FDROID_BACKGROUND_SYNC_MODE)?.let { it.onPreferenceClickListener = Preference.OnPreferenceClickListener { val initialMode = vectorPreferences.getFdroidSyncBackgroundMode() @@ -116,11 +139,51 @@ class VectorSettingsNotificationPreferenceFragment @Inject constructor( } } + bindEmailNotifications() refreshBackgroundSyncPrefs() handleSystemPreference() } + private fun bindEmailNotifications() { + val initialEmails = session.getEmailsWithPushInformation() + bindEmailNotificationCategory(initialEmails) + session.getEmailsWithPushInformationLive().observe(this) { emails -> + if (initialEmails != emails) { + bindEmailNotificationCategory(emails) + } + } + } + + private fun bindEmailNotificationCategory(emails: List>) { + findPreference(VectorPreferences.SETTINGS_EMAIL_NOTIFICATION_CATEGORY_PREFERENCE_KEY)?.let { category -> + category.removeAll() + if (emails.isEmpty()) { + val vectorPreference = VectorPreference(requireContext()) + vectorPreference.title = resources.getString(R.string.settings_notification_emails_no_emails) + category.addPreference(vectorPreference) + vectorPreference.setOnPreferenceClickListener { + interactionListener?.navigateToEmailAndPhoneNumbers() + true + } + } else { + emails.forEach { (emailPid, isEnabled) -> + val pref = VectorSwitchPreference(requireContext()) + pref.title = resources.getString(R.string.settings_notification_emails_enable_for_email, emailPid.email) + pref.isChecked = isEnabled + pref.setTransactionalSwitchChangeListener(lifecycleScope) { isChecked -> + if (isChecked) { + pushManager.registerEmailForPush(emailPid.email) + } else { + pushManager.unregisterEmailPusher(emailPid.email) + } + } + category.addPreference(pref) + } + } + } + } + private val batteryStartForActivityResult = registerStartForActivityResult { // Noop } @@ -277,46 +340,16 @@ class VectorSettingsNotificationPreferenceFragment @Inject constructor( override fun onPreferenceTreeClick(preference: Preference?): Boolean { return when (preference?.key) { - VectorPreferences.SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY -> { - updateEnabledForDevice(preference) - true - } - VectorPreferences.SETTINGS_ENABLE_ALL_NOTIF_PREFERENCE_KEY -> { + VectorPreferences.SETTINGS_ENABLE_ALL_NOTIF_PREFERENCE_KEY -> { updateEnabledForAccount(preference) true } - else -> { + else -> { return super.onPreferenceTreeClick(preference) } } } - private fun updateEnabledForDevice(preference: Preference?) { - val switchPref = preference as SwitchPreference - if (switchPref.isChecked) { - FcmHelper.getFcmToken(requireContext())?.let { - pushManager.registerPusherWithFcmKey(it) - } - } else { - FcmHelper.getFcmToken(requireContext())?.let { - lifecycleScope.launch { - runCatching { pushManager.unregisterPusher(it) } - .fold( - { session.refreshPushers() }, - { - if (!isAdded) { - return@fold - } - // revert the check box - switchPref.isChecked = !switchPref.isChecked - Toast.makeText(activity, R.string.unknown_error, Toast.LENGTH_SHORT).show() - } - ) - } - } - } - } - private fun updateEnabledForAccount(preference: Preference?) { val pushRuleService = session val switchPref = preference as SwitchPreference @@ -343,3 +376,40 @@ class VectorSettingsNotificationPreferenceFragment @Inject constructor( } } } + +private fun SwitchPreference.setTransactionalSwitchChangeListener(scope: CoroutineScope, transaction: suspend (Boolean) -> Unit) { + setOnPreferenceChangeListener { switchPreference, isChecked -> + require(switchPreference is SwitchPreference) + val originalState = switchPreference.isChecked + scope.launch { + try { + transaction(isChecked as Boolean) + } catch (failure: Throwable) { + switchPreference.isChecked = originalState + Toast.makeText(switchPreference.context, R.string.unknown_error, Toast.LENGTH_SHORT).show() + } + } + true + } +} + +/** + * Fetches the current users 3pid emails and pairs them with their enabled state. + * If no pusher is available for a given email we can infer that push is not registered for the email. + * @return a list of ThreePid emails paired with the email notification enabled state. true if email notifications are enabled, false if not. + * @see ThreePid.Email + */ +private fun Session.getEmailsWithPushInformation(): List> { + val emailPushers = getPushers().filter { it.kind == Pusher.KIND_EMAIL } + return getThreePids() + .filterIsInstance() + .map { it to emailPushers.any { pusher -> pusher.pushKey == it.email } } +} + +private fun Session.getEmailsWithPushInformationLive(): LiveData>> { + val emailThreePids = getThreePidsLive(refreshData = true).map { it.filterIsInstance() } + val emailPushers = getPushersLive().map { it.filter { pusher -> pusher.kind == Pusher.KIND_EMAIL } } + return combineLatest(emailThreePids, emailPushers) { emailThreePidsResult, emailPushersResult -> + emailThreePidsResult.map { it to emailPushersResult.any { pusher -> pusher.pushKey == it.email } } + }.distinctUntilChanged() +} diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationsTroubleshootFragment.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationsTroubleshootFragment.kt index 6e47079afa..1a830dc0c1 100644 --- a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationsTroubleshootFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationsTroubleshootFragment.kt @@ -40,7 +40,6 @@ import im.vector.app.features.settings.VectorSettingsFragmentInteractionListener import im.vector.app.features.settings.troubleshoot.NotificationTroubleshootTestManager import im.vector.app.features.settings.troubleshoot.TroubleshootTest import im.vector.app.push.fcm.NotificationTroubleshootTestManagerFactory - import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.tryOrNull import javax.inject.Inject diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsOtherNotificationPreferenceFragment.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsOtherNotificationPreferenceFragment.kt index 2e083a7d65..71f8f0920a 100644 --- a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsOtherNotificationPreferenceFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsOtherNotificationPreferenceFragment.kt @@ -20,8 +20,8 @@ import im.vector.app.R import im.vector.app.core.preference.VectorPreferenceCategory import org.matrix.android.sdk.api.pushrules.RuleIds -class VectorSettingsOtherNotificationPreferenceFragment - : VectorSettingsPushRuleNotificationPreferenceFragment() { +class VectorSettingsOtherNotificationPreferenceFragment : + VectorSettingsPushRuleNotificationPreferenceFragment() { override var titleRes: Int = R.string.settings_notification_other diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsPushRuleNotificationPreferenceFragment.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsPushRuleNotificationPreferenceFragment.kt index dbf33f8fb3..26ee2fc601 100644 --- a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsPushRuleNotificationPreferenceFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsPushRuleNotificationPreferenceFragment.kt @@ -24,8 +24,8 @@ import kotlinx.coroutines.launch import org.matrix.android.sdk.api.pushrules.RuleKind import org.matrix.android.sdk.api.pushrules.rest.PushRuleAndKind -abstract class VectorSettingsPushRuleNotificationPreferenceFragment - : VectorSettingsBaseFragment() { +abstract class VectorSettingsPushRuleNotificationPreferenceFragment : + VectorSettingsBaseFragment() { abstract val prefKeyToPushRuleId: Map diff --git a/vector/src/main/java/im/vector/app/features/settings/push/PushGateWayController.kt b/vector/src/main/java/im/vector/app/features/settings/push/PushGateWayController.kt index 679f406832..6cb19b13c5 100644 --- a/vector/src/main/java/im/vector/app/features/settings/push/PushGateWayController.kt +++ b/vector/src/main/java/im/vector/app/features/settings/push/PushGateWayController.kt @@ -26,6 +26,8 @@ class PushGateWayController @Inject constructor( private val stringProvider: StringProvider ) : TypedEpoxyController() { + var interactionListener: PushGatewayItemInteractions? = null + override fun buildModels(data: PushGatewayViewState?) { val host = this data?.pushGateways?.invoke()?.let { pushers -> @@ -39,6 +41,9 @@ class PushGateWayController @Inject constructor( pushGatewayItem { id("${it.pushKey}_${it.appId}") pusher(it) + host.interactionListener?.let { + interactions(it) + } } } } diff --git a/vector/src/main/java/im/vector/app/features/settings/push/PushGatewayAction.kt b/vector/src/main/java/im/vector/app/features/settings/push/PushGatewayAction.kt index 566a068a7d..034b0b5ac7 100644 --- a/vector/src/main/java/im/vector/app/features/settings/push/PushGatewayAction.kt +++ b/vector/src/main/java/im/vector/app/features/settings/push/PushGatewayAction.kt @@ -17,7 +17,9 @@ package im.vector.app.features.settings.push import im.vector.app.core.platform.VectorViewModelAction +import org.matrix.android.sdk.api.session.pushers.Pusher sealed class PushGatewayAction : VectorViewModelAction { object Refresh : PushGatewayAction() + data class RemovePusher(val pusher: Pusher) : PushGatewayAction() } diff --git a/vector/src/main/java/im/vector/app/features/settings/push/PushGatewayItem.kt b/vector/src/main/java/im/vector/app/features/settings/push/PushGatewayItem.kt index dc66e1983b..04aa2747d7 100644 --- a/vector/src/main/java/im/vector/app/features/settings/push/PushGatewayItem.kt +++ b/vector/src/main/java/im/vector/app/features/settings/push/PushGatewayItem.kt @@ -16,12 +16,14 @@ package im.vector.app.features.settings.push +import android.view.View import android.widget.TextView import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import com.airbnb.epoxy.EpoxyModelWithHolder import im.vector.app.R import im.vector.app.core.epoxy.VectorEpoxyHolder +import im.vector.app.core.extensions.setTextOrHide import org.matrix.android.sdk.api.session.pushers.Pusher @EpoxyModelClass(layout = R.layout.item_pushgateway) @@ -30,33 +32,45 @@ abstract class PushGatewayItem : EpoxyModelWithHolder() @EpoxyAttribute lateinit var pusher: Pusher + @EpoxyAttribute + lateinit var interactions: PushGatewayItemInteractions + override fun bind(holder: Holder) { super.bind(holder) holder.kind.text = when (pusher.kind) { - // TODO Create const - "http" -> "Http Pusher" - "mail" -> "Email Pusher" - else -> pusher.kind + Pusher.KIND_HTTP -> "Http Pusher" + Pusher.KIND_EMAIL -> "Email Pusher" + else -> pusher.kind } holder.appId.text = pusher.appId holder.pushKey.text = pusher.pushKey holder.appName.text = pusher.appDisplayName - holder.url.text = pusher.data.url - holder.format.text = pusher.data.format + holder.url.setTextOrHide(pusher.data.url, hideWhenBlank = true, holder.urlTitle) + holder.format.setTextOrHide(pusher.data.format, hideWhenBlank = true, holder.formatTitle) holder.deviceName.text = pusher.deviceDisplayName + holder.removeButton.setOnClickListener { + interactions.onRemovePushTapped(pusher) + } } class Holder : VectorEpoxyHolder() { val kind by bind(R.id.pushGatewayKind) val pushKey by bind(R.id.pushGatewayKeyValue) val deviceName by bind(R.id.pushGatewayDeviceNameValue) + val formatTitle by bind(R.id.pushGatewayFormat) val format by bind(R.id.pushGatewayFormatValue) + val urlTitle by bind(R.id.pushGatewayURL) val url by bind(R.id.pushGatewayURLValue) val appName by bind(R.id.pushGatewayAppNameValue) val appId by bind(R.id.pushGatewayAppIdValue) + val removeButton by bind(R.id.pushGatewayDeleteButton) } } +interface PushGatewayItemInteractions { + fun onRemovePushTapped(pusher: Pusher) +} + // // abstract class ReactionInfoSimpleItem : EpoxyModelWithHolder() { diff --git a/vector/src/main/java/im/vector/app/features/settings/push/PushGatewayViewEvents.kt b/vector/src/main/java/im/vector/app/features/settings/push/PushGatewayViewEvents.kt new file mode 100644 index 0000000000..380a5df876 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/push/PushGatewayViewEvents.kt @@ -0,0 +1,23 @@ +/* + * 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.settings.push + +import im.vector.app.core.platform.VectorViewEvents + +sealed class PushGatewayViewEvents : VectorViewEvents { + data class RemovePusherFailed(val cause: Throwable) : PushGatewayViewEvents() +} diff --git a/vector/src/main/java/im/vector/app/features/settings/push/PushGatewaysFragment.kt b/vector/src/main/java/im/vector/app/features/settings/push/PushGatewaysFragment.kt index be2457397d..65c62542bb 100644 --- a/vector/src/main/java/im/vector/app/features/settings/push/PushGatewaysFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/push/PushGatewaysFragment.kt @@ -24,17 +24,18 @@ import android.view.ViewGroup import androidx.appcompat.app.AppCompatActivity import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState +import com.google.android.material.dialog.MaterialAlertDialogBuilder import im.vector.app.R import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.configureWith +import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.FragmentGenericRecyclerBinding - +import org.matrix.android.sdk.api.session.pushers.Pusher import javax.inject.Inject // Referenced in vector_settings_notifications.xml class PushGatewaysFragment @Inject constructor( - val pushGatewaysViewModelFactory: PushGatewaysViewModel.Factory, private val epoxyController: PushGateWayController ) : VectorBaseFragment() { @@ -64,7 +65,21 @@ class PushGatewaysFragment @Inject constructor( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + epoxyController.interactionListener = object : PushGatewayItemInteractions { + override fun onRemovePushTapped(pusher: Pusher) = viewModel.handle(PushGatewayAction.RemovePusher(pusher)) + } views.genericRecyclerView.configureWith(epoxyController, dividerDrawable = R.drawable.divider_horizontal) + viewModel.observeViewEvents { + when (it) { + is PushGatewayViewEvents.RemovePusherFailed -> { + MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.dialog_title_error) + .setMessage(errorFormatter.toHumanReadable(it.cause)) + .setPositiveButton(android.R.string.ok, null) + .show() + } + }.exhaustive + } } override fun onDestroyView() { diff --git a/vector/src/main/java/im/vector/app/features/settings/push/PushGatewaysViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/push/PushGatewaysViewModel.kt index 7981d71ce1..1256673364 100644 --- a/vector/src/main/java/im/vector/app/features/settings/push/PushGatewaysViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/push/PushGatewaysViewModel.kt @@ -17,42 +17,35 @@ package im.vector.app.features.settings.push import com.airbnb.mvrx.Async -import com.airbnb.mvrx.FragmentViewModelContext -import com.airbnb.mvrx.MvRxState -import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.MavericksState +import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.Uninitialized -import com.airbnb.mvrx.ViewModelContext import dagger.assisted.Assisted -import dagger.assisted.AssistedInject import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.extensions.exhaustive -import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel +import kotlinx.coroutines.launch import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.pushers.Pusher -import org.matrix.android.sdk.rx.RxSession +import org.matrix.android.sdk.flow.flow data class PushGatewayViewState( val pushGateways: Async> = Uninitialized -) : MvRxState +) : MavericksState class PushGatewaysViewModel @AssistedInject constructor(@Assisted initialState: PushGatewayViewState, - private val session: Session) - : VectorViewModel(initialState) { + private val session: Session) : + VectorViewModel(initialState) { @AssistedFactory - interface Factory { - fun create(initialState: PushGatewayViewState): PushGatewaysViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: PushGatewayViewState): PushGatewaysViewModel } - companion object : MvRxViewModelFactory { - - @JvmStatic - override fun create(viewModelContext: ViewModelContext, state: PushGatewayViewState): PushGatewaysViewModel? { - val fragment: PushGatewaysFragment = (viewModelContext as FragmentViewModelContext).fragment() - return fragment.pushGatewaysViewModelFactory.create(state) - } - } + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() init { observePushers() @@ -61,7 +54,7 @@ class PushGatewaysViewModel @AssistedInject constructor(@Assisted initialState: } private fun observePushers() { - RxSession(session) + session.flow() .livePushers() .execute { copy(pushGateways = it) @@ -70,10 +63,21 @@ class PushGatewaysViewModel @AssistedInject constructor(@Assisted initialState: override fun handle(action: PushGatewayAction) { when (action) { - is PushGatewayAction.Refresh -> handleRefresh() + is PushGatewayAction.Refresh -> handleRefresh() + is PushGatewayAction.RemovePusher -> removePusher(action.pusher) }.exhaustive } + private fun removePusher(pusher: Pusher) { + viewModelScope.launch { + kotlin.runCatching { + session.removePusher(pusher) + }.onFailure { + _viewEvents.post(PushGatewayViewEvents.RemovePusherFailed(it)) + } + } + } + private fun handleRefresh() { session.refreshPushers() } diff --git a/vector/src/main/java/im/vector/app/features/settings/push/PushRulesFragment.kt b/vector/src/main/java/im/vector/app/features/settings/push/PushRulesFragment.kt index d9dbdcc8d2..666f27272b 100644 --- a/vector/src/main/java/im/vector/app/features/settings/push/PushRulesFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/push/PushRulesFragment.kt @@ -27,7 +27,6 @@ import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.configureWith import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.FragmentGenericRecyclerBinding - import javax.inject.Inject // Referenced in vector_settings_notifications.xml diff --git a/vector/src/main/java/im/vector/app/features/settings/push/PushRulesViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/push/PushRulesViewModel.kt index 64ddc275eb..0b6b72bb10 100644 --- a/vector/src/main/java/im/vector/app/features/settings/push/PushRulesViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/push/PushRulesViewModel.kt @@ -15,10 +15,11 @@ */ package im.vector.app.features.settings.push -import com.airbnb.mvrx.MvRxState -import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.MavericksState +import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.ViewModelContext -import im.vector.app.core.di.HasScreenInjector +import dagger.hilt.EntryPoints +import im.vector.app.core.di.SingletonEntryPoint import im.vector.app.core.platform.EmptyAction import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel @@ -26,15 +27,15 @@ import org.matrix.android.sdk.api.pushrules.rest.PushRule data class PushRulesViewState( val rules: List = emptyList() -) : MvRxState +) : MavericksState -class PushRulesViewModel(initialState: PushRulesViewState) - : VectorViewModel(initialState) { +class PushRulesViewModel(initialState: PushRulesViewState) : + VectorViewModel(initialState) { - companion object : MvRxViewModelFactory { + companion object : MavericksViewModelFactory { override fun initialState(viewModelContext: ViewModelContext): PushRulesViewState? { - val session = (viewModelContext.activity as HasScreenInjector).injector().activeSessionHolder().getActiveSession() + val session = EntryPoints.get(viewModelContext.app(), SingletonEntryPoint::class.java).activeSessionHolder().getActiveSession() val rules = session.getPushRules().getAllRules() return PushRulesViewState(rules) } diff --git a/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsController.kt b/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsController.kt index f9cf3bf5a3..cdc40185aa 100644 --- a/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsController.kt +++ b/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsController.kt @@ -295,9 +295,9 @@ class ThreePidsSettingsController @Inject constructor( val failure = (data.msisdnValidationRequests[threePid.value] as? Fail)?.error ?: return null // Wrong code? // See https://github.com/matrix-org/synapse/issues/8218 - return if (failure is Failure.ServerError - && failure.httpCode == 400 - && failure.error.code == MatrixError.M_UNKNOWN) { + return if (failure is Failure.ServerError && + failure.httpCode == 400 && + failure.error.code == MatrixError.M_UNKNOWN) { stringProvider.getString(R.string.settings_text_message_sent_wrong_code) } else { errorFormatter.toHumanReadable(failure) diff --git a/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsFragment.kt index 384348b85d..a893f0f508 100644 --- a/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsFragment.kt @@ -43,12 +43,10 @@ import org.matrix.android.sdk.api.session.identity.ThreePid import javax.inject.Inject class ThreePidsSettingsFragment @Inject constructor( - private val viewModelFactory: ThreePidsSettingsViewModel.Factory, private val epoxyController: ThreePidsSettingsController ) : VectorBaseFragment(), OnBackPressed, - ThreePidsSettingsViewModel.Factory by viewModelFactory, ThreePidsSettingsController.InteractionListener { private val viewModel: ThreePidsSettingsViewModel by fragmentViewModel() diff --git a/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsViewEvents.kt b/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsViewEvents.kt index 0346fd137e..94ea8d24c6 100644 --- a/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsViewEvents.kt @@ -21,6 +21,7 @@ import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse sealed class ThreePidsSettingsViewEvents : VectorViewEvents { data class Failure(val throwable: Throwable) : ThreePidsSettingsViewEvents() + // object RequestPassword : ThreePidsSettingsViewEvents() data class RequestReAuth(val registrationFlowResponse: RegistrationFlowResponse, val lastErrorCode: String?) : ThreePidsSettingsViewEvents() } diff --git a/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsViewModel.kt index ac565e72a1..12ff436ccb 100644 --- a/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsViewModel.kt @@ -16,32 +16,30 @@ package im.vector.app.features.settings.threepids -import androidx.lifecycle.viewModelScope -import com.airbnb.mvrx.ActivityViewModelContext import com.airbnb.mvrx.Fail -import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.Loading -import com.airbnb.mvrx.MvRxViewModelFactory -import com.airbnb.mvrx.ViewModelContext +import com.airbnb.mvrx.MavericksViewModelFactory import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import im.vector.app.R +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider import im.vector.app.core.utils.ReadOnceTrue import im.vector.app.features.auth.ReAuthActivity import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.auth.UIABaseAuth import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor +import org.matrix.android.sdk.api.auth.UserPasswordAuth import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.identity.ThreePid +import org.matrix.android.sdk.flow.flow import org.matrix.android.sdk.internal.crypto.crosssigning.fromBase64 import org.matrix.android.sdk.internal.crypto.model.rest.DefaultBaseAuth -import org.matrix.android.sdk.api.auth.UIABaseAuth -import org.matrix.android.sdk.api.auth.UserPasswordAuth -import org.matrix.android.sdk.rx.rx import timber.log.Timber import kotlin.coroutines.Continuation import kotlin.coroutines.resume @@ -80,21 +78,11 @@ class ThreePidsSettingsViewModel @AssistedInject constructor( } @AssistedFactory - interface Factory { - fun create(initialState: ThreePidsSettingsViewState): ThreePidsSettingsViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: ThreePidsSettingsViewState): ThreePidsSettingsViewModel } - companion object : MvRxViewModelFactory { - - @JvmStatic - override fun create(viewModelContext: ViewModelContext, state: ThreePidsSettingsViewState): ThreePidsSettingsViewModel? { - 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") - } - } + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() init { observeThreePids() @@ -102,7 +90,7 @@ class ThreePidsSettingsViewModel @AssistedInject constructor( } private fun observeThreePids() { - session.rx() + session.flow() .liveThreePIds(true) .execute { copy( @@ -112,7 +100,7 @@ class ThreePidsSettingsViewModel @AssistedInject constructor( } private fun observePendingThreePids() { - session.rx() + session.flow() .livePendingThreePIds() .execute { copy( @@ -131,13 +119,13 @@ class ThreePidsSettingsViewModel @AssistedInject constructor( override fun handle(action: ThreePidsSettingsAction) { when (action) { - is ThreePidsSettingsAction.AddThreePid -> handleAddThreePid(action) + is ThreePidsSettingsAction.AddThreePid -> handleAddThreePid(action) is ThreePidsSettingsAction.ContinueThreePid -> handleContinueThreePid(action) - is ThreePidsSettingsAction.SubmitCode -> handleSubmitCode(action) - is ThreePidsSettingsAction.CancelThreePid -> handleCancelThreePid(action) - is ThreePidsSettingsAction.DeleteThreePid -> handleDeleteThreePid(action) - is ThreePidsSettingsAction.ChangeUiState -> handleChangeUiState(action) - ThreePidsSettingsAction.SsoAuthDone -> { + is ThreePidsSettingsAction.SubmitCode -> handleSubmitCode(action) + is ThreePidsSettingsAction.CancelThreePid -> handleCancelThreePid(action) + is ThreePidsSettingsAction.DeleteThreePid -> handleDeleteThreePid(action) + is ThreePidsSettingsAction.ChangeUiState -> handleChangeUiState(action) + ThreePidsSettingsAction.SsoAuthDone -> { Timber.d("## UIA - FallBack success") if (pendingAuth != null) { uiaContinuation?.resume(pendingAuth!!) @@ -155,7 +143,7 @@ class ThreePidsSettingsViewModel @AssistedInject constructor( ) ) } - ThreePidsSettingsAction.ReAuthCancelled -> { + ThreePidsSettingsAction.ReAuthCancelled -> { Timber.d("## UIA - Reauth cancelled") uiaContinuation?.resumeWithException(Exception()) uiaContinuation = null diff --git a/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsViewState.kt b/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsViewState.kt index b080c06cbd..dbc81fd8f3 100644 --- a/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsViewState.kt +++ b/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsViewState.kt @@ -17,7 +17,7 @@ package im.vector.app.features.settings.threepids import com.airbnb.mvrx.Async -import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.Uninitialized import im.vector.app.core.utils.ReadOnceTrue import org.matrix.android.sdk.api.session.identity.ThreePid @@ -30,4 +30,4 @@ data class ThreePidsSettingsViewState( val msisdnValidationRequests: Map> = emptyMap(), val editTextReinitiator: ReadOnceTrue = ReadOnceTrue(), val msisdnValidationReinitiator: Map = emptyMap() -) : MvRxState +) : MavericksState diff --git a/vector/src/main/java/im/vector/app/features/settings/troubleshoot/NotificationTroubleshootRecyclerViewAdapter.kt b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/NotificationTroubleshootRecyclerViewAdapter.kt index 70e5bdff10..8f0b2cfe74 100644 --- a/vector/src/main/java/im/vector/app/features/settings/troubleshoot/NotificationTroubleshootRecyclerViewAdapter.kt +++ b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/NotificationTroubleshootRecyclerViewAdapter.kt @@ -24,8 +24,8 @@ import im.vector.app.R import im.vector.app.databinding.ItemNotificationTroubleshootBinding import im.vector.app.features.themes.ThemeUtils -class NotificationTroubleshootRecyclerViewAdapter(val tests: ArrayList) - : RecyclerView.Adapter() { +class NotificationTroubleshootRecyclerViewAdapter(val tests: ArrayList) : + RecyclerView.Adapter() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { val inflater = LayoutInflater.from(parent.context) diff --git a/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestAccountSettings.kt b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestAccountSettings.kt index dad35fd13f..9d06e1724c 100644 --- a/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestAccountSettings.kt +++ b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestAccountSettings.kt @@ -33,8 +33,8 @@ import javax.inject.Inject * Check that the main pushRule (RULE_ID_DISABLE_ALL) is correctly setup */ class TestAccountSettings @Inject constructor(private val stringProvider: StringProvider, - private val activeSessionHolder: ActiveSessionHolder) - : TroubleshootTest(R.string.settings_troubleshoot_test_account_settings_title) { + private val activeSessionHolder: ActiveSessionHolder) : + TroubleshootTest(R.string.settings_troubleshoot_test_account_settings_title) { override fun perform(activityResultLauncher: ActivityResultLauncher) { val session = activeSessionHolder.getSafeActiveSession() ?: return diff --git a/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestDeviceSettings.kt b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestDeviceSettings.kt index 0d661e8b16..f9fc7e80e3 100644 --- a/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestDeviceSettings.kt +++ b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestDeviceSettings.kt @@ -26,8 +26,8 @@ import javax.inject.Inject * Checks if notifications are enable in the system settings for this app. */ class TestDeviceSettings @Inject constructor(private val vectorPreferences: VectorPreferences, - private val stringProvider: StringProvider) - : TroubleshootTest(R.string.settings_troubleshoot_test_device_settings_title) { + private val stringProvider: StringProvider) : + TroubleshootTest(R.string.settings_troubleshoot_test_device_settings_title) { override fun perform(activityResultLauncher: ActivityResultLauncher) { if (vectorPreferences.areNotificationEnabledForDevice()) { diff --git a/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestNotification.kt b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestNotification.kt index 6f25ecfe39..412916c201 100644 --- a/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestNotification.kt +++ b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestNotification.kt @@ -29,8 +29,8 @@ import javax.inject.Inject */ class TestNotification @Inject constructor(private val context: Context, private val notificationUtils: NotificationUtils, - private val stringProvider: StringProvider) - : TroubleshootTest(R.string.settings_troubleshoot_test_notification_title) { + private val stringProvider: StringProvider) : + TroubleshootTest(R.string.settings_troubleshoot_test_notification_title) { override fun perform(activityResultLauncher: ActivityResultLauncher) { // Display the notification right now diff --git a/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestPushRulesSettings.kt b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestPushRulesSettings.kt index eea5705b7a..79e4377a5c 100644 --- a/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestPushRulesSettings.kt +++ b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestPushRulesSettings.kt @@ -26,8 +26,8 @@ import org.matrix.android.sdk.api.pushrules.getActions import javax.inject.Inject class TestPushRulesSettings @Inject constructor(private val activeSessionHolder: ActiveSessionHolder, - private val stringProvider: StringProvider) - : TroubleshootTest(R.string.settings_troubleshoot_test_bing_settings_title) { + private val stringProvider: StringProvider) : + TroubleshootTest(R.string.settings_troubleshoot_test_bing_settings_title) { private val testedRules = listOf(RuleIds.RULE_ID_CONTAIN_DISPLAY_NAME, diff --git a/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestSystemSettings.kt b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestSystemSettings.kt index ee652288be..42f506d4a6 100644 --- a/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestSystemSettings.kt +++ b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestSystemSettings.kt @@ -15,10 +15,10 @@ */ package im.vector.app.features.settings.troubleshoot -import android.content.Context import android.content.Intent import androidx.activity.result.ActivityResultLauncher import androidx.core.app.NotificationManagerCompat +import androidx.fragment.app.FragmentActivity import im.vector.app.R import im.vector.app.core.resources.StringProvider import im.vector.app.core.utils.startNotificationSettingsIntent @@ -27,9 +27,9 @@ import javax.inject.Inject /** * Checks if notifications are enable in the system settings for this app. */ -class TestSystemSettings @Inject constructor(private val context: Context, - private val stringProvider: StringProvider) - : TroubleshootTest(R.string.settings_troubleshoot_test_system_settings_title) { +class TestSystemSettings @Inject constructor(private val context: FragmentActivity, + private val stringProvider: StringProvider) : + TroubleshootTest(R.string.settings_troubleshoot_test_system_settings_title) { override fun perform(activityResultLauncher: ActivityResultLauncher) { if (NotificationManagerCompat.from(context).areNotificationsEnabled()) { @@ -40,7 +40,6 @@ class TestSystemSettings @Inject constructor(private val context: Context, description = stringProvider.getString(R.string.settings_troubleshoot_test_system_settings_failed) quickFix = object : TroubleshootQuickFix(R.string.open_settings) { override fun doFix() { - if (manager?.diagStatus == TestStatus.RUNNING) return // wait before all is finished startNotificationSettingsIntent(context, activityResultLauncher) } } diff --git a/vector/src/main/java/im/vector/app/features/share/IncomingShareActivity.kt b/vector/src/main/java/im/vector/app/features/share/IncomingShareActivity.kt index b9d3b1ba14..09321ad27e 100644 --- a/vector/src/main/java/im/vector/app/features/share/IncomingShareActivity.kt +++ b/vector/src/main/java/im/vector/app/features/share/IncomingShareActivity.kt @@ -17,12 +17,14 @@ package im.vector.app.features.share import com.google.android.material.appbar.MaterialToolbar +import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R import im.vector.app.core.extensions.addFragment import im.vector.app.core.platform.ToolbarConfigurable import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.databinding.ActivitySimpleBinding +@AndroidEntryPoint class IncomingShareActivity : VectorBaseActivity(), ToolbarConfigurable { override fun getBinding() = ActivitySimpleBinding.inflate(layoutInflater) diff --git a/vector/src/main/java/im/vector/app/features/share/IncomingShareFragment.kt b/vector/src/main/java/im/vector/app/features/share/IncomingShareFragment.kt index 313bf2d2b7..d5fd3050e9 100644 --- a/vector/src/main/java/im/vector/app/features/share/IncomingShareFragment.kt +++ b/vector/src/main/java/im/vector/app/features/share/IncomingShareFragment.kt @@ -41,7 +41,6 @@ import im.vector.app.databinding.FragmentIncomingShareBinding import im.vector.app.features.attachments.AttachmentsHelper import im.vector.app.features.attachments.preview.AttachmentsPreviewActivity import im.vector.app.features.attachments.preview.AttachmentsPreviewArgs - import org.matrix.android.sdk.api.session.content.ContentAttachmentData import org.matrix.android.sdk.api.session.room.model.RoomSummary import javax.inject.Inject @@ -51,7 +50,6 @@ import javax.inject.Inject * The user can select multiple rooms to send the data to */ class IncomingShareFragment @Inject constructor( - val incomingShareViewModelFactory: IncomingShareViewModel.Factory, private val incomingShareController: IncomingShareController, private val sessionHolder: ActiveSessionHolder ) : diff --git a/vector/src/main/java/im/vector/app/features/share/IncomingShareViewModel.kt b/vector/src/main/java/im/vector/app/features/share/IncomingShareViewModel.kt index 4d211d11de..4a413ad8ba 100644 --- a/vector/src/main/java/im/vector/app/features/share/IncomingShareViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/share/IncomingShareViewModel.kt @@ -16,48 +16,43 @@ package im.vector.app.features.share -import com.airbnb.mvrx.FragmentViewModelContext -import com.airbnb.mvrx.MvRxViewModelFactory -import com.airbnb.mvrx.ViewModelContext -import com.jakewharton.rxrelay2.BehaviorRelay +import com.airbnb.mvrx.MavericksViewModelFactory import dagger.assisted.Assisted -import dagger.assisted.AssistedInject import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.toggle import im.vector.app.core.platform.VectorViewModel import im.vector.app.features.attachments.isPreviewable import im.vector.app.features.attachments.toGroupedContentAttachmentData import im.vector.app.features.home.room.list.BreadcrumbsRoomComparator +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.sample import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.content.ContentAttachmentData 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.rx -import java.util.concurrent.TimeUnit +import org.matrix.android.sdk.flow.flow class IncomingShareViewModel @AssistedInject constructor( @Assisted initialState: IncomingShareViewState, private val session: Session, - private val breadcrumbsRoomComparator: BreadcrumbsRoomComparator) - : VectorViewModel(initialState) { + private val breadcrumbsRoomComparator: BreadcrumbsRoomComparator) : + VectorViewModel(initialState) { @AssistedFactory - interface Factory { - fun create(initialState: IncomingShareViewState): IncomingShareViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: IncomingShareViewState): IncomingShareViewModel } - companion object : MvRxViewModelFactory { + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() - @JvmStatic - override fun create(viewModelContext: ViewModelContext, state: IncomingShareViewState): IncomingShareViewModel? { - val fragment: IncomingShareFragment = (viewModelContext as FragmentViewModelContext).fragment() - return fragment.incomingShareViewModelFactory.create(state) - } - } - - private val filterStream: BehaviorRelay = BehaviorRelay.createDefault("") + private val filterStream = MutableStateFlow("") init { observeRoomSummaries() @@ -68,13 +63,13 @@ class IncomingShareViewModel @AssistedInject constructor( memberships = listOf(Membership.JOIN) } session - .rx().liveRoomSummaries(queryParams) + .flow().liveRoomSummaries(queryParams) .execute { copy(roomSummaries = it) } filterStream - .switchMap { filter -> + .flatMapLatest { filter -> val displayNameQuery = if (filter.isEmpty()) { QueryStringValue.NoCondition } else { @@ -84,9 +79,9 @@ class IncomingShareViewModel @AssistedInject constructor( displayName = displayNameQuery memberships = listOf(Membership.JOIN) } - session.rx().liveRoomSummaries(filterQueryParams) + session.flow().liveRoomSummaries(filterQueryParams) } - .throttleLast(300, TimeUnit.MILLISECONDS) + .sample(300) .map { it.sortedWith(breadcrumbsRoomComparator) } .execute { copy(filteredRoomSummaries = it) @@ -109,7 +104,7 @@ class IncomingShareViewModel @AssistedInject constructor( } private fun handleFilter(action: IncomingShareAction.FilterWith) { - filterStream.accept(action.filter) + filterStream.tryEmit(action.filter) } private fun handleShareToSelectedRooms() = withState { state -> diff --git a/vector/src/main/java/im/vector/app/features/share/IncomingShareViewState.kt b/vector/src/main/java/im/vector/app/features/share/IncomingShareViewState.kt index 751dc999a2..620709a515 100644 --- a/vector/src/main/java/im/vector/app/features/share/IncomingShareViewState.kt +++ b/vector/src/main/java/im/vector/app/features/share/IncomingShareViewState.kt @@ -17,7 +17,7 @@ package im.vector.app.features.share import com.airbnb.mvrx.Async -import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.Uninitialized import org.matrix.android.sdk.api.session.room.model.RoomSummary @@ -27,4 +27,4 @@ data class IncomingShareViewState( val filteredRoomSummaries: Async> = Uninitialized, val selectedRoomIds: Set = emptySet(), val isInMultiSelectionMode: Boolean = false -) : MvRxState +) : MavericksState diff --git a/vector/src/main/java/im/vector/app/features/signout/hard/SignedOutActivity.kt b/vector/src/main/java/im/vector/app/features/signout/hard/SignedOutActivity.kt index 08bd84994b..ee7557b402 100644 --- a/vector/src/main/java/im/vector/app/features/signout/hard/SignedOutActivity.kt +++ b/vector/src/main/java/im/vector/app/features/signout/hard/SignedOutActivity.kt @@ -19,17 +19,18 @@ package im.vector.app.features.signout.hard import android.content.Context import android.content.Intent import android.os.Bundle +import dagger.hilt.android.AndroidEntryPoint import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.databinding.ActivitySignedOutBinding import im.vector.app.features.MainActivity import im.vector.app.features.MainActivityArgs - import org.matrix.android.sdk.api.failure.GlobalError import timber.log.Timber /** * In this screen, the user is viewing a message informing that he has been logged out */ +@AndroidEntryPoint class SignedOutActivity : VectorBaseActivity() { override fun getBinding() = ActivitySignedOutBinding.inflate(layoutInflater) diff --git a/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutActivity.kt b/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutActivity.kt index 8c725cd81a..6e70b34002 100644 --- a/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutActivity.kt +++ b/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutActivity.kt @@ -23,14 +23,13 @@ import androidx.fragment.app.FragmentManager import com.airbnb.mvrx.Success import com.airbnb.mvrx.viewModel import com.google.android.material.dialog.MaterialAlertDialogBuilder +import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R -import im.vector.app.core.di.ScreenComponent import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.extensions.replaceFragment import im.vector.app.features.MainActivity import im.vector.app.features.MainActivityArgs import im.vector.app.features.login.LoginActivity - import org.matrix.android.sdk.api.failure.GlobalError import org.matrix.android.sdk.api.session.Session import timber.log.Timber @@ -40,19 +39,14 @@ import javax.inject.Inject * In this screen, the user is viewing a message informing that he has been logged out * Extends LoginActivity to get the login with SSO and forget password functionality for (nearly) free */ +@AndroidEntryPoint class SoftLogoutActivity : LoginActivity() { private val softLogoutViewModel: SoftLogoutViewModel by viewModel() - @Inject lateinit var softLogoutViewModelFactory: SoftLogoutViewModel.Factory @Inject lateinit var session: Session @Inject lateinit var errorFormatter: ErrorFormatter - override fun injectWith(injector: ScreenComponent) { - super.injectWith(injector) - injector.inject(this) - } - override fun initUiAndData() { super.initUiAndData() diff --git a/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutActivity2.kt b/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutActivity2.kt index 5db82c0631..ed45069e92 100644 --- a/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutActivity2.kt +++ b/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutActivity2.kt @@ -23,14 +23,13 @@ import androidx.fragment.app.FragmentManager import com.airbnb.mvrx.Success import com.airbnb.mvrx.viewModel import com.google.android.material.dialog.MaterialAlertDialogBuilder +import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R -import im.vector.app.core.di.ScreenComponent import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.extensions.replaceFragment import im.vector.app.features.MainActivity import im.vector.app.features.MainActivityArgs import im.vector.app.features.login2.LoginActivity2 - import org.matrix.android.sdk.api.failure.GlobalError import org.matrix.android.sdk.api.session.Session import timber.log.Timber @@ -42,19 +41,14 @@ import javax.inject.Inject * * This is just a copy of SoftLogoutActivity2, which extends LoginActivity2 */ +@AndroidEntryPoint class SoftLogoutActivity2 : LoginActivity2() { private val softLogoutViewModel: SoftLogoutViewModel by viewModel() - @Inject lateinit var softLogoutViewModelFactory: SoftLogoutViewModel.Factory @Inject lateinit var session: Session @Inject lateinit var errorFormatter: ErrorFormatter - override fun injectWith(injector: ScreenComponent) { - super.injectWith(injector) - injector.inject(this) - } - override fun initUiAndData() { super.initUiAndData() diff --git a/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutFragment.kt b/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutFragment.kt index 48cf7b12bc..2aa7f15172 100644 --- a/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutFragment.kt +++ b/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutFragment.kt @@ -32,7 +32,6 @@ import im.vector.app.features.login.AbstractLoginFragment import im.vector.app.features.login.LoginAction import im.vector.app.features.login.LoginMode import im.vector.app.features.login.LoginViewEvents - import javax.inject.Inject /** diff --git a/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutViewModel.kt b/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutViewModel.kt index f49527bd1d..52986a1f3b 100644 --- a/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutViewModel.kt @@ -16,11 +16,10 @@ package im.vector.app.features.signout.soft -import androidx.lifecycle.viewModelScope import com.airbnb.mvrx.ActivityViewModelContext import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Loading -import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.ViewModelContext @@ -28,6 +27,8 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import im.vector.app.core.di.ActiveSessionHolder +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.extensions.hasUnsavedKeys import im.vector.app.core.platform.VectorViewModel import im.vector.app.features.login.LoginMode @@ -48,11 +49,11 @@ class SoftLogoutViewModel @AssistedInject constructor( ) : VectorViewModel(initialState) { @AssistedFactory - interface Factory { - fun create(initialState: SoftLogoutViewState): SoftLogoutViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: SoftLogoutViewState): SoftLogoutViewModel } - companion object : MvRxViewModelFactory { + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() { override fun initialState(viewModelContext: ViewModelContext): SoftLogoutViewState? { val activity: SoftLogoutActivity = (viewModelContext as ActivityViewModelContext).activity() @@ -65,12 +66,6 @@ class SoftLogoutViewModel @AssistedInject constructor( hasUnsavedKeys = activity.session.hasUnsavedKeys() ) } - - @JvmStatic - override fun create(viewModelContext: ViewModelContext, state: SoftLogoutViewState): SoftLogoutViewModel? { - val activity: SoftLogoutActivity = (viewModelContext as ActivityViewModelContext).activity() - return activity.softLogoutViewModelFactory.create(state) - } } init { @@ -103,11 +98,11 @@ class SoftLogoutViewModel @AssistedInject constructor( val loginMode = when { // SSO login is taken first - data.supportedLoginTypes.contains(LoginFlowTypes.SSO) - && data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.SsoAndPassword(data.ssoIdentityProviders) - data.supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso(data.ssoIdentityProviders) - data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.Password - else -> LoginMode.Unsupported + data.supportedLoginTypes.contains(LoginFlowTypes.SSO) && + data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.SsoAndPassword(data.ssoIdentityProviders) + data.supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso(data.ssoIdentityProviders) + data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.Password + else -> LoginMode.Unsupported } setState { diff --git a/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutViewState.kt b/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutViewState.kt index ccc186aba7..511711ab2f 100644 --- a/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutViewState.kt +++ b/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutViewState.kt @@ -18,7 +18,7 @@ package im.vector.app.features.signout.soft import com.airbnb.mvrx.Async import com.airbnb.mvrx.Loading -import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized import im.vector.app.features.login.LoginMode @@ -32,11 +32,11 @@ data class SoftLogoutViewState( val userDisplayName: String, val hasUnsavedKeys: Boolean, val enteredPassword: String = "" -) : MvRxState { +) : MavericksState { fun isLoading(): Boolean { - return asyncLoginAction is Loading + return asyncLoginAction is Loading || // Keep loading when it is success because of the delay to switch to the next Activity - || asyncLoginAction is Success + asyncLoginAction is Success } } diff --git a/vector/src/main/java/im/vector/app/features/spaces/InviteRoomSpaceChooserBottomSheet.kt b/vector/src/main/java/im/vector/app/features/spaces/InviteRoomSpaceChooserBottomSheet.kt index bc2a9c604b..b4c1e67cfb 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/InviteRoomSpaceChooserBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/InviteRoomSpaceChooserBottomSheet.kt @@ -23,14 +23,15 @@ import android.view.View import android.view.ViewGroup import androidx.core.view.isVisible import com.airbnb.mvrx.args +import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder -import im.vector.app.core.di.ScreenComponent import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment import im.vector.app.databinding.BottomSheetSpaceInviteChooserBinding import kotlinx.parcelize.Parcelize import javax.inject.Inject +@AndroidEntryPoint class InviteRoomSpaceChooserBottomSheet : VectorBaseBottomSheetDialogFragment() { @Parcelize @@ -53,10 +54,6 @@ class InviteRoomSpaceChooserBottomSheet : VectorBaseBottomSheetDialogFragment() { val settingsViewModel: SpaceMenuViewModel by parentFragmentViewModel() @@ -59,10 +61,6 @@ class LeaveSpaceBottomSheet : VectorBaseBottomSheetDialogFragment(), SpaceDirectoryViewModel.Factory, MatrixToBottomSheet.InteractionListener { - - @Inject lateinit var spaceDirectoryViewModelFactory: SpaceDirectoryViewModel.Factory - - override fun injectWith(injector: ScreenComponent) { - injector.inject(this) - } +@AndroidEntryPoint +class SpaceExploreActivity : VectorBaseActivity(), MatrixToBottomSheet.InteractionListener { override fun getBinding(): ActivitySimpleBinding = ActivitySimpleBinding.inflate(layoutInflater) @@ -51,18 +45,18 @@ class SpaceExploreActivity : VectorBaseActivity(), SpaceD val sharedViewModel: SpaceDirectoryViewModel by viewModel() private val fragmentLifecycleCallbacks = object : FragmentManager.FragmentLifecycleCallbacks() { - override fun onFragmentAttached(fm: FragmentManager, f: Fragment, context: Context) { + override fun onFragmentResumed(fm: FragmentManager, f: Fragment) { if (f is MatrixToBottomSheet) { f.interactionListener = this@SpaceExploreActivity } - super.onFragmentAttached(fm, f, context) + super.onFragmentResumed(fm, f) } - override fun onFragmentDetached(fm: FragmentManager, f: Fragment) { + override fun onFragmentPaused(fm: FragmentManager, f: Fragment) { if (f is MatrixToBottomSheet) { f.interactionListener = null } - super.onFragmentDetached(fm, f) + super.onFragmentPaused(fm, f) } } @@ -72,12 +66,12 @@ class SpaceExploreActivity : VectorBaseActivity(), SpaceD if (isFirstCreation()) { val simpleName = SpaceDirectoryFragment::class.java.simpleName - val args = intent?.getParcelableExtra(MvRx.KEY_ARG) + val args = intent?.getParcelableExtra(Mavericks.KEY_ARG) if (supportFragmentManager.findFragmentByTag(simpleName) == null) { supportFragmentManager.commitTransaction { replace(R.id.simpleFragmentContainer, SpaceDirectoryFragment::class.java, - Bundle().apply { this.putParcelable(MvRx.KEY_ARG, args) }, + Bundle().apply { this.putParcelable(Mavericks.KEY_ARG, args) }, simpleName ) } @@ -86,14 +80,14 @@ class SpaceExploreActivity : VectorBaseActivity(), SpaceD sharedViewModel.observeViewEvents { when (it) { - SpaceDirectoryViewEvents.Dismiss -> { + SpaceDirectoryViewEvents.Dismiss -> { finish() } - is SpaceDirectoryViewEvents.NavigateToRoom -> { + is SpaceDirectoryViewEvents.NavigateToRoom -> { navigator.openRoom(this, it.roomId) } is SpaceDirectoryViewEvents.NavigateToMxToBottomSheet -> { - MatrixToBottomSheet.withLink(it.link, this).show(supportFragmentManager, "ShowChild") + MatrixToBottomSheet.withLink(it.link).show(supportFragmentManager, "ShowChild") } } } @@ -107,15 +101,16 @@ class SpaceExploreActivity : VectorBaseActivity(), SpaceD companion object { fun newIntent(context: Context, spaceId: String): Intent { return Intent(context, SpaceExploreActivity::class.java).apply { - putExtra(MvRx.KEY_ARG, SpaceDirectoryArgs(spaceId)) + putExtra(Mavericks.KEY_ARG, SpaceDirectoryArgs(spaceId)) } } } - override fun create(initialState: SpaceDirectoryState): SpaceDirectoryViewModel = - spaceDirectoryViewModelFactory.create(initialState) - - override fun navigateToRoom(roomId: String) { + override fun mxToBottomSheetNavigateToRoom(roomId: String) { navigator.openRoom(this, roomId) } + + override fun mxToBottomSheetSwitchToSpace(spaceId: String) { + navigator.switchToSpace(this, spaceId, Navigator.PostSwitchSpaceAction.None) + } } diff --git a/vector/src/main/java/im/vector/app/features/spaces/SpaceListAction.kt b/vector/src/main/java/im/vector/app/features/spaces/SpaceListAction.kt index 12d4b40f42..5bff2b733f 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/SpaceListAction.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/SpaceListAction.kt @@ -26,7 +26,7 @@ sealed class SpaceListAction : VectorViewModelAction { data class LeaveSpace(val spaceSummary: RoomSummary) : SpaceListAction() data class ToggleExpand(val spaceSummary: RoomSummary) : SpaceListAction() object AddSpace : SpaceListAction() - data class MoveSpace(val spaceId: String, val delta : Int) : SpaceListAction() + data class MoveSpace(val spaceId: String, val delta: Int) : SpaceListAction() data class OnStartDragging(val spaceId: String, val expanded: Boolean) : SpaceListAction() data class OnEndDragging(val spaceId: String, val expanded: Boolean) : SpaceListAction() diff --git a/vector/src/main/java/im/vector/app/features/spaces/SpaceListFragment.kt b/vector/src/main/java/im/vector/app/features/spaces/SpaceListFragment.kt index 0a67977e6c..dff98722eb 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/SpaceListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/SpaceListFragment.kt @@ -39,12 +39,11 @@ import org.matrix.android.sdk.api.session.room.model.RoomSummary import javax.inject.Inject class SpaceListFragment @Inject constructor( - val spaceListViewModelFactory: SpacesListViewModel.Factory, private val spaceController: SpaceSummaryController ) : VectorBaseFragment(), SpaceSummaryController.Callback { private lateinit var sharedActionViewModel: HomeSharedActionViewModel - private val viewModel: SpacesListViewModel by fragmentViewModel() + private val viewModel: SpaceListViewModel by fragmentViewModel() override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentGroupListBinding { return FragmentGroupListBinding.inflate(inflater, container, false) diff --git a/vector/src/main/java/im/vector/app/features/spaces/SpacesListViewModel.kt b/vector/src/main/java/im/vector/app/features/spaces/SpaceListViewModel.kt similarity index 80% rename from vector/src/main/java/im/vector/app/features/spaces/SpacesListViewModel.kt rename to vector/src/main/java/im/vector/app/features/spaces/SpaceListViewModel.kt index 12c4ddbfc4..4487833773 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/SpacesListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/SpaceListViewModel.kt @@ -16,25 +16,30 @@ package im.vector.app.features.spaces -import androidx.lifecycle.viewModelScope -import com.airbnb.mvrx.FragmentViewModelContext +import androidx.lifecycle.asFlow import com.airbnb.mvrx.Loading -import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.Success -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.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.platform.VectorViewModel import im.vector.app.features.invite.AutoAcceptInvites import im.vector.app.features.session.coroutineScope import im.vector.app.features.settings.VectorPreferences import im.vector.app.group import im.vector.app.space -import io.reactivex.Observable -import io.reactivex.schedulers.Schedulers +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.sample import kotlinx.coroutines.launch import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.query.ActiveSpaceFilter @@ -44,54 +49,41 @@ import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.group.groupSummaryQueryParams import org.matrix.android.sdk.api.session.room.RoomSortOrder -import org.matrix.android.sdk.api.session.room.accountdata.RoomAccountDataEvent import org.matrix.android.sdk.api.session.room.accountdata.RoomAccountDataTypes import org.matrix.android.sdk.api.session.room.model.Membership -import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams import org.matrix.android.sdk.api.session.room.summary.RoomAggregateNotificationCount import org.matrix.android.sdk.api.session.space.SpaceOrderUtils import org.matrix.android.sdk.api.session.space.model.SpaceOrderContent import org.matrix.android.sdk.api.session.space.model.TopLevelSpaceComparator -import org.matrix.android.sdk.api.session.user.model.User import org.matrix.android.sdk.api.util.toMatrixItem -import org.matrix.android.sdk.rx.asObservable -import org.matrix.android.sdk.rx.rx -import java.util.concurrent.TimeUnit +import org.matrix.android.sdk.flow.flow -class SpacesListViewModel @AssistedInject constructor(@Assisted initialState: SpaceListViewState, - private val appStateHandler: AppStateHandler, - private val session: Session, - private val vectorPreferences: VectorPreferences, - private val autoAcceptInvites: AutoAcceptInvites +class SpaceListViewModel @AssistedInject constructor(@Assisted initialState: SpaceListViewState, + private val appStateHandler: AppStateHandler, + private val session: Session, + private val vectorPreferences: VectorPreferences, + private val autoAcceptInvites: AutoAcceptInvites ) : VectorViewModel(initialState) { @AssistedFactory - interface Factory { - fun create(initialState: SpaceListViewState): SpacesListViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: SpaceListViewState): SpaceListViewModel } - companion object : MvRxViewModelFactory { - - @JvmStatic - override fun create(viewModelContext: ViewModelContext, state: SpaceListViewState): SpacesListViewModel { - val groupListFragment: SpaceListFragment = (viewModelContext as FragmentViewModelContext).fragment() - return groupListFragment.spaceListViewModelFactory.create(state) - } - } + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() // private var currentGroupingMethod : RoomGroupingMethod? = null init { - session.getUserLive(session.myUserId).asObservable() - .subscribe { - setState { - copy( - myMxItem = it?.getOrNull()?.toMatrixItem()?.let { Success(it) } ?: Loading() - ) - } - }.disposeOnClear() + session.getUserLive(session.myUserId) + .asFlow() + .setOnEach { + copy( + myMxItem = it.getOrNull()?.toMatrixItem()?.let { Success(it) } ?: Loading() + ) + } observeSpaceSummaries() // observeSelectionState() @@ -107,14 +99,10 @@ class SpacesListViewModel @AssistedInject constructor(@Assisted initialState: Sp .disposeOnClear() session.getGroupSummariesLive(groupSummaryQueryParams {}) - .asObservable() - .subscribe { - setState { - copy( - legacyGroups = it - ) - } - }.disposeOnClear() + .asFlow() + .setOnEach { + copy(legacyGroups = it) + } // XXX there should be a way to refactor this and share it session.getPagedRoomSummariesLive( @@ -124,10 +112,10 @@ class SpacesListViewModel @AssistedInject constructor(@Assisted initialState: Sp !vectorPreferences.prefSpacesShowAllRoomInHome() } ?: ActiveSpaceFilter.None }, sortOrder = RoomSortOrder.NONE - ).asObservable() - .throttleFirst(300, TimeUnit.MILLISECONDS) - .observeOn(Schedulers.computation()) - .subscribe { + ).asFlow() + .sample(300) + .flowOn(Dispatchers.Default) + .onEach { val inviteCount = if (autoAcceptInvites.hideInvites) { 0 } else { @@ -152,7 +140,7 @@ class SpacesListViewModel @AssistedInject constructor(@Assisted initialState: Sp homeAggregateCount = counts ) } - }.disposeOnClear() + }.launchIn(viewModelScope) } override fun handle(action: SpaceListAction) { @@ -286,21 +274,23 @@ class SpacesListViewModel @AssistedInject constructor(@Assisted initialState: Sp null) } - val rxSession = session.rx() + val flowSession = session.flow() - Observable.combineLatest, List, List>( - rxSession + combine( + flowSession .liveUser(session.myUserId) .map { it.getOrNull() }, - rxSession + flowSession .liveSpaceSummaries(spaceSummaryQueryParams), - session.accountDataService().getLiveRoomAccountDataEvents(setOf(RoomAccountDataTypes.EVENT_TYPE_SPACE_ORDER)).asObservable(), - { _, communityGroups, _ -> - communityGroups - } - ) + session + .accountDataService() + .getLiveRoomAccountDataEvents(setOf(RoomAccountDataTypes.EVENT_TYPE_SPACE_ORDER)) + .asFlow() + ) { _, communityGroups, _ -> + communityGroups + } .execute { async -> val rootSpaces = session.spaceService().getRootSpaceSummaries() val orders = rootSpaces.map { @@ -319,7 +309,8 @@ class SpacesListViewModel @AssistedInject constructor(@Assisted initialState: Sp // clear local echos on update session.accountDataService() .getLiveRoomAccountDataEvents(setOf(RoomAccountDataTypes.EVENT_TYPE_SPACE_ORDER)) - .asObservable().execute { + .asFlow() + .execute { copy( spaceOrderLocalEchos = emptyMap() ) diff --git a/vector/src/main/java/im/vector/app/features/spaces/SpaceListViewState.kt b/vector/src/main/java/im/vector/app/features/spaces/SpaceListViewState.kt index 7482f4881e..eafc6a241e 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/SpaceListViewState.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/SpaceListViewState.kt @@ -17,7 +17,7 @@ package im.vector.app.features.spaces import com.airbnb.mvrx.Async -import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.Uninitialized import im.vector.app.RoomGroupingMethod import org.matrix.android.sdk.api.session.group.model.GroupSummary @@ -26,7 +26,7 @@ import org.matrix.android.sdk.api.session.room.summary.RoomAggregateNotification import org.matrix.android.sdk.api.util.MatrixItem data class SpaceListViewState( - val myMxItem : Async = Uninitialized, + val myMxItem: Async = Uninitialized, val asyncSpaces: Async> = Uninitialized, val selectedGroupingMethod: RoomGroupingMethod = RoomGroupingMethod.BySpace(null), val rootSpacesOrdered: List? = null, @@ -34,5 +34,5 @@ data class SpaceListViewState( val spaceOrderLocalEchos: Map? = null, val legacyGroups: List? = null, val expandedStates: Map = emptyMap(), - val homeAggregateCount : RoomAggregateNotificationCount = RoomAggregateNotificationCount(0, 0) -) : MvRxState + val homeAggregateCount: RoomAggregateNotificationCount = RoomAggregateNotificationCount(0, 0) +) : MavericksState diff --git a/vector/src/main/java/im/vector/app/features/spaces/SpaceMenuState.kt b/vector/src/main/java/im/vector/app/features/spaces/SpaceMenuState.kt index 395fcc9df1..7ac844d297 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/SpaceMenuState.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/SpaceMenuState.kt @@ -17,7 +17,7 @@ package im.vector.app.features.spaces import com.airbnb.mvrx.Async -import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.Uninitialized import org.matrix.android.sdk.api.session.room.model.RoomSummary @@ -30,7 +30,7 @@ data class SpaceMenuState( val isLastAdmin: Boolean = false, val leaveMode: LeaveMode = LeaveMode.LEAVE_NONE, val leavingState: Async = Uninitialized -) : MvRxState { +) : MavericksState { constructor(args: SpaceBottomSheetSettingsArgs) : this(spaceId = args.spaceId) enum class LeaveMode { diff --git a/vector/src/main/java/im/vector/app/features/spaces/SpaceMenuViewModel.kt b/vector/src/main/java/im/vector/app/features/spaces/SpaceMenuViewModel.kt index 24ca218942..2e9af2eacb 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/SpaceMenuViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/SpaceMenuViewModel.kt @@ -16,22 +16,23 @@ package im.vector.app.features.spaces -import com.airbnb.mvrx.ActivityViewModelContext import com.airbnb.mvrx.Fail -import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.Loading -import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized -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.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel -import im.vector.app.features.powerlevel.PowerLevelsObservableFactory +import im.vector.app.features.powerlevel.PowerLevelsFlowFactory import im.vector.app.features.session.coroutineScope +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import org.matrix.android.sdk.api.query.ActiveSpaceFilter import org.matrix.android.sdk.api.session.Session @@ -40,7 +41,7 @@ import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper import org.matrix.android.sdk.api.session.room.powerlevels.Role import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams -import org.matrix.android.sdk.rx.rx +import org.matrix.android.sdk.flow.flow import timber.log.Timber class SpaceMenuViewModel @AssistedInject constructor( @@ -50,21 +51,11 @@ class SpaceMenuViewModel @AssistedInject constructor( ) : VectorViewModel(initialState) { @AssistedFactory - interface Factory { - fun create(initialState: SpaceMenuState): SpaceMenuViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: SpaceMenuState): SpaceMenuViewModel } - companion object : MvRxViewModelFactory { - - @JvmStatic - override fun create(viewModelContext: ViewModelContext, state: SpaceMenuState): SpaceMenuViewModel? { - 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") - } - } + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() init { val roomSummary = session.getRoomSummary(initialState.spaceId) @@ -75,7 +66,7 @@ class SpaceMenuViewModel @AssistedInject constructor( session.getRoom(initialState.spaceId)?.let { room -> - room.rx().liveRoomSummary().subscribe { + room.flow().liveRoomSummary().onEach { it.getOrNull()?.let { if (it.membership == Membership.LEAVE) { setState { copy(leavingState = Success(Unit)) } @@ -85,11 +76,11 @@ class SpaceMenuViewModel @AssistedInject constructor( } } } - }.disposeOnClear() + }.launchIn(viewModelScope) - PowerLevelsObservableFactory(room) - .createObservable() - .subscribe { + PowerLevelsFlowFactory(room) + .createFlow() + .onEach { val powerLevelsHelper = PowerLevelsHelper(it) val canInvite = powerLevelsHelper.isUserAbleToInvite(session.myUserId) @@ -114,8 +105,7 @@ class SpaceMenuViewModel @AssistedInject constructor( isLastAdmin = isLastAdmin ) } - } - .disposeOnClear() + }.launchIn(viewModelScope) } } diff --git a/vector/src/main/java/im/vector/app/features/spaces/SpacePreviewActivity.kt b/vector/src/main/java/im/vector/app/features/spaces/SpacePreviewActivity.kt index 0dcaf9d754..ef65f35716 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/SpacePreviewActivity.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/SpacePreviewActivity.kt @@ -19,7 +19,8 @@ package im.vector.app.features.spaces import android.content.Context import android.content.Intent import android.os.Bundle -import com.airbnb.mvrx.MvRx +import com.airbnb.mvrx.Mavericks +import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R import im.vector.app.core.extensions.commitTransaction import im.vector.app.core.platform.VectorBaseActivity @@ -27,6 +28,7 @@ import im.vector.app.databinding.ActivitySimpleBinding import im.vector.app.features.spaces.preview.SpacePreviewArgs import im.vector.app.features.spaces.preview.SpacePreviewFragment +@AndroidEntryPoint class SpacePreviewActivity : VectorBaseActivity() { lateinit var sharedActionViewModel: SpacePreviewSharedActionViewModel @@ -50,12 +52,12 @@ class SpacePreviewActivity : VectorBaseActivity() { if (isFirstCreation()) { val simpleName = SpacePreviewFragment::class.java.simpleName - val args = intent?.getParcelableExtra(MvRx.KEY_ARG) + val args = intent?.getParcelableExtra(Mavericks.KEY_ARG) if (supportFragmentManager.findFragmentByTag(simpleName) == null) { supportFragmentManager.commitTransaction { replace(R.id.simpleFragmentContainer, SpacePreviewFragment::class.java, - Bundle().apply { this.putParcelable(MvRx.KEY_ARG, args) }, + Bundle().apply { this.putParcelable(Mavericks.KEY_ARG, args) }, simpleName ) } @@ -66,7 +68,7 @@ class SpacePreviewActivity : VectorBaseActivity() { companion object { fun newIntent(context: Context, spaceIdOrAlias: String): Intent { return Intent(context, SpacePreviewActivity::class.java).apply { - putExtra(MvRx.KEY_ARG, SpacePreviewArgs(spaceIdOrAlias)) + putExtra(Mavericks.KEY_ARG, SpacePreviewArgs(spaceIdOrAlias)) } } } diff --git a/vector/src/main/java/im/vector/app/features/spaces/SpaceSettingsMenuBottomSheet.kt b/vector/src/main/java/im/vector/app/features/spaces/SpaceSettingsMenuBottomSheet.kt index 040f1f9057..7449868292 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/SpaceSettingsMenuBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/SpaceSettingsMenuBottomSheet.kt @@ -26,7 +26,7 @@ import com.airbnb.mvrx.Success import com.airbnb.mvrx.args import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState -import im.vector.app.core.di.ScreenComponent +import dagger.hilt.android.AndroidEntryPoint import im.vector.app.core.extensions.setTextOrHide import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment import im.vector.app.databinding.BottomSheetSpaceSettingsBinding @@ -46,12 +46,12 @@ data class SpaceBottomSheetSettingsArgs( val spaceId: String ) : Parcelable -class SpaceSettingsMenuBottomSheet : VectorBaseBottomSheetDialogFragment(), SpaceMenuViewModel.Factory { +@AndroidEntryPoint +class SpaceSettingsMenuBottomSheet : VectorBaseBottomSheetDialogFragment() { @Inject lateinit var navigator: Navigator @Inject lateinit var avatarRenderer: AvatarRenderer @Inject lateinit var bugReporter: BugReporter - @Inject lateinit var viewModelFactory: SpaceMenuViewModel.Factory private val spaceArgs: SpaceBottomSheetSettingsArgs by args() @@ -65,10 +65,6 @@ class SpaceSettingsMenuBottomSheet : VectorBaseBottomSheetDialogFragment + sharedViewModel.onEach { state -> views.accessInfoHelpText.text = stringProvider.getString(R.string.create_spaces_make_sure_access, state.name ?: "") } } diff --git a/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceState.kt b/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceState.kt index 6fb5853269..57782d9182 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceState.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceState.kt @@ -18,7 +18,7 @@ package im.vector.app.features.spaces.create import android.net.Uri import com.airbnb.mvrx.Async -import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.Uninitialized data class CreateSpaceState( @@ -28,7 +28,7 @@ data class CreateSpaceState( val step: Step = Step.ChooseType, val spaceType: SpaceType? = null, val spaceTopology: SpaceTopology? = null, - val homeServerName: String? = null, + val homeServerName: String = "", val aliasLocalPart: String? = null, val aliasManuallyModified: Boolean = false, val aliasVerificationTask: Async = Uninitialized, @@ -38,7 +38,7 @@ data class CreateSpaceState( val emailValidationResult: Map? = null, val creationResult: Async = Uninitialized, val canInviteByMail: Boolean = false -) : MvRxState { +) : MavericksState { enum class Step { ChooseType, diff --git a/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceViewModel.kt b/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceViewModel.kt index e6ead2294e..8ddeab3223 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceViewModel.kt @@ -16,12 +16,9 @@ package im.vector.app.features.spaces.create -import androidx.lifecycle.viewModelScope -import com.airbnb.mvrx.ActivityViewModelContext import com.airbnb.mvrx.Fail -import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.Loading -import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.ViewModelContext @@ -29,6 +26,8 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import im.vector.app.R +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.isEmail @@ -76,8 +75,8 @@ class CreateSpaceViewModel @AssistedInject constructor( } @AssistedFactory - interface Factory { - fun create(initialState: CreateSpaceState): CreateSpaceViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: CreateSpaceState): CreateSpaceViewModel } private fun startListenToIdentityManager() { @@ -93,17 +92,9 @@ class CreateSpaceViewModel @AssistedInject constructor( super.onCleared() } - companion object : MvRxViewModelFactory { + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() { - override fun create(viewModelContext: ViewModelContext, state: CreateSpaceState): CreateSpaceViewModel? { - 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 initialState(viewModelContext: ViewModelContext): CreateSpaceState? { + override fun initialState(viewModelContext: ViewModelContext): CreateSpaceState { return CreateSpaceState( defaultRooms = mapOf( 0 to viewModelContext.activity.getString(R.string.create_spaces_default_public_room_name), @@ -134,7 +125,7 @@ class CreateSpaceViewModel @AssistedInject constructor( ) } else { val tentativeAlias = - MatrixPatterns.candidateAliasFromRoomName(action.name) + MatrixPatterns.candidateAliasFromRoomName(action.name, homeServerName) copy( nameInlineError = null, name = action.name, diff --git a/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceViewModelTask.kt b/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceViewModelTask.kt index f6f168c365..00b4b64296 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceViewModelTask.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceViewModelTask.kt @@ -35,7 +35,6 @@ import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesAllowEntry 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.RestrictedRoomPreset - import org.matrix.android.sdk.api.session.room.powerlevels.Role import org.matrix.android.sdk.api.session.space.CreateSpaceParams import timber.log.Timber diff --git a/vector/src/main/java/im/vector/app/features/spaces/create/SpaceDetailEpoxyController.kt b/vector/src/main/java/im/vector/app/features/spaces/create/SpaceDetailEpoxyController.kt index 27c08d1f6f..14b0db2cd1 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/create/SpaceDetailEpoxyController.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/create/SpaceDetailEpoxyController.kt @@ -27,6 +27,7 @@ import im.vector.app.features.form.formEditableSquareAvatarItem import im.vector.app.features.form.formMultiLineEditTextItem import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.roomdirectory.createroom.RoomAliasErrorFormatter +import org.matrix.android.sdk.api.MatrixConstants import org.matrix.android.sdk.api.session.room.alias.RoomAliasError import org.matrix.android.sdk.api.util.MatrixItem import javax.inject.Inject @@ -94,6 +95,7 @@ class SpaceDetailEpoxyController @Inject constructor( hint(host.stringProvider.getString(R.string.create_space_alias_hint)) suffixText(":" + data.homeServerName) prefixText("#") + maxLength(MatrixConstants.maxAliasLocalPartLength(data.homeServerName)) onFocusChange { hasFocus -> host.aliasTextIsFocused = hasFocus } diff --git a/vector/src/main/java/im/vector/app/features/spaces/create/WizardButtonView.kt b/vector/src/main/java/im/vector/app/features/spaces/create/WizardButtonView.kt index fc06ac4f7e..971d61dd9c 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/create/WizardButtonView.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/create/WizardButtonView.kt @@ -29,8 +29,8 @@ import im.vector.app.R import im.vector.app.core.extensions.setTextOrHide import im.vector.app.databinding.ViewSpaceTypeButtonBinding -class WizardButtonView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) - : ConstraintLayout(context, attrs, defStyle) { +class WizardButtonView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) : + ConstraintLayout(context, attrs, defStyle) { private val views: ViewSpaceTypeButtonBinding diff --git a/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryFragment.kt b/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryFragment.kt index 3af66cbb6b..cd7d6a379a 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryFragment.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryFragment.kt @@ -25,6 +25,7 @@ import android.view.View import android.view.ViewGroup import androidx.core.text.toSpannable import androidx.core.view.isVisible +import androidx.lifecycle.lifecycleScope import com.airbnb.epoxy.EpoxyVisibilityTracker import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.withState @@ -46,8 +47,7 @@ import im.vector.app.features.permalink.PermalinkHandler import im.vector.app.features.spaces.manage.ManageType import im.vector.app.features.spaces.manage.SpaceAddRoomSpaceChooserBottomSheet import im.vector.app.features.spaces.manage.SpaceManageActivity -import io.reactivex.android.schedulers.AndroidSchedulers -import io.reactivex.schedulers.Schedulers +import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo import java.net.URL @@ -110,7 +110,7 @@ class SpaceDirectoryFragment @Inject constructor( views.spaceDirectoryList.configureWith(epoxyController) epoxyVisibilityTracker.attach(views.spaceDirectoryList) - viewModel.selectSubscribe(this, SpaceDirectoryState::canAddRooms) { + viewModel.onEach(SpaceDirectoryState::canAddRooms) { invalidateOptionsMenu() } @@ -200,33 +200,29 @@ class SpaceDirectoryFragment @Inject constructor( } override fun onUrlClicked(url: String, title: String): Boolean { - permalinkHandler - .launch(requireActivity(), url, null) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { managed -> - if (!managed) { - if (title.isValidUrl() && url.isValidUrl() && URL(title).host != URL(url).host) { - MaterialAlertDialogBuilder(requireActivity(), R.style.ThemeOverlay_Vector_MaterialAlertDialog_Destructive) - .setTitle(R.string.external_link_confirmation_title) - .setMessage( - getString(R.string.external_link_confirmation_message, title, url) - .toSpannable() - .colorizeMatchingText(url, colorProvider.getColorFromAttribute(R.attr.vctr_content_tertiary)) - .colorizeMatchingText(title, colorProvider.getColorFromAttribute(R.attr.vctr_content_tertiary)) - ) - .setPositiveButton(R.string._continue) { _, _ -> - openUrlInExternalBrowser(requireContext(), url) - } - .setNegativeButton(R.string.cancel, null) - .show() - } else { - // Open in external browser, in a new Tab - openUrlInExternalBrowser(requireContext(), url) - } - } + viewLifecycleOwner.lifecycleScope.launch { + val isHandled = permalinkHandler.launch(requireActivity(), url, null) + if (!isHandled) { + if (title.isValidUrl() && url.isValidUrl() && URL(title).host != URL(url).host) { + MaterialAlertDialogBuilder(requireActivity(), R.style.ThemeOverlay_Vector_MaterialAlertDialog_Destructive) + .setTitle(R.string.external_link_confirmation_title) + .setMessage( + getString(R.string.external_link_confirmation_message, title, url) + .toSpannable() + .colorizeMatchingText(url, colorProvider.getColorFromAttribute(R.attr.vctr_content_tertiary)) + .colorizeMatchingText(title, colorProvider.getColorFromAttribute(R.attr.vctr_content_tertiary)) + ) + .setPositiveButton(R.string._continue) { _, _ -> + openUrlInExternalBrowser(requireContext(), url) + } + .setNegativeButton(R.string.cancel, null) + .show() + } else { + // Open in external browser, in a new Tab + openUrlInExternalBrowser(requireContext(), url) } - .disposeOnDestroyView() + } + } // In fact it is always managed return true } diff --git a/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryState.kt b/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryState.kt index 33b494075d..1467b69659 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryState.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryState.kt @@ -17,7 +17,7 @@ package im.vector.app.features.spaces.explore import com.airbnb.mvrx.Async -import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MavericksState import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo @@ -39,7 +39,7 @@ data class SpaceDirectoryState( // cached room summaries of known rooms, we use it because computed room name would be better using it val knownRoomSummaries: List = emptyList(), val paginationStatus: Map> = emptyMap() -) : MvRxState { +) : MavericksState { constructor(args: SpaceDirectoryArgs) : this( spaceId = args.spaceId ) diff --git a/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryViewModel.kt b/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryViewModel.kt index 1006f5a570..d7bdf4f511 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryViewModel.kt @@ -16,21 +16,22 @@ package im.vector.app.features.spaces.explore -import androidx.lifecycle.viewModelScope -import com.airbnb.mvrx.ActivityViewModelContext import com.airbnb.mvrx.Fail -import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.Loading -import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized -import com.airbnb.mvrx.ViewModelContext import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.platform.VectorViewModel -import im.vector.app.features.powerlevel.PowerLevelsObservableFactory +import im.vector.app.features.powerlevel.PowerLevelsFlowFactory import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.events.model.EventType @@ -41,7 +42,7 @@ import org.matrix.android.sdk.api.session.room.model.RoomType import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams -import org.matrix.android.sdk.rx.rx +import org.matrix.android.sdk.flow.flow import timber.log.Timber class SpaceDirectoryViewModel @AssistedInject constructor( @@ -50,19 +51,11 @@ class SpaceDirectoryViewModel @AssistedInject constructor( ) : VectorViewModel(initialState) { @AssistedFactory - interface Factory { - fun create(initialState: SpaceDirectoryState): SpaceDirectoryViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: SpaceDirectoryState): SpaceDirectoryViewModel } - companion object : MvRxViewModelFactory { - override fun create(viewModelContext: ViewModelContext, state: SpaceDirectoryState): SpaceDirectoryViewModel? { - 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") - } - } + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() init { @@ -83,17 +76,17 @@ class SpaceDirectoryViewModel @AssistedInject constructor( private fun observePermissions() { val room = session.getRoom(initialState.spaceId) ?: return - val powerLevelsContentLive = PowerLevelsObservableFactory(room).createObservable() + val powerLevelsContentLive = PowerLevelsFlowFactory(room).createFlow() powerLevelsContentLive - .subscribe { + .onEach { val powerLevelsHelper = PowerLevelsHelper(it) setState { copy(canAddRooms = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_SPACE_CHILD)) } } - .disposeOnClear() + .launchIn(viewModelScope) } private fun refreshFromApi(rootId: String?) = withState { state -> @@ -146,7 +139,7 @@ class SpaceDirectoryViewModel @AssistedInject constructor( excludeType = null } session - .rx() + .flow() .liveRoomSummaries(queryParams) .map { it.map { it.roomId }.toSet() @@ -157,12 +150,11 @@ class SpaceDirectoryViewModel @AssistedInject constructor( } private fun observeMembershipChanges() { - session.rx() + session.flow() .liveRoomChangeMembershipState() - .subscribe { - setState { copy(changeMembershipStates = it) } + .setOnEach { + copy(changeMembershipStates = it) } - .disposeOnClear() } override fun handle(action: SpaceDirectoryViewAction) { diff --git a/vector/src/main/java/im/vector/app/features/spaces/invite/SpaceInviteBottomSheet.kt b/vector/src/main/java/im/vector/app/features/spaces/invite/SpaceInviteBottomSheet.kt index 28c06d84d1..bd6dec7c4b 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/invite/SpaceInviteBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/invite/SpaceInviteBottomSheet.kt @@ -30,19 +30,21 @@ import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.args import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState +import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R -import im.vector.app.core.di.ScreenComponent import im.vector.app.core.platform.ButtonStateView import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment import im.vector.app.core.utils.toast import im.vector.app.databinding.BottomSheetInvitedToSpaceBinding +import im.vector.app.features.displayname.getBestName import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.matrixto.SpaceCardRenderer import kotlinx.parcelize.Parcelize import org.matrix.android.sdk.api.util.toMatrixItem import javax.inject.Inject -class SpaceInviteBottomSheet : VectorBaseBottomSheetDialogFragment(), SpaceInviteBottomSheetViewModel.Factory { +@AndroidEntryPoint +class SpaceInviteBottomSheet : VectorBaseBottomSheetDialogFragment() { interface InteractionListener { fun spaceInviteBottomSheetOnAccept(spaceId: String) @@ -56,22 +58,11 @@ class SpaceInviteBottomSheet : VectorBaseBottomSheetDialogFragment> = Uninitialized, val joinActionState: Async = Uninitialized, val rejectActionState: Async = Uninitialized -) : MvRxState { +) : MavericksState { constructor(args: SpaceInviteBottomSheet.Args) : this( spaceId = args.spaceId ) diff --git a/vector/src/main/java/im/vector/app/features/spaces/invite/SpaceInviteBottomSheetViewModel.kt b/vector/src/main/java/im/vector/app/features/spaces/invite/SpaceInviteBottomSheetViewModel.kt index 4524b57004..79ad043813 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/invite/SpaceInviteBottomSheetViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/invite/SpaceInviteBottomSheetViewModel.kt @@ -16,23 +16,27 @@ package im.vector.app.features.spaces.invite -import com.airbnb.mvrx.ActivityViewModelContext +import androidx.lifecycle.viewModelScope import com.airbnb.mvrx.Fail -import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.Loading -import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized -import com.airbnb.mvrx.ViewModelContext import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.platform.VectorViewModel import im.vector.app.features.session.coroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.api.session.room.model.RoomSummary +import org.matrix.android.sdk.api.session.room.peeking.PeekResult class SpaceInviteBottomSheetViewModel @AssistedInject constructor( @Assisted private val initialState: SpaceInviteBottomSheetState, @@ -42,7 +46,6 @@ class SpaceInviteBottomSheetViewModel @AssistedInject constructor( init { session.getRoomSummary(initialState.spaceId)?.let { roomSummary -> - val knownMembers = roomSummary.otherMemberIds.filter { session.getExistingDirectRoomWithUser(it) != null }.mapNotNull { session.getUser(it) } @@ -57,24 +60,43 @@ class SpaceInviteBottomSheetViewModel @AssistedInject constructor( peopleYouKnow = Success(peopleYouKnow) ) } + if (roomSummary.membership == Membership.INVITE) { + getLatestRoomSummary(roomSummary) + } + } + } + + /** + * Try to request the room summary api to get more info + */ + private fun getLatestRoomSummary(roomSummary: RoomSummary) { + viewModelScope.launch(Dispatchers.IO) { + val peekResult = tryOrNull { session.peekRoom(roomSummary.roomId) } as? PeekResult.Success ?: return@launch + setState { + copy( + summary = Success( + roomSummary.copy( + joinedMembersCount = peekResult.numJoinedMembers, + // it's also possible that the name/avatar did change since the invite.. + // if it's null keep the old one as summary API might not be available + // and peek result could be null for other reasons (not peekable) + avatarUrl = peekResult.avatarUrl ?: roomSummary.avatarUrl, + displayName = peekResult.name ?: roomSummary.displayName, + topic = peekResult.topic ?: roomSummary.topic + // maybe use someMembers field later? + ) + ) + ) + } } } @AssistedFactory - interface Factory { - fun create(initialState: SpaceInviteBottomSheetState): SpaceInviteBottomSheetViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: SpaceInviteBottomSheetState): SpaceInviteBottomSheetViewModel } - companion object : MvRxViewModelFactory { - - override fun create(viewModelContext: ViewModelContext, state: SpaceInviteBottomSheetState): SpaceInviteBottomSheetViewModel? { - 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") - } - } + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() override fun handle(action: SpaceInviteBottomSheetAction) { when (action) { diff --git a/vector/src/main/java/im/vector/app/features/spaces/leave/SelectChildrenController.kt b/vector/src/main/java/im/vector/app/features/spaces/leave/SelectChildrenController.kt index 88f3e1c9ff..39d1d72675 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/leave/SelectChildrenController.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/leave/SelectChildrenController.kt @@ -95,8 +95,8 @@ class SelectChildrenController @Inject constructor( } // if filter is "Jo Do", it should match "John Doe" return filter.split(" ").all { - roomSummary.name.contains(it, ignoreCase = true).orFalse() - || roomSummary.topic.contains(it, ignoreCase = true).orFalse() + roomSummary.name.contains(it, ignoreCase = true).orFalse() || + roomSummary.topic.contains(it, ignoreCase = true).orFalse() } } } diff --git a/vector/src/main/java/im/vector/app/features/spaces/leave/SpaceLeaveAdvanceViewState.kt b/vector/src/main/java/im/vector/app/features/spaces/leave/SpaceLeaveAdvanceViewState.kt index f7802d2a31..b8dcd3f7a2 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/leave/SpaceLeaveAdvanceViewState.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/leave/SpaceLeaveAdvanceViewState.kt @@ -17,7 +17,7 @@ package im.vector.app.features.spaces.leave import com.airbnb.mvrx.Async -import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.Uninitialized import im.vector.app.features.spaces.SpaceBottomSheetSettingsArgs import org.matrix.android.sdk.api.session.room.model.RoomSummary @@ -29,7 +29,7 @@ data class SpaceLeaveAdvanceViewState( val selectedRooms: List = emptyList(), val currentFilter: String = "", val leaveState: Async = Uninitialized -) : MvRxState { +) : MavericksState { constructor(args: SpaceBottomSheetSettingsArgs) : this( spaceId = args.spaceId ) diff --git a/vector/src/main/java/im/vector/app/features/spaces/leave/SpaceLeaveAdvancedActivity.kt b/vector/src/main/java/im/vector/app/features/spaces/leave/SpaceLeaveAdvancedActivity.kt index cb66708324..541d883405 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/leave/SpaceLeaveAdvancedActivity.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/leave/SpaceLeaveAdvancedActivity.kt @@ -23,13 +23,13 @@ import androidx.core.view.isGone import androidx.core.view.isVisible import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Loading -import com.airbnb.mvrx.MvRx +import com.airbnb.mvrx.Mavericks import com.airbnb.mvrx.Success import com.airbnb.mvrx.viewModel import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.dialog.MaterialAlertDialogBuilder +import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R -import im.vector.app.core.di.ScreenComponent import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.extensions.commitTransaction import im.vector.app.core.extensions.hideKeyboard @@ -40,24 +40,16 @@ import im.vector.app.databinding.ActivitySimpleLoadingBinding import im.vector.app.features.spaces.SpaceBottomSheetSettingsArgs import javax.inject.Inject +@AndroidEntryPoint class SpaceLeaveAdvancedActivity : VectorBaseActivity(), - SpaceLeaveAdvancedViewModel.Factory, ToolbarConfigurable { override fun getBinding(): ActivitySimpleLoadingBinding = ActivitySimpleLoadingBinding.inflate(layoutInflater) val leaveViewModel: SpaceLeaveAdvancedViewModel by viewModel() - @Inject lateinit var viewModelFactory: SpaceLeaveAdvancedViewModel.Factory @Inject lateinit var errorFormatter: ErrorFormatter - override fun create(initialState: SpaceLeaveAdvanceViewState) = viewModelFactory.create(initialState) - - override fun injectWith(injector: ScreenComponent) { - super.injectWith(injector) - injector.inject(this) - } - override fun showWaitingView(text: String?) { hideKeyboard() views.waitingView.waitingStatusText.isGone = views.waitingView.waitingStatusText.text.isNullOrBlank() @@ -74,7 +66,7 @@ class SpaceLeaveAdvancedActivity : VectorBaseActivity(MvRx.KEY_ARG) + val args = intent?.getParcelableExtra(Mavericks.KEY_ARG) if (isFirstCreation()) { val simpleName = SpaceLeaveAdvancedFragment::class.java.simpleName @@ -83,7 +75,7 @@ class SpaceLeaveAdvancedActivity : VectorBaseActivity - room.rx().liveRoomSummary().subscribe { - it.getOrNull()?.let { - if (it.membership == Membership.LEAVE) { - setState { copy(leaveState = Success(Unit)) } - if (appStateHandler.safeActiveSpaceId() == initialState.spaceId) { - // switch to home? - appStateHandler.setCurrentSpace(null, session) + room.flow().liveRoomSummary() + .unwrap() + .onEach { + if (it.membership == Membership.LEAVE) { + setState { copy(leaveState = Success(Unit)) } + if (appStateHandler.safeActiveSpaceId() == initialState.spaceId) { + // switch to home? + appStateHandler.setCurrentSpace(null, session) + } } - } - } - } + }.launchIn(viewModelScope) } viewModelScope.launch { @@ -125,17 +126,9 @@ class SpaceLeaveAdvancedViewModel @AssistedInject constructor( } @AssistedFactory - interface Factory { - fun create(initialState: SpaceLeaveAdvanceViewState): SpaceLeaveAdvancedViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: SpaceLeaveAdvanceViewState): SpaceLeaveAdvancedViewModel } - companion object : MvRxViewModelFactory { - override fun create(viewModelContext: ViewModelContext, state: SpaceLeaveAdvanceViewState): SpaceLeaveAdvancedViewModel? { - 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") - } - } + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() } diff --git a/vector/src/main/java/im/vector/app/features/spaces/manage/AddRoomListController.kt b/vector/src/main/java/im/vector/app/features/spaces/manage/AddRoomListController.kt index e23cd1ce3f..15103dd870 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/manage/AddRoomListController.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/manage/AddRoomListController.kt @@ -43,8 +43,8 @@ class AddRoomListController @Inject constructor( override fun areContentsTheSame(oldItem: RoomSummary, newItem: RoomSummary): Boolean { // for this use case we can test less things - return oldItem.displayName == newItem.displayName - && oldItem.avatarUrl == newItem.avatarUrl + return oldItem.displayName == newItem.displayName && + oldItem.avatarUrl == newItem.avatarUrl } } ) { diff --git a/vector/src/main/java/im/vector/app/features/spaces/manage/RoomManageSelectionItem.kt b/vector/src/main/java/im/vector/app/features/spaces/manage/RoomManageSelectionItem.kt index 566750fbae..21da4f0dfe 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/manage/RoomManageSelectionItem.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/manage/RoomManageSelectionItem.kt @@ -27,6 +27,7 @@ import im.vector.app.core.epoxy.ClickListener import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.epoxy.onClick +import im.vector.app.features.displayname.getBestName import im.vector.app.features.home.AvatarRenderer import org.matrix.android.sdk.api.util.MatrixItem diff --git a/vector/src/main/java/im/vector/app/features/spaces/manage/RoomSelectionItem.kt b/vector/src/main/java/im/vector/app/features/spaces/manage/RoomSelectionItem.kt index fde4781884..df736bebbb 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/manage/RoomSelectionItem.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/manage/RoomSelectionItem.kt @@ -26,6 +26,7 @@ import im.vector.app.core.epoxy.ClickListener import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.epoxy.onClick +import im.vector.app.features.displayname.getBestName import im.vector.app.features.home.AvatarRenderer import org.matrix.android.sdk.api.util.MatrixItem diff --git a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceAddRoomFragment.kt b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceAddRoomFragment.kt index e192ec3c88..5dbd35fc20 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceAddRoomFragment.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceAddRoomFragment.kt @@ -44,9 +44,8 @@ class SpaceAddRoomFragment @Inject constructor( private val spaceEpoxyController: AddRoomListController, private val roomEpoxyController: AddRoomListController, private val dmEpoxyController: AddRoomListController, - private val viewModelFactory: SpaceAddRoomsViewModel.Factory ) : VectorBaseFragment(), - OnBackPressed, AddRoomListController.Listener, SpaceAddRoomsViewModel.Factory { + OnBackPressed, AddRoomListController.Listener { override fun getBinding(inflater: LayoutInflater, container: ViewGroup?) = FragmentSpaceAddRoomsBinding.inflate(layoutInflater, container, false) @@ -55,9 +54,6 @@ class SpaceAddRoomFragment @Inject constructor( private val sharedViewModel: SpaceManageSharedViewModel by activityViewModel() - override fun create(initialState: SpaceAddRoomsState): SpaceAddRoomsViewModel = - viewModelFactory.create(initialState) - override fun getMenuRes(): Int = R.menu.menu_space_add_room private var saveNeeded = false @@ -91,35 +87,35 @@ class SpaceAddRoomFragment @Inject constructor( invalidateOptionsMenu() } - viewModel.selectSubscribe(this, SpaceAddRoomsState::spaceName) { + viewModel.onEach(SpaceAddRoomsState::spaceName) { views.appBarSpaceInfo.text = it - }.disposeOnDestroyView() + } - viewModel.selectSubscribe(this, SpaceAddRoomsState::ignoreRooms) { + viewModel.onEach(SpaceAddRoomsState::ignoreRooms) { spaceEpoxyController.ignoreRooms = it roomEpoxyController.ignoreRooms = it dmEpoxyController.ignoreRooms = it - }.disposeOnDestroyView() + } - viewModel.selectSubscribe(this, SpaceAddRoomsState::isSaving) { + viewModel.onEach(SpaceAddRoomsState::isSaving) { if (it is Loading) { sharedViewModel.handle(SpaceManagedSharedAction.ShowLoading) } else { sharedViewModel.handle(SpaceManagedSharedAction.HideLoading) } - }.disposeOnDestroyView() + } - viewModel.selectSubscribe(this, SpaceAddRoomsState::shouldShowDMs) { + viewModel.onEach(SpaceAddRoomsState::shouldShowDMs) { dmEpoxyController.disabled = !it - }.disposeOnDestroyView() + } - viewModel.selectSubscribe(this, SpaceAddRoomsState::onlyShowSpaces) { + viewModel.onEach(SpaceAddRoomsState::onlyShowSpaces) { spaceEpoxyController.disabled = !it roomEpoxyController.disabled = it views.createNewRoom.text = if (it) getString(R.string.create_space) else getString(R.string.create_new_room) val title = if (it) getString(R.string.space_add_existing_spaces) else getString(R.string.space_add_existing_rooms_only) views.appBarTitle.text = title - }.disposeOnDestroyView() + } views.createNewRoom.debouncedClicks { withState(viewModel) { state -> diff --git a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceAddRoomSpaceChooserBottomSheet.kt b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceAddRoomSpaceChooserBottomSheet.kt index d0f5a9e8ba..971ff7e0b1 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceAddRoomSpaceChooserBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceAddRoomSpaceChooserBottomSheet.kt @@ -56,8 +56,7 @@ class SpaceAddRoomSpaceChooserBottomSheet : VectorBaseBottomSheetDialogFragment< const val ACTION_ADD_ROOMS = "Action.AddRoom" const val ACTION_ADD_SPACES = "Action.AddSpaces" - fun newInstance() - : SpaceAddRoomSpaceChooserBottomSheet { + fun newInstance(): SpaceAddRoomSpaceChooserBottomSheet { return SpaceAddRoomSpaceChooserBottomSheet() } } diff --git a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceAddRoomsState.kt b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceAddRoomsState.kt index e941d04b22..bec8b905d8 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceAddRoomsState.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceAddRoomsState.kt @@ -17,7 +17,7 @@ package im.vector.app.features.spaces.manage import com.airbnb.mvrx.Async -import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.Uninitialized data class SpaceAddRoomsState( @@ -30,7 +30,7 @@ data class SpaceAddRoomsState( val shouldShowDMs: Boolean = false, val onlyShowSpaces: Boolean = false // val selectionList: Map = emptyMap() -) : MvRxState { +) : MavericksState { constructor(args: SpaceManageArgs) : this( spaceId = args.spaceId, onlyShowSpaces = args.manageType == ManageType.AddRoomsOnlySpaces diff --git a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceAddRoomsViewEvents.kt b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceAddRoomsViewEvents.kt index cb5571b107..53de1b7ee4 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceAddRoomsViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceAddRoomsViewEvents.kt @@ -21,5 +21,5 @@ import im.vector.app.core.platform.VectorViewEvents sealed class SpaceAddRoomsViewEvents : VectorViewEvents { object WarnUnsavedChanged : SpaceAddRoomsViewEvents() object SavedDone : SpaceAddRoomsViewEvents() - data class SaveFailed(val reason: Throwable): SpaceAddRoomsViewEvents() + data class SaveFailed(val reason: Throwable) : SpaceAddRoomsViewEvents() } diff --git a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceAddRoomsViewModel.kt b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceAddRoomsViewModel.kt index 9f5cd7d35e..8fa269d439 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceAddRoomsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceAddRoomsViewModel.kt @@ -19,16 +19,15 @@ package im.vector.app.features.spaces.manage import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import androidx.paging.PagedList -import com.airbnb.mvrx.ActivityViewModelContext import com.airbnb.mvrx.Fail -import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.Loading -import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.Success -import com.airbnb.mvrx.ViewModelContext import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.platform.VectorViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -55,10 +54,12 @@ class SpaceAddRoomsViewModel @AssistedInject constructor( ) : VectorViewModel(initialState) { @AssistedFactory - interface Factory { - fun create(initialState: SpaceAddRoomsState): SpaceAddRoomsViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: SpaceAddRoomsState): SpaceAddRoomsViewModel } + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() + val updatableLiveSpacePageResult: UpdatableLivePageResult by lazy { session.getFilteredPagedRoomSummariesLive( roomSummaryQueryParams { @@ -132,16 +133,6 @@ class SpaceAddRoomsViewModel @AssistedInject constructor( } } - companion object : MvRxViewModelFactory { - override fun create(viewModelContext: ViewModelContext, state: SpaceAddRoomsState): SpaceAddRoomsViewModel? { - 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") - } - } - fun canGoBack(): Boolean { val needToSave = selectionList.values.any { it } if (needToSave) { diff --git a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceChildInfoMatchFilter.kt b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceChildInfoMatchFilter.kt index 17f5d13e19..66878a4011 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceChildInfoMatchFilter.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceChildInfoMatchFilter.kt @@ -30,8 +30,8 @@ class SpaceChildInfoMatchFilter : Predicate { } // if filter is "Jo Do", it should match "John Doe" return filter.split(" ").all { - spaceChildInfo.name?.contains(it, ignoreCase = true).orFalse() - || spaceChildInfo.topic?.contains(it, ignoreCase = true).orFalse() + spaceChildInfo.name?.contains(it, ignoreCase = true).orFalse() || + spaceChildInfo.topic?.contains(it, ignoreCase = true).orFalse() } } } diff --git a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageActivity.kt b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageActivity.kt index 16a1e18da2..2dae088c2e 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageActivity.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageActivity.kt @@ -22,12 +22,12 @@ import android.os.Bundle import android.os.Parcelable import androidx.core.view.isGone import androidx.core.view.isVisible -import com.airbnb.mvrx.MvRx +import com.airbnb.mvrx.Mavericks import com.airbnb.mvrx.viewModel import com.airbnb.mvrx.withState import com.google.android.material.appbar.MaterialToolbar +import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R -import im.vector.app.core.di.ScreenComponent import im.vector.app.core.extensions.addFragmentToBackstack import im.vector.app.core.extensions.commitTransaction import im.vector.app.core.extensions.hideKeyboard @@ -42,7 +42,6 @@ import im.vector.app.features.roomprofile.RoomProfileArgs import im.vector.app.features.roomprofile.alias.RoomAliasFragment import im.vector.app.features.roomprofile.permissions.RoomPermissionsFragment import kotlinx.parcelize.Parcelize -import javax.inject.Inject @Parcelize data class SpaceManageArgs( @@ -50,17 +49,12 @@ data class SpaceManageArgs( val manageType: ManageType ) : Parcelable +@AndroidEntryPoint class SpaceManageActivity : VectorBaseActivity(), - ToolbarConfigurable, - SpaceManageSharedViewModel.Factory { + ToolbarConfigurable { - @Inject lateinit var sharedViewModelFactory: SpaceManageSharedViewModel.Factory private lateinit var sharedDirectoryActionViewModel: RoomDirectorySharedActionViewModel - override fun injectWith(injector: ScreenComponent) { - injector.inject(this) - } - override fun getBinding(): ActivitySimpleLoadingBinding = ActivitySimpleLoadingBinding.inflate(layoutInflater) override fun getTitleRes(): Int = R.string.space_add_existing_rooms @@ -95,7 +89,7 @@ class SpaceManageActivity : VectorBaseActivity(), } .disposeOnDestroy() - val args = intent?.getParcelableExtra(MvRx.KEY_ARG) + val args = intent?.getParcelableExtra(Mavericks.KEY_ARG) if (isFirstCreation()) { withState(sharedViewModel) { when (it.manageType) { @@ -106,7 +100,7 @@ class SpaceManageActivity : VectorBaseActivity(), supportFragmentManager.commitTransaction { replace(R.id.simpleFragmentContainer, SpaceAddRoomFragment::class.java, - Bundle().apply { this.putParcelable(MvRx.KEY_ARG, args) }, + Bundle().apply { this.putParcelable(Mavericks.KEY_ARG, args) }, simpleName ) } @@ -118,7 +112,7 @@ class SpaceManageActivity : VectorBaseActivity(), supportFragmentManager.commitTransaction { replace(R.id.simpleFragmentContainer, SpaceSettingsFragment::class.java, - Bundle().apply { this.putParcelable(MvRx.KEY_ARG, RoomProfileArgs(args.spaceId)) }, + Bundle().apply { this.putParcelable(Mavericks.KEY_ARG, RoomProfileArgs(args.spaceId)) }, simpleName ) } @@ -189,13 +183,11 @@ class SpaceManageActivity : VectorBaseActivity(), companion object { fun newIntent(context: Context, spaceId: String, manageType: ManageType): Intent { return Intent(context, SpaceManageActivity::class.java).apply { - putExtra(MvRx.KEY_ARG, SpaceManageArgs(spaceId, manageType)) + putExtra(Mavericks.KEY_ARG, SpaceManageArgs(spaceId, manageType)) } } } - override fun create(initialState: SpaceManageViewState) = sharedViewModelFactory.create(initialState) - override fun configure(toolbar: MaterialToolbar) { configureToolbar(toolbar) } diff --git a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageRoomViewState.kt b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageRoomViewState.kt index 34173828a7..211d3645f5 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageRoomViewState.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageRoomViewState.kt @@ -17,7 +17,7 @@ package im.vector.app.features.spaces.manage import com.airbnb.mvrx.Async -import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.Uninitialized import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.space.SpaceHierarchyData @@ -32,7 +32,7 @@ data class SpaceManageRoomViewState( val paginationStatus: Async = Uninitialized, // cached room summaries of known rooms, we use it because computed room name would be better using it val knownRoomSummaries: List = emptyList() -) : MvRxState { +) : MavericksState { constructor(args: SpaceManageArgs) : this( spaceId = args.spaceId ) diff --git a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageRoomsFragment.kt b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageRoomsFragment.kt index 186d733982..5fbac3bb6a 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageRoomsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageRoomsFragment.kt @@ -45,10 +45,8 @@ import java.util.concurrent.TimeUnit import javax.inject.Inject class SpaceManageRoomsFragment @Inject constructor( - private val viewModelFactory: SpaceManageRoomsViewModel.Factory, private val epoxyController: SpaceManageRoomsController ) : VectorBaseFragment(), - SpaceManageRoomsViewModel.Factory, OnBackPressed, SpaceManageRoomsController.Listener, Callback { @@ -80,7 +78,7 @@ class SpaceManageRoomsFragment @Inject constructor( } .disposeOnDestroyView() - viewModel.selectSubscribe(SpaceManageRoomViewState::actionState) { actionState -> + viewModel.onEach(SpaceManageRoomViewState::actionState) { actionState -> when (actionState) { is Loading -> { sharedViewModel.handle(SpaceManagedSharedAction.ShowLoading) @@ -107,8 +105,6 @@ class SpaceManageRoomsFragment @Inject constructor( super.onDestroyView() } - override fun create(initialState: SpaceManageRoomViewState) = viewModelFactory.create(initialState) - override fun invalidate() = withState(viewModel) { state -> epoxyController.setData(state) diff --git a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageRoomsViewModel.kt b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageRoomsViewModel.kt index b1f6d5c3c3..a1dd26a936 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageRoomsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageRoomsViewModel.kt @@ -16,18 +16,16 @@ package im.vector.app.features.spaces.manage -import androidx.lifecycle.viewModelScope -import com.airbnb.mvrx.ActivityViewModelContext import com.airbnb.mvrx.Fail -import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.Loading -import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized -import com.airbnb.mvrx.ViewModelContext import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.mvrx.runCatchingToAsync import im.vector.app.core.platform.VectorViewModel import im.vector.app.features.session.coroutineScope @@ -56,19 +54,11 @@ class SpaceManageRoomsViewModel @AssistedInject constructor( } @AssistedFactory - interface Factory { - fun create(initialState: SpaceManageRoomViewState): SpaceManageRoomsViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: SpaceManageRoomViewState): SpaceManageRoomsViewModel } - companion object : MvRxViewModelFactory { - override fun create(viewModelContext: ViewModelContext, state: SpaceManageRoomViewState): SpaceManageRoomsViewModel? { - 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") - } - } + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() override fun handle(action: SpaceManageRoomViewAction) { when (action) { diff --git a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageSharedViewModel.kt b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageSharedViewModel.kt index 8f23788d19..bedd1873e8 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageSharedViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageSharedViewModel.kt @@ -16,13 +16,12 @@ package im.vector.app.features.spaces.manage -import com.airbnb.mvrx.ActivityViewModelContext -import com.airbnb.mvrx.FragmentViewModelContext -import com.airbnb.mvrx.MvRxViewModelFactory -import com.airbnb.mvrx.ViewModelContext +import com.airbnb.mvrx.MavericksViewModelFactory import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import org.matrix.android.sdk.api.session.Session @@ -33,19 +32,11 @@ class SpaceManageSharedViewModel @AssistedInject constructor( ) : VectorViewModel(initialState) { @AssistedFactory - interface Factory { - fun create(initialState: SpaceManageViewState): SpaceManageSharedViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: SpaceManageViewState): SpaceManageSharedViewModel } - companion object : MvRxViewModelFactory { - override fun create(viewModelContext: ViewModelContext, state: SpaceManageViewState): SpaceManageSharedViewModel? { - 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") - } - } + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() override fun handle(action: SpaceManagedSharedAction) { when (action) { diff --git a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageViewState.kt b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageViewState.kt index 35596f0884..82abc823c3 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageViewState.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageViewState.kt @@ -16,7 +16,7 @@ package im.vector.app.features.spaces.manage -import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MavericksState enum class ManageType { AddRooms, @@ -27,7 +27,7 @@ enum class ManageType { data class SpaceManageViewState( val spaceId: String = "", val manageType: ManageType -) : MvRxState { +) : MavericksState { constructor(args: SpaceManageArgs) : this( spaceId = args.spaceId, manageType = args.manageType diff --git a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceSettingsFragment.kt b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceSettingsFragment.kt index 5e5eb50b87..c2ab015858 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceSettingsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceSettingsFragment.kt @@ -59,11 +59,9 @@ import javax.inject.Inject class SpaceSettingsFragment @Inject constructor( private val epoxyController: SpaceSettingsController, private val colorProvider: ColorProvider, - val viewModelFactory: RoomSettingsViewModel.Factory, private val avatarRenderer: AvatarRenderer, private val drawableProvider: DrawableProvider ) : VectorBaseFragment(), - RoomSettingsViewModel.Factory, SpaceSettingsController.Callback, GalleryOrCameraDialogHelper.Listener, OnBackPressed { @@ -81,10 +79,6 @@ class SpaceSettingsFragment @Inject constructor( override fun getMenuRes() = R.menu.vector_room_settings - override fun create(initialState: RoomSettingsViewState): RoomSettingsViewModel { - return viewModelFactory.create(initialState) - } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) setupToolbar(views.roomSettingsToolbar) diff --git a/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleActivity.kt b/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleActivity.kt index b4b48d7710..1f08802137 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleActivity.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleActivity.kt @@ -21,7 +21,8 @@ import android.content.Intent import android.os.Bundle import androidx.core.view.isGone import androidx.core.view.isVisible -import com.airbnb.mvrx.MvRx +import com.airbnb.mvrx.Mavericks +import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R import im.vector.app.core.extensions.commitTransaction import im.vector.app.core.extensions.hideKeyboard @@ -30,6 +31,7 @@ import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.databinding.ActivitySimpleLoadingBinding import im.vector.app.features.spaces.share.ShareSpaceBottomSheet +@AndroidEntryPoint class SpacePeopleActivity : VectorBaseActivity() { override fun getBinding() = ActivitySimpleLoadingBinding.inflate(layoutInflater) @@ -57,14 +59,14 @@ class SpacePeopleActivity : VectorBaseActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - val args = intent?.getParcelableExtra(MvRx.KEY_ARG) + val args = intent?.getParcelableExtra(Mavericks.KEY_ARG) if (isFirstCreation()) { val simpleName = SpacePeopleFragment::class.java.simpleName if (supportFragmentManager.findFragmentByTag(simpleName) == null) { supportFragmentManager.commitTransaction { replace(R.id.simpleFragmentContainer, SpacePeopleFragment::class.java, - Bundle().apply { this.putParcelable(MvRx.KEY_ARG, args) }, + Bundle().apply { this.putParcelable(Mavericks.KEY_ARG, args) }, simpleName ) } @@ -97,7 +99,7 @@ class SpacePeopleActivity : VectorBaseActivity() { companion object { fun newIntent(context: Context, spaceId: String): Intent { return Intent(context, SpacePeopleActivity::class.java).apply { - putExtra(MvRx.KEY_ARG, GenericIdArgs(spaceId)) + putExtra(Mavericks.KEY_ARG, GenericIdArgs(spaceId)) } } } diff --git a/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleFragment.kt b/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleFragment.kt index e1629d5dc1..6e14893f77 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleFragment.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleFragment.kt @@ -37,21 +37,16 @@ import im.vector.app.core.resources.DrawableProvider import im.vector.app.databinding.FragmentRecyclerviewWithSearchBinding import im.vector.app.features.roomprofile.members.RoomMemberListAction import im.vector.app.features.roomprofile.members.RoomMemberListViewModel -import im.vector.app.features.roomprofile.members.RoomMemberListViewState import io.reactivex.rxkotlin.subscribeBy import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary import java.util.concurrent.TimeUnit import javax.inject.Inject class SpacePeopleFragment @Inject constructor( - private val viewModelFactory: SpacePeopleViewModel.Factory, - private val roomMemberModelFactory: RoomMemberListViewModel.Factory, private val drawableProvider: DrawableProvider, private val colorProvider: ColorProvider, private val epoxyController: SpacePeopleListController ) : VectorBaseFragment(), - SpacePeopleViewModel.Factory, - RoomMemberListViewModel.Factory, OnBackPressed, SpacePeopleListController.InteractionListener { private val viewModel by fragmentViewModel(SpacePeopleViewModel::class) @@ -66,14 +61,6 @@ class SpacePeopleFragment @Inject constructor( return true } - override fun create(initialState: SpacePeopleViewState): SpacePeopleViewModel { - return viewModelFactory.create(initialState) - } - - override fun create(initialState: RoomMemberListViewState): RoomMemberListViewModel { - return roomMemberModelFactory.create(initialState) - } - override fun invalidate() = withState(membersViewModel) { memberListState -> views.appBarTitle.text = getString(R.string.bottom_action_people) val memberCount = (memberListState.roomSummary.invoke()?.otherMemberIds?.size ?: 0) + 1 diff --git a/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleViewModel.kt b/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleViewModel.kt index 95cf5fb461..55d1dbe61e 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleViewModel.kt @@ -16,17 +16,15 @@ package im.vector.app.features.spaces.people -import androidx.lifecycle.viewModelScope -import com.airbnb.mvrx.ActivityViewModelContext import com.airbnb.mvrx.Fail -import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.Loading -import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.Success -import com.airbnb.mvrx.ViewModelContext import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import im.vector.app.features.raw.wellknown.getElementWellknown @@ -44,19 +42,11 @@ class SpacePeopleViewModel @AssistedInject constructor( ) : VectorViewModel(initialState) { @AssistedFactory - interface Factory { - fun create(initialState: SpacePeopleViewState): SpacePeopleViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: SpacePeopleViewState): SpacePeopleViewModel } - companion object : MvRxViewModelFactory { - override fun create(viewModelContext: ViewModelContext, state: SpacePeopleViewState): SpacePeopleViewModel? { - 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") - } - } + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() override fun handle(action: SpacePeopleViewAction) { when (action) { diff --git a/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleViewState.kt b/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleViewState.kt index ea322e3fbd..b24636a9d4 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleViewState.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleViewState.kt @@ -17,14 +17,14 @@ package im.vector.app.features.spaces.people import com.airbnb.mvrx.Async -import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.Uninitialized import im.vector.app.core.platform.GenericIdArgs data class SpacePeopleViewState( val spaceId: String, val createAndInviteState: Async = Uninitialized -) : MvRxState { +) : MavericksState { constructor(args: GenericIdArgs) : this( spaceId = args.id ) diff --git a/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewFragment.kt b/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewFragment.kt index eb02ed7c2d..7e08d7c924 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewFragment.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewFragment.kt @@ -49,10 +49,9 @@ data class SpacePreviewArgs( ) : Parcelable class SpacePreviewFragment @Inject constructor( - private val viewModelFactory: SpacePreviewViewModel.Factory, private val avatarRenderer: AvatarRenderer, private val epoxyController: SpacePreviewController -) : VectorBaseFragment(), SpacePreviewViewModel.Factory { +) : VectorBaseFragment() { private val viewModel by fragmentViewModel(SpacePreviewViewModel::class) lateinit var sharedActionViewModel: SpacePreviewSharedActionViewModel @@ -66,8 +65,6 @@ class SpacePreviewFragment @Inject constructor( sharedActionViewModel = activityViewModelProvider.get(SpacePreviewSharedActionViewModel::class.java) } - override fun create(initialState: SpacePreviewState) = viewModelFactory.create(initialState) - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) diff --git a/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewState.kt b/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewState.kt index d31d05cf96..14f9a45d68 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewState.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewState.kt @@ -17,7 +17,7 @@ package im.vector.app.features.spaces.preview import com.airbnb.mvrx.Async -import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.Uninitialized data class SpacePreviewState( @@ -28,7 +28,7 @@ data class SpacePreviewState( val spaceInfo: Async = Uninitialized, val childInfoList: Async> = Uninitialized, val inviteTermination: Async = Uninitialized -) : MvRxState { +) : MavericksState { constructor(args: SpacePreviewArgs) : this(idOrAlias = args.idOrAlias) } diff --git a/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewViewEvents.kt b/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewViewEvents.kt index 2f0eddb189..8f85008c97 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewViewEvents.kt @@ -19,7 +19,7 @@ package im.vector.app.features.spaces.preview import im.vector.app.core.platform.VectorViewEvents sealed class SpacePreviewViewEvents : VectorViewEvents { - object Dismiss: SpacePreviewViewEvents() - object JoinSuccess: SpacePreviewViewEvents() - data class JoinFailure(val message: String?): SpacePreviewViewEvents() + object Dismiss : SpacePreviewViewEvents() + object JoinSuccess : SpacePreviewViewEvents() + data class JoinFailure(val message: String?) : SpacePreviewViewEvents() } diff --git a/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewViewModel.kt b/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewViewModel.kt index 0f1afd8371..8d34ad94d8 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewViewModel.kt @@ -17,17 +17,16 @@ package im.vector.app.features.spaces.preview import androidx.lifecycle.viewModelScope -import com.airbnb.mvrx.ActivityViewModelContext import com.airbnb.mvrx.Fail -import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.Loading -import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized -import com.airbnb.mvrx.ViewModelContext import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.platform.VectorViewModel import kotlinx.coroutines.Dispatchers @@ -58,19 +57,11 @@ class SpacePreviewViewModel @AssistedInject constructor( } @AssistedFactory - interface Factory { - fun create(initialState: SpacePreviewState): SpacePreviewViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: SpacePreviewState): SpacePreviewViewModel } - companion object : MvRxViewModelFactory { - override fun create(viewModelContext: ViewModelContext, state: SpacePreviewState): SpacePreviewViewModel? { - 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") - } - } + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() override fun handle(action: SpacePreviewViewAction) { when (action) { diff --git a/vector/src/main/java/im/vector/app/features/spaces/preview/SpaceTabView.kt b/vector/src/main/java/im/vector/app/features/spaces/preview/SpaceTabView.kt index 675e7070e7..41fc8bf6b9 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/preview/SpaceTabView.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/preview/SpaceTabView.kt @@ -23,8 +23,8 @@ import im.vector.app.R class SpaceTabView constructor(context: Context, attrs: AttributeSet? = null, - defStyleAttr: Int = 0) - : LinearLayout(context, attrs, defStyleAttr) { + defStyleAttr: Int = 0) : + LinearLayout(context, attrs, defStyleAttr) { constructor(context: Context, attrs: AttributeSet) : this(context, attrs, 0) {} constructor(context: Context) : this(context, null, 0) {} diff --git a/vector/src/main/java/im/vector/app/features/spaces/share/ShareSpaceBottomSheet.kt b/vector/src/main/java/im/vector/app/features/spaces/share/ShareSpaceBottomSheet.kt index 4289af7b3b..6a98aa3cf8 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/share/ShareSpaceBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/share/ShareSpaceBottomSheet.kt @@ -25,17 +25,17 @@ import androidx.core.view.isVisible import androidx.fragment.app.FragmentManager import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState +import dagger.hilt.android.AndroidEntryPoint 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.VectorBaseBottomSheetDialogFragment import im.vector.app.core.utils.startSharePlainTextIntent import im.vector.app.databinding.BottomSheetSpaceInviteBinding import im.vector.app.features.invite.InviteUsersToRoomActivity import kotlinx.parcelize.Parcelize -import javax.inject.Inject -class ShareSpaceBottomSheet : VectorBaseBottomSheetDialogFragment(), ShareSpaceViewModel.Factory { +@AndroidEntryPoint +class ShareSpaceBottomSheet : VectorBaseBottomSheetDialogFragment() { @Parcelize data class Args( @@ -47,14 +47,6 @@ class ShareSpaceBottomSheet : VectorBaseBottomSheetDialogFragment(initialState) { @AssistedFactory - interface Factory { - fun create(initialState: ShareSpaceViewState): ShareSpaceViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: ShareSpaceViewState): ShareSpaceViewModel } - companion object : MvRxViewModelFactory { - override fun create(viewModelContext: ViewModelContext, state: ShareSpaceViewState): ShareSpaceViewModel? { - 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") - } - } + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() init { val roomSummary = session.getRoomSummary(initialState.spaceId) @@ -63,9 +56,9 @@ class ShareSpaceViewModel @AssistedInject constructor( private fun observePowerLevel() { val room = session.getRoom(initialState.spaceId) ?: return - PowerLevelsObservableFactory(room) - .createObservable() - .subscribe { powerLevelContent -> + PowerLevelsFlowFactory(room) + .createFlow() + .onEach { powerLevelContent -> val powerLevelsHelper = PowerLevelsHelper(powerLevelContent) setState { copy( @@ -73,7 +66,7 @@ class ShareSpaceViewModel @AssistedInject constructor( ) } } - .disposeOnClear() + .launchIn(viewModelScope) } override fun handle(action: ShareSpaceAction) { diff --git a/vector/src/main/java/im/vector/app/features/spaces/share/ShareSpaceViewState.kt b/vector/src/main/java/im/vector/app/features/spaces/share/ShareSpaceViewState.kt index 97606e9506..826719b762 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/share/ShareSpaceViewState.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/share/ShareSpaceViewState.kt @@ -17,7 +17,7 @@ package im.vector.app.features.spaces.share import com.airbnb.mvrx.Async -import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.Uninitialized import org.matrix.android.sdk.api.session.room.model.RoomSummary @@ -27,7 +27,7 @@ data class ShareSpaceViewState( val canInviteByMxId: Boolean = false, val canShareLink: Boolean = false, val postCreation: Boolean = false -) : MvRxState { +) : MavericksState { constructor(args: ShareSpaceBottomSheet.Args) : this( spaceId = args.spaceId, postCreation = args.postCreation diff --git a/vector/src/main/java/im/vector/app/features/sync/widget/SyncStateView.kt b/vector/src/main/java/im/vector/app/features/sync/widget/SyncStateView.kt index ff1da2f8f0..ce6df67d53 100755 --- a/vector/src/main/java/im/vector/app/features/sync/widget/SyncStateView.kt +++ b/vector/src/main/java/im/vector/app/features/sync/widget/SyncStateView.kt @@ -16,27 +16,41 @@ package im.vector.app.features.sync.widget +import android.annotation.SuppressLint import android.content.Context import android.util.AttributeSet -import android.widget.FrameLayout +import android.widget.LinearLayout import androidx.core.view.isVisible import im.vector.app.R import im.vector.app.core.utils.isAirplaneModeOn import im.vector.app.databinding.ViewSyncStateBinding - +import org.matrix.android.sdk.api.session.initsync.SyncStatusService import org.matrix.android.sdk.api.session.sync.SyncState -class SyncStateView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) - : FrameLayout(context, attrs, defStyle) { +class SyncStateView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) : + LinearLayout(context, attrs, defStyle) { private val views: ViewSyncStateBinding init { inflate(context, R.layout.view_sync_state, this) views = ViewSyncStateBinding.bind(this) + orientation = VERTICAL } - fun render(newState: SyncState) { + @SuppressLint("SetTextI18n") + fun render(newState: SyncState, + incrementalSyncStatus: SyncStatusService.Status.IncrementalSyncStatus, + pushCounter: Int, + showDebugInfo: Boolean + ) { + views.syncStateDebugInfo.isVisible = showDebugInfo + if (showDebugInfo) { + views.syncStateDebugInfoText.text = + "Sync thread : ${newState.toHumanReadable()}\nSync request: ${incrementalSyncStatus.toHumanReadable()}" + views.syncStateDebugInfoPushCounter.text = + "Push: $pushCounter" + } views.syncStateProgressBar.isVisible = newState is SyncState.Running && newState.afterPause if (newState == SyncState.NoNetwork) { @@ -48,4 +62,26 @@ class SyncStateView @JvmOverloads constructor(context: Context, attrs: Attribute views.syncStateNoNetworkAirplane.isVisible = false } } + + private fun SyncState.toHumanReadable(): String { + return when (this) { + SyncState.Idle -> "Idle" + SyncState.InvalidToken -> "InvalidToken" + SyncState.Killed -> "Killed" + SyncState.Killing -> "Killing" + SyncState.NoNetwork -> "NoNetwork" + SyncState.Paused -> "Paused" + is SyncState.Running -> "$this" + } + } + + private fun SyncStatusService.Status.IncrementalSyncStatus.toHumanReadable(): String { + return when (this) { + SyncStatusService.Status.IncrementalSyncIdle -> "Idle" + is SyncStatusService.Status.IncrementalSyncParsing -> "Parsing ${this.rooms} room(s) ${this.toDevice} toDevice(s)" + SyncStatusService.Status.IncrementalSyncError -> "Error" + SyncStatusService.Status.IncrementalSyncDone -> "Done" + else -> "?" + } + } } diff --git a/vector/src/main/java/im/vector/app/features/terms/ReviewTermsActivity.kt b/vector/src/main/java/im/vector/app/features/terms/ReviewTermsActivity.kt index 02f25563b8..0efb6119af 100644 --- a/vector/src/main/java/im/vector/app/features/terms/ReviewTermsActivity.kt +++ b/vector/src/main/java/im/vector/app/features/terms/ReviewTermsActivity.kt @@ -20,8 +20,8 @@ import android.content.Context import android.content.Intent import com.airbnb.mvrx.viewModel import com.google.android.material.dialog.MaterialAlertDialogBuilder +import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R -import im.vector.app.core.di.ScreenComponent import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.replaceFragment @@ -29,18 +29,13 @@ import im.vector.app.core.platform.SimpleFragmentActivity import org.matrix.android.sdk.api.session.terms.TermsService import javax.inject.Inject +@AndroidEntryPoint class ReviewTermsActivity : SimpleFragmentActivity() { @Inject lateinit var errorFormatter: ErrorFormatter - @Inject lateinit var viewModelFactory: ReviewTermsViewModel.Factory private val reviewTermsViewModel: ReviewTermsViewModel by viewModel() - override fun injectWith(injector: ScreenComponent) { - super.injectWith(injector) - injector.inject(this) - } - override fun initUiAndData() { super.initUiAndData() diff --git a/vector/src/main/java/im/vector/app/features/terms/ReviewTermsFragment.kt b/vector/src/main/java/im/vector/app/features/terms/ReviewTermsFragment.kt index 7a97cbf8cd..cb76e5b31f 100644 --- a/vector/src/main/java/im/vector/app/features/terms/ReviewTermsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/terms/ReviewTermsFragment.kt @@ -33,7 +33,6 @@ import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.utils.openUrlInChromeCustomTab import im.vector.app.databinding.FragmentReviewTermsBinding - import org.matrix.android.sdk.api.session.terms.TermsService import javax.inject.Inject diff --git a/vector/src/main/java/im/vector/app/features/terms/ReviewTermsViewModel.kt b/vector/src/main/java/im/vector/app/features/terms/ReviewTermsViewModel.kt index 4ecad80876..9932efb11a 100644 --- a/vector/src/main/java/im/vector/app/features/terms/ReviewTermsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/terms/ReviewTermsViewModel.kt @@ -15,16 +15,15 @@ */ package im.vector.app.features.terms -import androidx.lifecycle.viewModelScope -import com.airbnb.mvrx.ActivityViewModelContext import com.airbnb.mvrx.Loading -import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized -import com.airbnb.mvrx.ViewModelContext import dagger.assisted.Assisted -import dagger.assisted.AssistedInject import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import kotlinx.coroutines.launch @@ -37,18 +36,11 @@ class ReviewTermsViewModel @AssistedInject constructor( ) : VectorViewModel(initialState) { @AssistedFactory - interface Factory { - fun create(initialState: ReviewTermsViewState): ReviewTermsViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: ReviewTermsViewState): ReviewTermsViewModel } - companion object : MvRxViewModelFactory { - - @JvmStatic - override fun create(viewModelContext: ViewModelContext, state: ReviewTermsViewState): ReviewTermsViewModel? { - val activity: ReviewTermsActivity = (viewModelContext as ActivityViewModelContext).activity() - return activity.viewModelFactory.create(state) - } - } + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() lateinit var termsArgs: ServiceTermsArgs diff --git a/vector/src/main/java/im/vector/app/features/terms/ReviewTermsViewState.kt b/vector/src/main/java/im/vector/app/features/terms/ReviewTermsViewState.kt index 20b09f0cd7..e87fd9620c 100644 --- a/vector/src/main/java/im/vector/app/features/terms/ReviewTermsViewState.kt +++ b/vector/src/main/java/im/vector/app/features/terms/ReviewTermsViewState.kt @@ -17,9 +17,9 @@ package im.vector.app.features.terms import com.airbnb.mvrx.Async -import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.Uninitialized data class ReviewTermsViewState( val termsList: Async> = Uninitialized -) : MvRxState +) : MavericksState diff --git a/vector/src/main/java/im/vector/app/features/themes/ThemeUtils.kt b/vector/src/main/java/im/vector/app/features/themes/ThemeUtils.kt index c4c9ca63ae..6c8ea0a3f9 100644 --- a/vector/src/main/java/im/vector/app/features/themes/ThemeUtils.kt +++ b/vector/src/main/java/im/vector/app/features/themes/ThemeUtils.kt @@ -71,8 +71,8 @@ object ThemeUtils { */ fun isLightTheme(context: Context): Boolean { val theme = getApplicationTheme(context) - return theme == THEME_LIGHT_VALUE - || (theme == SYSTEM_THEME_VALUE && !isSystemDarkTheme(context.resources)) + return theme == THEME_LIGHT_VALUE || + (theme == SYSTEM_THEME_VALUE && !isSystemDarkTheme(context.resources)) } /** diff --git a/vector/src/main/java/im/vector/app/features/usercode/ScanUserCodeFragment.kt b/vector/src/main/java/im/vector/app/features/usercode/ScanUserCodeFragment.kt index da9c6792ff..e70ffd0d76 100644 --- a/vector/src/main/java/im/vector/app/features/usercode/ScanUserCodeFragment.kt +++ b/vector/src/main/java/im/vector/app/features/usercode/ScanUserCodeFragment.kt @@ -37,13 +37,12 @@ import im.vector.app.core.utils.registerForPermissionsResult import im.vector.app.databinding.FragmentQrCodeScannerWithButtonBinding import im.vector.lib.multipicker.MultiPicker import im.vector.lib.multipicker.utils.ImageUtils - import me.dm7.barcodescanner.zxing.ZXingScannerView import org.matrix.android.sdk.api.extensions.tryOrNull import javax.inject.Inject -class ScanUserCodeFragment @Inject constructor() - : VectorBaseFragment(), +class ScanUserCodeFragment @Inject constructor() : + VectorBaseFragment(), ZXingScannerView.ResultHandler { override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentQrCodeScannerWithButtonBinding { diff --git a/vector/src/main/java/im/vector/app/features/usercode/ShowUserCodeFragment.kt b/vector/src/main/java/im/vector/app/features/usercode/ShowUserCodeFragment.kt index c451118813..b794b23d0e 100644 --- a/vector/src/main/java/im/vector/app/features/usercode/ShowUserCodeFragment.kt +++ b/vector/src/main/java/im/vector/app/features/usercode/ShowUserCodeFragment.kt @@ -30,7 +30,6 @@ import im.vector.app.core.utils.registerForPermissionsResult import im.vector.app.core.utils.startSharePlainTextIntent import im.vector.app.databinding.FragmentUserCodeShowBinding import im.vector.app.features.home.AvatarRenderer - import javax.inject.Inject class ShowUserCodeFragment @Inject constructor( diff --git a/vector/src/main/java/im/vector/app/features/usercode/UserCodeActivity.kt b/vector/src/main/java/im/vector/app/features/usercode/UserCodeActivity.kt index 0771a5d238..7fa7a45131 100644 --- a/vector/src/main/java/im/vector/app/features/usercode/UserCodeActivity.kt +++ b/vector/src/main/java/im/vector/app/features/usercode/UserCodeActivity.kt @@ -24,11 +24,12 @@ import android.widget.Toast import androidx.core.app.ActivityCompat import androidx.core.view.isVisible import androidx.fragment.app.Fragment -import com.airbnb.mvrx.MvRx +import androidx.fragment.app.FragmentManager +import com.airbnb.mvrx.Mavericks import com.airbnb.mvrx.viewModel import com.airbnb.mvrx.withState +import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R -import im.vector.app.core.di.ScreenComponent import im.vector.app.core.extensions.commitTransaction import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorBaseActivity @@ -36,15 +37,12 @@ import im.vector.app.core.utils.onPermissionDeniedSnackbar import im.vector.app.databinding.ActivitySimpleBinding import im.vector.app.features.matrixto.MatrixToBottomSheet import kotlinx.parcelize.Parcelize -import javax.inject.Inject import kotlin.reflect.KClass +@AndroidEntryPoint class UserCodeActivity : VectorBaseActivity(), - UserCodeSharedViewModel.Factory, MatrixToBottomSheet.InteractionListener { - @Inject lateinit var viewModelFactory: UserCodeSharedViewModel.Factory - val sharedViewModel: UserCodeSharedViewModel by viewModel() @Parcelize @@ -56,25 +54,38 @@ class UserCodeActivity : VectorBaseActivity(), override fun getCoordinatorLayout() = views.coordinatorLayout - override fun injectWith(injector: ScreenComponent) { - injector.inject(this) + private val fragmentLifecycleCallbacks = object : FragmentManager.FragmentLifecycleCallbacks() { + override fun onFragmentResumed(fm: FragmentManager, f: Fragment) { + if (f is MatrixToBottomSheet) { + f.interactionListener = this@UserCodeActivity + } + super.onFragmentResumed(fm, f) + } + + override fun onFragmentPaused(fm: FragmentManager, f: Fragment) { + if (f is MatrixToBottomSheet) { + f.interactionListener = null + } + super.onFragmentPaused(fm, f) + } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, false) if (isFirstCreation()) { // should be there early for shared element transition showFragment(ShowUserCodeFragment::class, Bundle.EMPTY) } - sharedViewModel.selectSubscribe(this, UserCodeState::mode) { mode -> + sharedViewModel.onEach(UserCodeState::mode) { mode -> when (mode) { UserCodeState.Mode.SHOW -> showFragment(ShowUserCodeFragment::class, Bundle.EMPTY) UserCodeState.Mode.SCAN -> showFragment(ScanUserCodeFragment::class, Bundle.EMPTY) is UserCodeState.Mode.RESULT -> { showFragment(ShowUserCodeFragment::class, Bundle.EMPTY) - MatrixToBottomSheet.withLink(mode.rawLink, this).show(supportFragmentManager, "MatrixToBottomSheet") + MatrixToBottomSheet.withLink(mode.rawLink).show(supportFragmentManager, "MatrixToBottomSheet") } } } @@ -97,6 +108,11 @@ class UserCodeActivity : VectorBaseActivity(), } } + override fun onDestroy() { + supportFragmentManager.unregisterFragmentLifecycleCallbacks(fragmentLifecycleCallbacks) + super.onDestroy() + } + private fun showFragment(fragmentClass: KClass, bundle: Bundle) { if (supportFragmentManager.findFragmentByTag(fragmentClass.simpleName) == null) { supportFragmentManager.commitTransaction { @@ -110,10 +126,12 @@ class UserCodeActivity : VectorBaseActivity(), } } - override fun navigateToRoom(roomId: String) { + override fun mxToBottomSheetNavigateToRoom(roomId: String) { navigator.openRoom(this, roomId) } + override fun mxToBottomSheetSwitchToSpace(spaceId: String) {} + override fun onBackPressed() = withState(sharedViewModel) { when (it.mode) { UserCodeState.Mode.SHOW -> super.onBackPressed() @@ -122,13 +140,10 @@ class UserCodeActivity : VectorBaseActivity(), }.exhaustive } - override fun create(initialState: UserCodeState) = - viewModelFactory.create(initialState) - companion object { fun newIntent(context: Context, userId: String): Intent { return Intent(context, UserCodeActivity::class.java).apply { - putExtra(MvRx.KEY_ARG, Args(userId)) + putExtra(Mavericks.KEY_ARG, Args(userId)) } } } diff --git a/vector/src/main/java/im/vector/app/features/usercode/UserCodeSharedViewModel.kt b/vector/src/main/java/im/vector/app/features/usercode/UserCodeSharedViewModel.kt index 071044fc8a..64bcf9cead 100644 --- a/vector/src/main/java/im/vector/app/features/usercode/UserCodeSharedViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/usercode/UserCodeSharedViewModel.kt @@ -16,15 +16,13 @@ package im.vector.app.features.usercode -import androidx.lifecycle.viewModelScope -import com.airbnb.mvrx.ActivityViewModelContext -import com.airbnb.mvrx.FragmentViewModelContext -import com.airbnb.mvrx.MvRxViewModelFactory -import com.airbnb.mvrx.ViewModelContext +import com.airbnb.mvrx.MavericksViewModelFactory import dagger.assisted.Assisted -import dagger.assisted.AssistedInject import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import im.vector.app.R +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider import im.vector.app.features.createdirect.DirectRoomHelper @@ -45,15 +43,7 @@ class UserCodeSharedViewModel @AssistedInject constructor( private val directRoomHelper: DirectRoomHelper, private val rawService: RawService) : VectorViewModel(initialState) { - companion object : MvRxViewModelFactory { - override fun create(viewModelContext: ViewModelContext, state: UserCodeState): UserCodeSharedViewModel? { - 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") - } - } + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() init { val user = session.getUser(initialState.userId) @@ -66,8 +56,8 @@ class UserCodeSharedViewModel @AssistedInject constructor( } @AssistedFactory - interface Factory { - fun create(initialState: UserCodeState): UserCodeSharedViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: UserCodeState): UserCodeSharedViewModel } override fun handle(action: UserCodeActions) { diff --git a/vector/src/main/java/im/vector/app/features/usercode/UserCodeState.kt b/vector/src/main/java/im/vector/app/features/usercode/UserCodeState.kt index c26da7c0a4..a323609344 100644 --- a/vector/src/main/java/im/vector/app/features/usercode/UserCodeState.kt +++ b/vector/src/main/java/im/vector/app/features/usercode/UserCodeState.kt @@ -16,7 +16,7 @@ package im.vector.app.features.usercode -import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MavericksState import org.matrix.android.sdk.api.util.MatrixItem data class UserCodeState( @@ -24,7 +24,7 @@ data class UserCodeState( val matrixItem: MatrixItem? = null, val shareLink: String? = null, val mode: Mode = Mode.SHOW -) : MvRxState { +) : MavericksState { sealed class Mode { object SHOW : Mode() object SCAN : Mode() diff --git a/vector/src/main/java/im/vector/app/features/userdirectory/InviteByEmailItem.kt b/vector/src/main/java/im/vector/app/features/userdirectory/InviteByEmailItem.kt new file mode 100644 index 0000000000..2258239bde --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/userdirectory/InviteByEmailItem.kt @@ -0,0 +1,58 @@ +/* + * 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.userdirectory + +import android.widget.ImageView +import android.widget.TextView +import androidx.core.view.isVisible +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.features.home.AvatarRenderer + +@EpoxyModelClass(layout = R.layout.item_invite_by_mail) +abstract class InviteByEmailItem : VectorEpoxyModel() { + + @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer + @EpoxyAttribute lateinit var foundItem: ThreePidUser + @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var clickListener: ClickListener? = null + @EpoxyAttribute var selected: Boolean = false + + override fun bind(holder: Holder) { + super.bind(holder) + holder.itemTitleText.text = foundItem.email + holder.checkedImageView.isVisible = false + holder.avatarImageView.isVisible = true + holder.view.setOnClickListener(clickListener) + if (selected) { + holder.checkedImageView.isVisible = true + holder.avatarImageView.isVisible = false + } else { + holder.checkedImageView.isVisible = false + holder.avatarImageView.isVisible = true + } + } + + class Holder : VectorEpoxyHolder() { + val itemTitleText by bind(R.id.itemTitle) + val avatarImageView by bind(R.id.itemAvatar) + val checkedImageView by bind(R.id.itemAvatarChecked) + } +} diff --git a/vector/src/main/java/im/vector/app/features/userdirectory/PendingSelection.kt b/vector/src/main/java/im/vector/app/features/userdirectory/PendingSelection.kt index 57f950b2c8..e754eb88f8 100644 --- a/vector/src/main/java/im/vector/app/features/userdirectory/PendingSelection.kt +++ b/vector/src/main/java/im/vector/app/features/userdirectory/PendingSelection.kt @@ -16,8 +16,10 @@ package im.vector.app.features.userdirectory +import im.vector.app.features.displayname.getBestName import org.matrix.android.sdk.api.session.identity.ThreePid import org.matrix.android.sdk.api.session.user.model.User +import org.matrix.android.sdk.api.util.toMatrixItem sealed class PendingSelection { data class UserPendingSelection(val user: User) : PendingSelection() @@ -25,7 +27,14 @@ sealed class PendingSelection { fun getBestName(): String { return when (this) { - is UserPendingSelection -> user.getBestName() + is UserPendingSelection -> user.toMatrixItem().getBestName() + is ThreePidPendingSelection -> threePid.value + } + } + + fun getMxId(): String { + return when (this) { + is UserPendingSelection -> user.userId is ThreePidPendingSelection -> threePid.value } } diff --git a/vector/src/main/java/im/vector/app/features/userdirectory/UserListAction.kt b/vector/src/main/java/im/vector/app/features/userdirectory/UserListAction.kt index 7835232b09..83829c1119 100644 --- a/vector/src/main/java/im/vector/app/features/userdirectory/UserListAction.kt +++ b/vector/src/main/java/im/vector/app/features/userdirectory/UserListAction.kt @@ -24,4 +24,5 @@ sealed class UserListAction : VectorViewModelAction { data class AddPendingSelection(val pendingSelection: PendingSelection) : UserListAction() data class RemovePendingSelection(val pendingSelection: PendingSelection) : UserListAction() object ComputeMatrixToLinkForSharing : UserListAction() + data class UpdateUserConsent(val consent: Boolean) : UserListAction() } diff --git a/vector/src/main/java/im/vector/app/features/userdirectory/UserListController.kt b/vector/src/main/java/im/vector/app/features/userdirectory/UserListController.kt index bc2ef1f694..147367c1da 100644 --- a/vector/src/main/java/im/vector/app/features/userdirectory/UserListController.kt +++ b/vector/src/main/java/im/vector/app/features/userdirectory/UserListController.kt @@ -26,9 +26,14 @@ import im.vector.app.core.epoxy.errorWithRetryItem import im.vector.app.core.epoxy.loadingItem import im.vector.app.core.epoxy.noResultItem import im.vector.app.core.error.ErrorFormatter +import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.StringProvider +import im.vector.app.core.ui.list.genericPillItem +import im.vector.app.features.displayname.getBestName import im.vector.app.features.home.AvatarRenderer +import me.gujun.android.span.span import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.identity.IdentityServiceError import org.matrix.android.sdk.api.session.identity.ThreePid import org.matrix.android.sdk.api.session.user.model.User import org.matrix.android.sdk.api.util.toMatrixItem @@ -37,6 +42,7 @@ import javax.inject.Inject class UserListController @Inject constructor(private val session: Session, private val avatarRenderer: AvatarRenderer, private val stringProvider: StringProvider, + private val colorProvider: ColorProvider, private val errorFormatter: ErrorFormatter) : EpoxyController() { private var state: UserListViewState? = null @@ -86,6 +92,119 @@ class UserListController @Inject constructor(private val session: Session, } } + when (val matchingEmail = currentState.matchingEmail) { + is Success -> { + matchingEmail()?.let { threePidUser -> + userListHeaderItem { + id("identity_server_result_header") + header(host.stringProvider.getString(R.string.discovery_section, currentState.configuredIdentityServer ?: "")) + } + val isSelected = currentState.pendingSelections.any { pendingSelection -> + when (pendingSelection) { + is PendingSelection.ThreePidPendingSelection -> { + when (pendingSelection.threePid) { + is ThreePid.Email -> pendingSelection.threePid.email == threePidUser.email + is ThreePid.Msisdn -> false + } + } + is PendingSelection.UserPendingSelection -> { + threePidUser.user != null && threePidUser.user.userId == pendingSelection.user.userId + } + } + } + if (threePidUser.user == null) { + inviteByEmailItem { + id("email_${threePidUser.email}") + foundItem(threePidUser) + selected(isSelected) + clickListener { + host.callback?.onThreePidClick(ThreePid.Email(threePidUser.email)) + } + } + } else { + userDirectoryUserItem { + id(threePidUser.user.userId) + selected(isSelected) + matrixItem(threePidUser.user.toMatrixItem().let { + it.copy( + displayName = "${it.getBestName()} [${threePidUser.email}]" + ) + }) + avatarRenderer(host.avatarRenderer) + clickListener { + host.callback?.onItemClick(threePidUser.user) + } + } + } + } + } + is Fail -> { + when (matchingEmail.error) { + is IdentityServiceError.UserConsentNotProvided -> { + genericPillItem { + id("consent_not_given") + text( + span { + span { + text = host.stringProvider.getString(R.string.settings_discovery_consent_notice_off) + } + +"\n" + span { + text = host.stringProvider.getString(R.string.settings_discovery_consent_action_give_consent) + textStyle = "bold" + textColor = host.colorProvider.getColorFromAttribute(R.attr.colorPrimary) + } + } + ) + itemClickAction { + host.callback?.giveIdentityServerConsent() + } + } + } + is IdentityServiceError.NoIdentityServerConfigured -> { + genericPillItem { + id("no_IDS") + imageRes(R.drawable.ic_info) + text( + span { + span { + text = host.stringProvider.getString(R.string.finish_setting_up_discovery) + textColor = host.colorProvider.getColorFromAttribute(R.attr.vctr_content_primary) + } + +"\n" + span { + text = host.stringProvider.getString(R.string.discovery_invite) + textColor = host.colorProvider.getColorFromAttribute(R.attr.vctr_content_secondary) + } + +"\n" + span { + text = host.stringProvider.getString(R.string.finish_setup) + textStyle = "bold" + textColor = host.colorProvider.getColorFromAttribute(R.attr.colorPrimary) + } + } + ) + itemClickAction { + host.callback?.onSetupDiscovery() + } + } + } + } + } + is Loading -> { + userListHeaderItem { + id("identity_server_result_header_loading") + header(host.stringProvider.getString(R.string.discovery_section, currentState.configuredIdentityServer ?: "")) + } + loadingItem { + id("is_loading") + } + } + else -> { + // nop + } + } + when (currentState.knownUsers) { is Uninitialized -> renderEmptyState() is Loading -> renderLoading() @@ -196,5 +315,7 @@ class UserListController @Inject constructor(private val session: Session, fun onItemClick(user: User) fun onMatrixIdClick(matrixId: String) fun onThreePidClick(threePid: ThreePid) + fun onSetupDiscovery() + fun giveIdentityServerConsent() } } diff --git a/vector/src/main/java/im/vector/app/features/userdirectory/UserListFragment.kt b/vector/src/main/java/im/vector/app/features/userdirectory/UserListFragment.kt index 6e6df7a7aa..aed134816a 100644 --- a/vector/src/main/java/im/vector/app/features/userdirectory/UserListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/userdirectory/UserListFragment.kt @@ -39,10 +39,12 @@ import im.vector.app.core.extensions.hideKeyboard import im.vector.app.core.extensions.setupAsSearch import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.utils.DimensionConverter +import im.vector.app.core.utils.showIdentityServerConsentDialog import im.vector.app.core.utils.startSharePlainTextIntent import im.vector.app.databinding.FragmentUserListBinding import im.vector.app.features.homeserver.HomeServerCapabilitiesViewModel - +import im.vector.app.features.navigation.SettingsActivityPayload +import im.vector.app.features.settings.VectorSettingsActivity import org.matrix.android.sdk.api.session.identity.ThreePid import org.matrix.android.sdk.api.session.user.model.User import javax.inject.Inject @@ -50,7 +52,6 @@ import javax.inject.Inject class UserListFragment @Inject constructor( private val userListController: UserListController, private val dimensionConverter: DimensionConverter, - val homeServerCapabilitiesViewModelFactory: HomeServerCapabilitiesViewModel.Factory ) : VectorBaseFragment(), UserListController.Callback { @@ -79,11 +80,11 @@ class UserListFragment @Inject constructor( setupRecyclerView() setupSearchView() - homeServerCapabilitiesViewModel.subscribe { + homeServerCapabilitiesViewModel.onEach { views.userListE2EbyDefaultDisabled.isVisible = !it.isE2EByDefault } - viewModel.selectSubscribe(this, UserListViewState::pendingSelections) { + viewModel.onEach(UserListViewState::pendingSelections) { renderSelectedUsers(it) } @@ -130,9 +131,6 @@ class UserListFragment @Inject constructor( } private fun setupSearchView() { - withState(viewModel) { - views.userListSearch.hint = getString(R.string.user_directory_search_hint) - } views.userListSearch .textChanges() .startWith(views.userListSearch.text) @@ -217,6 +215,25 @@ class UserListFragment @Inject constructor( viewModel.handle(UserListAction.AddPendingSelection(PendingSelection.ThreePidPendingSelection(threePid))) } + override fun onSetupDiscovery() { + navigator.openSettings( + requireContext(), + VectorSettingsActivity.EXTRA_DIRECT_ACCESS_DISCOVERY_SETTINGS + ) + } + + override fun giveIdentityServerConsent() { + withState(viewModel) { state -> + requireContext().showIdentityServerConsentDialog( + state.configuredIdentityServer, + policyLinkCallback = { + navigator.openSettings(requireContext(), SettingsActivityPayload.DiscoverySettings(expandIdentityPolicies = true)) + }, + consentCallBack = { viewModel.handle(UserListAction.UpdateUserConsent(true)) } + ) + } + } + override fun onUseQRCode() { view?.hideKeyboard() sharedActionViewModel.post(UserListSharedAction.AddByQrCode) diff --git a/vector/src/main/java/im/vector/app/features/userdirectory/UserListViewModel.kt b/vector/src/main/java/im/vector/app/features/userdirectory/UserListViewModel.kt index 5d5247ec06..fde69ce9ba 100644 --- a/vector/src/main/java/im/vector/app/features/userdirectory/UserListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/userdirectory/UserListViewModel.kt @@ -16,74 +16,123 @@ package im.vector.app.features.userdirectory -import com.airbnb.mvrx.ActivityViewModelContext -import com.airbnb.mvrx.FragmentViewModelContext -import com.airbnb.mvrx.MvRxViewModelFactory -import com.airbnb.mvrx.ViewModelContext -import com.jakewharton.rxrelay2.BehaviorRelay +import androidx.lifecycle.asFlow +import com.airbnb.mvrx.MavericksViewModelFactory +import com.airbnb.mvrx.Uninitialized import dagger.assisted.Assisted -import dagger.assisted.AssistedInject import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.extensions.exhaustive +import im.vector.app.core.extensions.isEmail import im.vector.app.core.extensions.toggle import im.vector.app.core.platform.VectorViewModel -import io.reactivex.Single -import io.reactivex.android.schedulers.AndroidSchedulers +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.sample import org.matrix.android.sdk.api.MatrixPatterns +import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.identity.IdentityServiceListener +import org.matrix.android.sdk.api.session.identity.ThreePid import org.matrix.android.sdk.api.session.profile.ProfileService import org.matrix.android.sdk.api.session.user.model.User import org.matrix.android.sdk.api.util.toMatrixItem -import org.matrix.android.sdk.api.util.toOptional -import org.matrix.android.sdk.rx.rx -import java.util.concurrent.TimeUnit -private typealias KnownUsersSearch = String -private typealias DirectoryUsersSearch = String +data class ThreePidUser( + val email: String, + val user: User? +) class UserListViewModel @AssistedInject constructor(@Assisted initialState: UserListViewState, - private val session: Session) - : VectorViewModel(initialState) { + private val session: Session) : + VectorViewModel(initialState) { - private val knownUsersSearch = BehaviorRelay.create() - private val directoryUsersSearch = BehaviorRelay.create() + private val knownUsersSearch = MutableStateFlow("") + private val directoryUsersSearch = MutableStateFlow("") + private val identityServerUsersSearch = MutableStateFlow("") @AssistedFactory - interface Factory { - fun create(initialState: UserListViewState): UserListViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: UserListViewState): UserListViewModel } - companion object : MvRxViewModelFactory { + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() - override fun create(viewModelContext: ViewModelContext, state: UserListViewState): UserListViewModel? { - val factory = when (viewModelContext) { - is FragmentViewModelContext -> viewModelContext.fragment as? Factory - is ActivityViewModelContext -> viewModelContext.activity as? Factory + private val identityServerListener = object : IdentityServiceListener { + override fun onIdentityServerChange() { + withState { + identityServerUsersSearch.tryEmit(it.searchTerm) + val identityServerURL = cleanISURL(session.identityService().getCurrentIdentityServerUrl()) + setState { + copy(configuredIdentityServer = identityServerURL) + } } - return factory?.create(state) ?: error("You should let your activity/fragment implements Factory interface") } } init { observeUsers() + setState { + copy( + configuredIdentityServer = cleanISURL(session.identityService().getCurrentIdentityServerUrl()) + ) + } + session.identityService().addListener(identityServerListener) + } + + private fun cleanISURL(url: String?): String? { + return url?.removePrefix("https://") + } + + override fun onCleared() { + session.identityService().removeListener(identityServerListener) + super.onCleared() } override fun handle(action: UserListAction) { when (action) { - is UserListAction.SearchUsers -> handleSearchUsers(action.value) - is UserListAction.ClearSearchUsers -> handleClearSearchUsers() - is UserListAction.AddPendingSelection -> handleSelectUser(action) - is UserListAction.RemovePendingSelection -> handleRemoveSelectedUser(action) + is UserListAction.SearchUsers -> handleSearchUsers(action.value) + is UserListAction.ClearSearchUsers -> handleClearSearchUsers() + is UserListAction.AddPendingSelection -> handleSelectUser(action) + is UserListAction.RemovePendingSelection -> handleRemoveSelectedUser(action) UserListAction.ComputeMatrixToLinkForSharing -> handleShareMyMatrixToLink() + is UserListAction.UpdateUserConsent -> handleISUpdateConsent(action) }.exhaustive } + private fun handleISUpdateConsent(action: UserListAction.UpdateUserConsent) { + session.identityService().setUserConsent(action.consent) + withState { + identityServerUsersSearch.tryEmit(it.searchTerm) + } + } + private fun handleSearchUsers(searchTerm: String) { setState { - copy(searchTerm = searchTerm) + copy( + searchTerm = searchTerm + ) } - knownUsersSearch.accept(searchTerm) - directoryUsersSearch.accept(searchTerm) + if (searchTerm.isEmail().not()) { + // if it's not an email reset to uninitialized + // because the flow won't be triggered and result would stay + setState { + copy( + matchingEmail = Uninitialized + ) + } + } + identityServerUsersSearch.tryEmit(searchTerm) + knownUsersSearch.tryEmit(searchTerm) + directoryUsersSearch.tryEmit(searchTerm) } private fun handleShareMyMatrixToLink() { @@ -93,80 +142,93 @@ class UserListViewModel @AssistedInject constructor(@Assisted initialState: User } private fun handleClearSearchUsers() { - knownUsersSearch.accept("") - directoryUsersSearch.accept("") + knownUsersSearch.tryEmit("") + directoryUsersSearch.tryEmit("") + identityServerUsersSearch.tryEmit("") setState { copy(searchTerm = "") } } private fun observeUsers() = withState { state -> + identityServerUsersSearch + .filter { it.isEmail() } + .sample(300) + .onEach { search -> + executeSearchEmail(search) + }.launchIn(viewModelScope) + knownUsersSearch - .throttleLast(300, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .switchMap { - session.rx().livePagedUsers(it, state.excludedUserIds) - } - .execute { async -> - copy(knownUsers = async) + .sample(300) + .flowOn(Dispatchers.Main) + .flatMapLatest { search -> + session.getPagedUsersLive(search, state.excludedUserIds).asFlow() + }.execute { + copy(knownUsers = it) } directoryUsersSearch - .debounce(300, TimeUnit.MILLISECONDS) - .switchMapSingle { search -> - val stream = if (search.isBlank()) { - Single.just(emptyList()) - } else { - val searchObservable = session.rx() - .searchUsersDirectory(search, 50, state.excludedUserIds.orEmpty()) - .map { users -> - users.sortedBy { it.toMatrixItem().firstLetterOfDisplayName() } - } - // If it's a valid user id try to use Profile API - // because directory only returns users that are in public rooms or share a room with you, where as - // profile will work other federations - if (!MatrixPatterns.isUserId(search)) { - searchObservable - } else { - val profileObservable = session.rx().getProfileInfo(search) - .map { json -> - User( - userId = search, - displayName = json[ProfileService.DISPLAY_NAME_KEY] as? String, - avatarUrl = json[ProfileService.AVATAR_URL_KEY] as? String - ).toOptional() - } - .onErrorReturn { - // Profile API can be restricted and doesn't have to return result. - // In this case allow inviting valid user ids. - User( - userId = search, - displayName = null, - avatarUrl = null - ).toOptional() - } + .debounce(300) + .onEach { search -> + executeSearchDirectory(state, search) + }.launchIn(viewModelScope) + } - Single.zip( - searchObservable, - profileObservable, - { searchResults, optionalProfile -> - val profile = optionalProfile.getOrNull() ?: return@zip searchResults - val searchContainsProfile = searchResults.any { it.userId == profile.userId } - if (searchContainsProfile) { - searchResults - } else { - listOf(profile) + searchResults - } - } + private suspend fun executeSearchEmail(search: String) { + suspend { + val params = listOf(ThreePid.Email(search)) + val foundThreePid = tryOrNull { + session.identityService().lookUp(params).firstOrNull() + } + if (foundThreePid == null) { + null + } else { + try { + val json = session.getProfile(foundThreePid.matrixId) + ThreePidUser( + email = search, + user = User( + userId = foundThreePid.matrixId, + displayName = json[ProfileService.DISPLAY_NAME_KEY] as? String, + avatarUrl = json[ProfileService.AVATAR_URL_KEY] as? String ) - } - } - stream.toAsync { - copy(directoryUsers = it) - } + ) + } catch (failure: Throwable) { + ThreePidUser(email = search, user = User(foundThreePid.matrixId)) } - .subscribe() - .disposeOnClear() + } + }.execute { + copy(matchingEmail = it) + } + } + + private suspend fun executeSearchDirectory(state: UserListViewState, search: String) { + suspend { + if (search.isBlank()) { + emptyList() + } else { + val searchResult = session + .searchUsersDirectory(search, 50, state.excludedUserIds.orEmpty()) + .sortedBy { it.toMatrixItem().firstLetterOfDisplayName() } + val userProfile = if (MatrixPatterns.isUserId(search)) { + val json = tryOrNull { session.getProfile(search) } + User( + userId = search, + displayName = json?.get(ProfileService.DISPLAY_NAME_KEY) as? String, + avatarUrl = json?.get(ProfileService.AVATAR_URL_KEY) as? String + ) + } else { + null + } + if (userProfile == null || searchResult.any { it.userId == userProfile.userId }) { + searchResult + } else { + listOf(userProfile) + searchResult + } + } + }.execute { + copy(directoryUsers = it) + } } private fun handleSelectUser(action: UserListAction.AddPendingSelection) = withState { state -> diff --git a/vector/src/main/java/im/vector/app/features/userdirectory/UserListViewState.kt b/vector/src/main/java/im/vector/app/features/userdirectory/UserListViewState.kt index f1cbbd3b9d..e389bbefee 100644 --- a/vector/src/main/java/im/vector/app/features/userdirectory/UserListViewState.kt +++ b/vector/src/main/java/im/vector/app/features/userdirectory/UserListViewState.kt @@ -18,7 +18,7 @@ package im.vector.app.features.userdirectory import androidx.paging.PagedList import com.airbnb.mvrx.Async -import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.Uninitialized import im.vector.app.core.contacts.MappedContact import org.matrix.android.sdk.api.session.user.model.User @@ -27,13 +27,15 @@ data class UserListViewState( val excludedUserIds: Set? = null, val knownUsers: Async> = Uninitialized, val directoryUsers: Async> = Uninitialized, + val matchingEmail: Async = Uninitialized, val filteredMappedContacts: List = emptyList(), val pendingSelections: Set = emptySet(), val searchTerm: String = "", val singleSelection: Boolean, + val configuredIdentityServer: String? = null, private val showInviteActions: Boolean, val showContactBookAction: Boolean -) : MvRxState { +) : MavericksState { constructor(args: UserListFragmentArgs) : this( excludedUserIds = args.excludedUserIds, diff --git a/vector/src/main/java/im/vector/app/features/voice/AbstractVoiceRecorder.kt b/vector/src/main/java/im/vector/app/features/voice/AbstractVoiceRecorder.kt index 8a0f829f94..786920aa22 100644 --- a/vector/src/main/java/im/vector/app/features/voice/AbstractVoiceRecorder.kt +++ b/vector/src/main/java/im/vector/app/features/voice/AbstractVoiceRecorder.kt @@ -26,17 +26,15 @@ abstract class AbstractVoiceRecorder( context: Context, private val filenameExt: String ) : VoiceRecorder { - private val outputDirectory = File(context.cacheDir, "voice_records") + private val outputDirectory: File by lazy { + File(context.cacheDir, "voice_records").also { + it.mkdirs() + } + } private var mediaRecorder: MediaRecorder? = null private var outputFile: File? = null - init { - if (!outputDirectory.exists()) { - outputDirectory.mkdirs() - } - } - abstract fun setOutputFormat(mediaRecorder: MediaRecorder) abstract fun convertFile(recordedFile: File?): File? diff --git a/vector/src/main/java/im/vector/app/features/voice/VoicePlayerHelper.kt b/vector/src/main/java/im/vector/app/features/voice/VoicePlayerHelper.kt index f1b316c456..d2f7927d75 100644 --- a/vector/src/main/java/im/vector/app/features/voice/VoicePlayerHelper.kt +++ b/vector/src/main/java/im/vector/app/features/voice/VoicePlayerHelper.kt @@ -27,11 +27,9 @@ import javax.inject.Inject class VoicePlayerHelper @Inject constructor( context: Context ) { - private val outputDirectory = File(context.cacheDir, "voice_records") - - init { - if (!outputDirectory.exists()) { - outputDirectory.mkdirs() + private val outputDirectory: File by lazy { + File(context.cacheDir, "voice_records").also { + it.mkdirs() } } diff --git a/vector/src/main/java/im/vector/app/features/webview/ConsentWebViewEventListener.kt b/vector/src/main/java/im/vector/app/features/webview/ConsentWebViewEventListener.kt index b9ad28c2df..a3d4902b41 100644 --- a/vector/src/main/java/im/vector/app/features/webview/ConsentWebViewEventListener.kt +++ b/vector/src/main/java/im/vector/app/features/webview/ConsentWebViewEventListener.kt @@ -31,8 +31,8 @@ private const val RIOT_BOT_ID = "@riot-bot:matrix.org" */ class ConsentWebViewEventListener(activity: VectorBaseActivity<*>, private val session: Session, - private val delegate: WebViewEventListener) - : WebViewEventListener by delegate { + private val delegate: WebViewEventListener) : + WebViewEventListener by delegate { private val safeActivity: VectorBaseActivity<*>? by weak(activity) diff --git a/vector/src/main/java/im/vector/app/features/webview/VectorWebViewActivity.kt b/vector/src/main/java/im/vector/app/features/webview/VectorWebViewActivity.kt index ab8af20063..ab7913a99c 100644 --- a/vector/src/main/java/im/vector/app/features/webview/VectorWebViewActivity.kt +++ b/vector/src/main/java/im/vector/app/features/webview/VectorWebViewActivity.kt @@ -20,8 +20,8 @@ import android.content.Context import android.content.Intent import android.webkit.WebChromeClient import android.webkit.WebView -import androidx.annotation.CallSuper -import im.vector.app.core.di.ScreenComponent +import dagger.hilt.android.AndroidEntryPoint +import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.databinding.ActivityVectorWebViewBinding import org.matrix.android.sdk.api.session.Session @@ -33,15 +33,14 @@ import javax.inject.Inject * It relies on the VectorWebViewClient * This class shouldn't be extended. To add new behaviors, you might create a new WebViewMode and a new WebViewEventListener */ +@AndroidEntryPoint class VectorWebViewActivity : VectorBaseActivity() { override fun getBinding() = ActivityVectorWebViewBinding.inflate(layoutInflater) - @Inject lateinit var session: Session - - @CallSuper - override fun injectWith(injector: ScreenComponent) { - session = injector.activeSessionHolder().getActiveSession() + @Inject lateinit var activeSessionHolder: ActiveSessionHolder + val session: Session by lazy { + activeSessionHolder.getActiveSession() } override fun initUiAndData() { diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetActivity.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetActivity.kt index 9072957a95..a31edfcb02 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetActivity.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetActivity.kt @@ -19,12 +19,12 @@ package im.vector.app.features.widgets import android.app.Activity import android.content.Context import android.content.Intent -import com.google.android.material.appbar.MaterialToolbar import androidx.core.view.isVisible -import com.airbnb.mvrx.MvRx +import com.airbnb.mvrx.Mavericks import com.airbnb.mvrx.viewModel +import com.google.android.material.appbar.MaterialToolbar +import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R -import im.vector.app.core.di.ScreenComponent import im.vector.app.core.extensions.addFragment import im.vector.app.core.platform.ToolbarConfigurable import im.vector.app.core.platform.VectorBaseActivity @@ -32,16 +32,12 @@ import im.vector.app.databinding.ActivityWidgetBinding import im.vector.app.features.widgets.permissions.RoomWidgetPermissionBottomSheet import im.vector.app.features.widgets.permissions.RoomWidgetPermissionViewEvents import im.vector.app.features.widgets.permissions.RoomWidgetPermissionViewModel -import im.vector.app.features.widgets.permissions.RoomWidgetPermissionViewState - import org.matrix.android.sdk.api.session.events.model.Content import java.io.Serializable -import javax.inject.Inject +@AndroidEntryPoint class WidgetActivity : VectorBaseActivity(), - ToolbarConfigurable, - WidgetViewModel.Factory, - RoomWidgetPermissionViewModel.Factory { + ToolbarConfigurable { companion object { @@ -51,7 +47,7 @@ class WidgetActivity : VectorBaseActivity(), fun newIntent(context: Context, args: WidgetArgs): Intent { return Intent(context, WidgetActivity::class.java).apply { - putExtra(MvRx.KEY_ARG, args) + putExtra(Mavericks.KEY_ARG, args) } } @@ -67,9 +63,6 @@ class WidgetActivity : VectorBaseActivity(), } } - @Inject lateinit var viewModelFactory: WidgetViewModel.Factory - @Inject lateinit var permissionsViewModelFactory: RoomWidgetPermissionViewModel.Factory - private val viewModel: WidgetViewModel by viewModel() private val permissionViewModel: RoomWidgetPermissionViewModel by viewModel() @@ -79,12 +72,8 @@ class WidgetActivity : VectorBaseActivity(), override fun getTitleRes() = R.string.room_widget_activity_title - override fun injectWith(injector: ScreenComponent) { - injector.inject(this) - } - override fun initUiAndData() { - val widgetArgs: WidgetArgs? = intent?.extras?.getParcelable(MvRx.KEY_ARG) + val widgetArgs: WidgetArgs? = intent?.extras?.getParcelable(Mavericks.KEY_ARG) if (widgetArgs == null) { finish() return @@ -103,14 +92,14 @@ class WidgetActivity : VectorBaseActivity(), } } - viewModel.selectSubscribe(this, WidgetViewState::status) { ws -> + viewModel.onEach(WidgetViewState::status) { ws -> when (ws) { WidgetStatus.UNKNOWN -> { } WidgetStatus.WIDGET_NOT_ALLOWED -> { val dFrag = supportFragmentManager.findFragmentByTag(WIDGET_PERMISSION_FRAGMENT_TAG) as? RoomWidgetPermissionBottomSheet if (dFrag != null && dFrag.dialog?.isShowing == true && !dFrag.isRemoving) { - return@selectSubscribe + return@onEach } else { RoomWidgetPermissionBottomSheet .newInstance(widgetArgs) @@ -125,23 +114,15 @@ class WidgetActivity : VectorBaseActivity(), } } - viewModel.selectSubscribe(this, WidgetViewState::widgetName) { name -> + viewModel.onEach(WidgetViewState::widgetName) { name -> supportActionBar?.title = name } - viewModel.selectSubscribe(this, WidgetViewState::canManageWidgets) { + viewModel.onEach(WidgetViewState::canManageWidgets) { invalidateOptionsMenu() } } - override fun create(initialState: WidgetViewState): WidgetViewModel { - return viewModelFactory.create(initialState) - } - - override fun create(initialState: RoomWidgetPermissionViewState): RoomWidgetPermissionViewModel { - return permissionsViewModelFactory.create(initialState) - } - private fun handleClose(event: WidgetViewEvents.Close) { if (event.content != null) { val intent = createResultIntent(event.content) diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetPostAPIHandler.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetPostAPIHandler.kt index 7d2d89e1bd..99b3595d11 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetPostAPIHandler.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetPostAPIHandler.kt @@ -18,8 +18,8 @@ package im.vector.app.features.widgets import android.text.TextUtils import dagger.assisted.Assisted -import dagger.assisted.AssistedInject import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import im.vector.app.R import im.vector.app.core.resources.StringProvider import im.vector.app.features.session.coroutineScope diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetViewModel.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetViewModel.kt index bf27173d83..20fae6e31a 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetViewModel.kt @@ -17,20 +17,20 @@ package im.vector.app.features.widgets import android.net.Uri -import androidx.lifecycle.viewModelScope -import com.airbnb.mvrx.ActivityViewModelContext import com.airbnb.mvrx.Fail -import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.Loading -import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.Success -import com.airbnb.mvrx.ViewModelContext import dagger.assisted.Assisted -import dagger.assisted.AssistedInject import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider import im.vector.app.features.widgets.permissions.WidgetPermissionsHelper +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.Session @@ -41,36 +41,26 @@ import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerS import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper import org.matrix.android.sdk.api.session.widgets.WidgetManagementFailure -import org.matrix.android.sdk.rx.mapOptional -import org.matrix.android.sdk.rx.rx -import org.matrix.android.sdk.rx.unwrap +import org.matrix.android.sdk.flow.flow +import org.matrix.android.sdk.flow.mapOptional +import org.matrix.android.sdk.flow.unwrap import timber.log.Timber import javax.net.ssl.HttpsURLConnection class WidgetViewModel @AssistedInject constructor(@Assisted val initialState: WidgetViewState, widgetPostAPIHandlerFactory: WidgetPostAPIHandler.Factory, private val stringProvider: StringProvider, - private val session: Session) - : VectorViewModel(initialState), + private val session: Session) : + VectorViewModel(initialState), WidgetPostAPIHandler.NavigationCallback, IntegrationManagerService.Listener { @AssistedFactory - interface Factory { - fun create(initialState: WidgetViewState): WidgetViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: WidgetViewState): WidgetViewModel } - companion object : MvRxViewModelFactory { - - @JvmStatic - override fun create(viewModelContext: ViewModelContext, state: WidgetViewState): WidgetViewModel? { - 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") - } - } + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() private val room = session.getRoom(initialState.roomId) private val widgetService = session.widgetService() @@ -101,7 +91,7 @@ class WidgetViewModel @AssistedInject constructor(@Assisted val initialState: Wi } private fun subscribeToWidget() { - asyncSubscribe(WidgetViewState::asyncWidget) { + onAsync(WidgetViewState::asyncWidget) { setState { copy(widgetName = it.name) } } } @@ -118,16 +108,15 @@ class WidgetViewModel @AssistedInject constructor(@Assisted val initialState: Wi if (room == null) { return } - room.rx().liveStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.NoCondition) + room.flow().liveStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.NoCondition) .mapOptional { it.content.toModel() } .unwrap() .map { PowerLevelsHelper(it).isUserAllowedToSend(session.myUserId, true, null) } - .subscribe { - setState { copy(canManageWidgets = it) } + .setOnEach { + copy(canManageWidgets = it) } - .disposeOnClear() } private fun observeWidgetIfNeeded() { @@ -135,7 +124,7 @@ class WidgetViewModel @AssistedInject constructor(@Assisted val initialState: Wi return } val widgetId = initialState.widgetId ?: return - session.rx() + session.flow() .liveRoomWidgets(initialState.roomId, QueryStringValue.Equals(widgetId)) .filter { it.isNotEmpty() } .map { it.first() } diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetViewState.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetViewState.kt index 845ee81a2d..2d98f734dd 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetViewState.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetViewState.kt @@ -18,7 +18,7 @@ package im.vector.app.features.widgets import androidx.annotation.StringRes import com.airbnb.mvrx.Async -import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.Uninitialized import im.vector.app.R import org.matrix.android.sdk.api.session.widgets.model.Widget @@ -52,7 +52,7 @@ data class WidgetViewState( val widgetName: String = "", val canManageWidgets: Boolean = false, val asyncWidget: Async = Uninitialized -) : MvRxState { +) : MavericksState { constructor(widgetArgs: WidgetArgs) : this( widgetKind = widgetArgs.kind, diff --git a/vector/src/main/java/im/vector/app/features/widgets/permissions/RoomWidgetPermissionBottomSheet.kt b/vector/src/main/java/im/vector/app/features/widgets/permissions/RoomWidgetPermissionBottomSheet.kt index 4036195b65..ae3028925a 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/permissions/RoomWidgetPermissionBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/permissions/RoomWidgetPermissionBottomSheet.kt @@ -23,20 +23,20 @@ import android.text.style.BulletSpan import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import com.airbnb.mvrx.MvRx +import com.airbnb.mvrx.Mavericks import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.withState +import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R -import im.vector.app.core.di.ScreenComponent import im.vector.app.core.extensions.withArgs import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment import im.vector.app.databinding.BottomSheetRoomWidgetPermissionBinding import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.widgets.WidgetArgs - import org.matrix.android.sdk.api.util.toMatrixItem import javax.inject.Inject +@AndroidEntryPoint class RoomWidgetPermissionBottomSheet : VectorBaseBottomSheetDialogFragment() { @@ -50,10 +50,6 @@ class RoomWidgetPermissionBottomSheet : override val showExpanded = true - override fun injectWith(injector: ScreenComponent) { - injector.inject(this) - } - // Use this if you don't need the full activity view model var directListener: ((Boolean) -> Unit)? = null @@ -116,7 +112,7 @@ class RoomWidgetPermissionBottomSheet : companion object { fun newInstance(widgetArgs: WidgetArgs) = RoomWidgetPermissionBottomSheet().withArgs { - putParcelable(MvRx.KEY_ARG, widgetArgs) + putParcelable(Mavericks.KEY_ARG, widgetArgs) } } } diff --git a/vector/src/main/java/im/vector/app/features/widgets/permissions/RoomWidgetPermissionViewModel.kt b/vector/src/main/java/im/vector/app/features/widgets/permissions/RoomWidgetPermissionViewModel.kt index 844a6619b4..f29e6d1928 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/permissions/RoomWidgetPermissionViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/permissions/RoomWidgetPermissionViewModel.kt @@ -15,29 +15,29 @@ */ package im.vector.app.features.widgets.permissions -import androidx.lifecycle.viewModelScope -import com.airbnb.mvrx.ActivityViewModelContext -import com.airbnb.mvrx.FragmentViewModelContext -import com.airbnb.mvrx.MvRxViewModelFactory -import com.airbnb.mvrx.ViewModelContext +import com.airbnb.mvrx.MavericksViewModelFactory import dagger.assisted.Assisted -import dagger.assisted.AssistedInject import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import im.vector.app.R +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.platform.VectorViewModel +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.widgets.model.WidgetType -import org.matrix.android.sdk.rx.rx +import org.matrix.android.sdk.flow.flow import timber.log.Timber import java.net.URL class RoomWidgetPermissionViewModel @AssistedInject constructor(@Assisted val initialState: RoomWidgetPermissionViewState, - private val session: Session) - : VectorViewModel(initialState) { + private val session: Session) : + VectorViewModel(initialState) { private val widgetService = session.widgetService() private val integrationManagerService = session.integrationManagerService() @@ -48,7 +48,7 @@ class RoomWidgetPermissionViewModel @AssistedInject constructor(@Assisted val in private fun observeWidget() { val widgetId = initialState.widgetId ?: return - session.rx() + session.flow() .liveRoomWidgets(initialState.roomId, QueryStringValue.Equals(widgetId)) .filter { it.isNotEmpty() } .map { @@ -140,19 +140,9 @@ class RoomWidgetPermissionViewModel @AssistedInject constructor(@Assisted val in } @AssistedFactory - interface Factory { - fun create(initialState: RoomWidgetPermissionViewState): RoomWidgetPermissionViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: RoomWidgetPermissionViewState): RoomWidgetPermissionViewModel } - companion object : MvRxViewModelFactory { - - @JvmStatic - override fun create(viewModelContext: ViewModelContext, state: RoomWidgetPermissionViewState): RoomWidgetPermissionViewModel? { - 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") - } - } + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() } diff --git a/vector/src/main/java/im/vector/app/features/widgets/permissions/RoomWidgetPermissionViewState.kt b/vector/src/main/java/im/vector/app/features/widgets/permissions/RoomWidgetPermissionViewState.kt index 1cc14a91c2..79f9b8cee3 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/permissions/RoomWidgetPermissionViewState.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/permissions/RoomWidgetPermissionViewState.kt @@ -17,7 +17,7 @@ package im.vector.app.features.widgets.permissions import com.airbnb.mvrx.Async -import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.Uninitialized import im.vector.app.features.widgets.WidgetArgs import org.matrix.android.sdk.api.session.widgets.model.Widget @@ -26,7 +26,7 @@ data class RoomWidgetPermissionViewState( val roomId: String, val widgetId: String?, val permissionData: Async = Uninitialized -) : MvRxState { +) : MavericksState { constructor(widgetArgs: WidgetArgs) : this( roomId = widgetArgs.roomId, diff --git a/vector/src/main/java/im/vector/app/features/workers/signout/ServerBackupStatusViewModel.kt b/vector/src/main/java/im/vector/app/features/workers/signout/ServerBackupStatusViewModel.kt index 6d95911bdb..8fb5b27376 100644 --- a/vector/src/main/java/im/vector/app/features/workers/signout/ServerBackupStatusViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/workers/signout/ServerBackupStatusViewModel.kt @@ -17,39 +17,35 @@ package im.vector.app.features.workers.signout import androidx.lifecycle.MutableLiveData -import com.airbnb.mvrx.ActivityViewModelContext import com.airbnb.mvrx.Async -import com.airbnb.mvrx.FragmentViewModelContext -import com.airbnb.mvrx.MvRxState -import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.MavericksState +import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.Uninitialized -import com.airbnb.mvrx.ViewModelContext import dagger.assisted.Assisted -import dagger.assisted.AssistedInject import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.platform.EmptyAction import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel -import io.reactivex.Observable -import io.reactivex.functions.Function4 -import io.reactivex.subjects.PublishSubject +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.sample +import kotlinx.coroutines.launch import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.Session -import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME -import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo import org.matrix.android.sdk.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME import org.matrix.android.sdk.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupState import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupStateListener -import org.matrix.android.sdk.api.util.Optional -import org.matrix.android.sdk.internal.crypto.store.PrivateKeysInfo -import org.matrix.android.sdk.rx.rx -import java.util.concurrent.TimeUnit +import org.matrix.android.sdk.flow.flow data class ServerBackupStatusViewState( val bannerState: Async = Uninitialized -) : MvRxState +) : MavericksState /** * The state representing the view @@ -67,67 +63,51 @@ sealed class BannerState { } class ServerBackupStatusViewModel @AssistedInject constructor(@Assisted initialState: ServerBackupStatusViewState, - private val session: Session) - : VectorViewModel(initialState), KeysBackupStateListener { + private val session: Session) : + VectorViewModel(initialState), KeysBackupStateListener { @AssistedFactory - interface Factory { - fun create(initialState: ServerBackupStatusViewState): ServerBackupStatusViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: ServerBackupStatusViewState): ServerBackupStatusViewModel } - companion object : MvRxViewModelFactory { - - @JvmStatic - override fun create(viewModelContext: ViewModelContext, state: ServerBackupStatusViewState): ServerBackupStatusViewModel? { - 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") - } - } + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() // Keys exported manually val keysExportedToFile = MutableLiveData() val keysBackupState = MutableLiveData() - private val keyBackupPublishSubject: PublishSubject = PublishSubject.create() + private val keyBackupFlow = MutableSharedFlow(0) init { session.cryptoService().keysBackupService().addListener(this) - keysBackupState.value = session.cryptoService().keysBackupService().state - - Observable.combineLatest, Optional, KeysBackupState, Optional, BannerState>( - session.rx().liveUserAccountData(setOf(MASTER_KEY_SSSS_NAME, USER_SIGNING_KEY_SSSS_NAME, SELF_SIGNING_KEY_SSSS_NAME)), - session.rx().liveCrossSigningInfo(session.myUserId), - keyBackupPublishSubject, - session.rx().liveCrossSigningPrivateKeys(), - Function4 { _, crossSigningInfo, keyBackupState, pInfo -> - // first check if 4S is already setup - if (session.sharedSecretStorageService.isRecoverySetup()) { - // 4S is already setup sp we should not display anything - return@Function4 when (keyBackupState) { - KeysBackupState.BackingUp -> BannerState.BackingUp - else -> BannerState.Hidden - } - } - - // So recovery is not setup - // Check if cross signing is enabled and local secrets known - if ( - crossSigningInfo.getOrNull() == null - || (crossSigningInfo.getOrNull()?.isTrusted() == true - && pInfo.getOrNull()?.allKnown().orFalse()) - ) { - // So 4S is not setup and we have local secrets, - return@Function4 BannerState.Setup(numberOfKeys = getNumberOfKeysToBackup()) - } - - BannerState.Hidden + val liveUserAccountData = session.flow().liveUserAccountData(setOf(MASTER_KEY_SSSS_NAME, USER_SIGNING_KEY_SSSS_NAME, SELF_SIGNING_KEY_SSSS_NAME)) + val liveCrossSigningInfo = session.flow().liveCrossSigningInfo(session.myUserId) + val liveCrossSigningPrivateKeys = session.flow().liveCrossSigningPrivateKeys() + combine(liveUserAccountData, liveCrossSigningInfo, keyBackupFlow, liveCrossSigningPrivateKeys) { _, crossSigningInfo, keyBackupState, pInfo -> + // first check if 4S is already setup + if (session.sharedSecretStorageService.isRecoverySetup()) { + // 4S is already setup sp we should not display anything + return@combine when (keyBackupState) { + KeysBackupState.BackingUp -> BannerState.BackingUp + else -> BannerState.Hidden } - ) - .throttleLast(1000, TimeUnit.MILLISECONDS) // we don't want to flicker or catch transient states + } + + // So recovery is not setup + // Check if cross signing is enabled and local secrets known + if ( + crossSigningInfo.getOrNull() == null || + (crossSigningInfo.getOrNull()?.isTrusted() == true && + pInfo.getOrNull()?.allKnown().orFalse()) + ) { + // So 4S is not setup and we have local secrets, + return@combine BannerState.Setup(numberOfKeys = getNumberOfKeysToBackup()) + } + BannerState.Hidden + } + .sample(1000) // we don't want to flicker or catch transient states .distinctUntilChanged() .execute { async -> copy( @@ -135,7 +115,9 @@ class ServerBackupStatusViewModel @AssistedInject constructor(@Assisted initialS ) } - keyBackupPublishSubject.onNext(session.cryptoService().keysBackupService().state) + viewModelScope.launch { + keyBackupFlow.tryEmit(session.cryptoService().keysBackupService().state) + } } /** @@ -165,7 +147,9 @@ class ServerBackupStatusViewModel @AssistedInject constructor(@Assisted initialS } override fun onStateChange(newState: KeysBackupState) { - keyBackupPublishSubject.onNext(session.cryptoService().keysBackupService().state) + viewModelScope.launch { + keyBackupFlow.tryEmit(session.cryptoService().keysBackupService().state) + } keysBackupState.value = newState } diff --git a/vector/src/main/java/im/vector/app/features/workers/signout/SignOutBottomSheetDialogFragment.kt b/vector/src/main/java/im/vector/app/features/workers/signout/SignOutBottomSheetDialogFragment.kt index 48459cdd9e..5d38dac15f 100644 --- a/vector/src/main/java/im/vector/app/features/workers/signout/SignOutBottomSheetDialogFragment.kt +++ b/vector/src/main/java/im/vector/app/features/workers/signout/SignOutBottomSheetDialogFragment.kt @@ -31,8 +31,8 @@ import com.airbnb.mvrx.withState import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.dialog.MaterialAlertDialogBuilder +import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R -import im.vector.app.core.di.ScreenComponent import im.vector.app.core.dialogs.ExportKeysDialog import im.vector.app.core.extensions.queryExportKeys import im.vector.app.core.extensions.registerStartForActivityResult @@ -41,14 +41,12 @@ import im.vector.app.databinding.BottomSheetLogoutAndBackupBinding import im.vector.app.features.crypto.keysbackup.setup.KeysBackupSetupActivity import im.vector.app.features.crypto.recover.BootstrapBottomSheet import im.vector.app.features.crypto.recover.SetupMode - import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupState -import javax.inject.Inject // TODO this needs to be refactored to current standard and remove legacy +@AndroidEntryPoint class SignOutBottomSheetDialogFragment : - VectorBaseBottomSheetDialogFragment(), - SignoutCheckViewModel.Factory { + VectorBaseBottomSheetDialogFragment() { var onSignOut: Runnable? = null @@ -60,19 +58,8 @@ class SignOutBottomSheetDialogFragment : isCancelable = true } - @Inject - lateinit var viewModelFactory: SignoutCheckViewModel.Factory - - override fun create(initialState: SignoutCheckViewState): SignoutCheckViewModel { - return viewModelFactory.create(initialState) - } - private val viewModel: SignoutCheckViewModel by fragmentViewModel(SignoutCheckViewModel::class) - override fun injectWith(injector: ScreenComponent) { - injector.inject(this) - } - override fun onResume() { super.onResume() viewModel.refreshRemoteStateIfNeeded() diff --git a/vector/src/main/java/im/vector/app/features/workers/signout/SignOutUiWorker.kt b/vector/src/main/java/im/vector/app/features/workers/signout/SignOutUiWorker.kt index c5fa130d9b..59ea37036c 100644 --- a/vector/src/main/java/im/vector/app/features/workers/signout/SignOutUiWorker.kt +++ b/vector/src/main/java/im/vector/app/features/workers/signout/SignOutUiWorker.kt @@ -20,14 +20,14 @@ import androidx.fragment.app.FragmentActivity import com.google.android.material.dialog.MaterialAlertDialogBuilder import im.vector.app.R import im.vector.app.core.extensions.cannotLogoutSafely -import im.vector.app.core.extensions.vectorComponent +import im.vector.app.core.extensions.singletonEntryPoint import im.vector.app.features.MainActivity import im.vector.app.features.MainActivityArgs class SignOutUiWorker(private val activity: FragmentActivity) { fun perform() { - val session = activity.vectorComponent().activeSessionHolder().getSafeActiveSession() ?: return + val session = activity.singletonEntryPoint().activeSessionHolder().getSafeActiveSession() ?: return if (session.cannotLogoutSafely()) { // The backup check on logout flow has to be displayed if there are keys in the store, and the keys backup state is not Ready val signOutDialog = SignOutBottomSheetDialogFragment.newInstance() diff --git a/vector/src/main/java/im/vector/app/features/workers/signout/SignoutCheckViewModel.kt b/vector/src/main/java/im/vector/app/features/workers/signout/SignoutCheckViewModel.kt index df7a826b48..4daaef6fe1 100644 --- a/vector/src/main/java/im/vector/app/features/workers/signout/SignoutCheckViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/workers/signout/SignoutCheckViewModel.kt @@ -17,24 +17,24 @@ package im.vector.app.features.workers.signout import android.net.Uri -import androidx.lifecycle.viewModelScope -import com.airbnb.mvrx.ActivityViewModelContext import com.airbnb.mvrx.Async -import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.Loading -import com.airbnb.mvrx.MvRxState -import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.MavericksState +import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized -import com.airbnb.mvrx.ViewModelContext import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModelAction import im.vector.app.features.crypto.keys.KeysExporter +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME @@ -42,7 +42,7 @@ import org.matrix.android.sdk.api.session.crypto.crosssigning.SELF_SIGNING_KEY_S import org.matrix.android.sdk.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupState import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupStateListener -import org.matrix.android.sdk.rx.rx +import org.matrix.android.sdk.flow.flow import timber.log.Timber data class SignoutCheckViewState( @@ -51,7 +51,7 @@ data class SignoutCheckViewState( val crossSigningSetupAllKeysKnown: Boolean = false, val keysBackupState: KeysBackupState = KeysBackupState.Unknown, val hasBeenExportedToFile: Async = Uninitialized -) : MvRxState +) : MavericksState class SignoutCheckViewModel @AssistedInject constructor( @Assisted initialState: SignoutCheckViewState, @@ -65,21 +65,11 @@ class SignoutCheckViewModel @AssistedInject constructor( } @AssistedFactory - interface Factory { - fun create(initialState: SignoutCheckViewState): SignoutCheckViewModel + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: SignoutCheckViewState): SignoutCheckViewModel } - companion object : MvRxViewModelFactory { - - @JvmStatic - override fun create(viewModelContext: ViewModelContext, state: SignoutCheckViewState): SignoutCheckViewModel? { - 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") - } - } + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() init { session.cryptoService().keysBackupService().addListener(this) @@ -97,7 +87,7 @@ class SignoutCheckViewModel @AssistedInject constructor( ) } - session.rx().liveUserAccountData(setOf(MASTER_KEY_SSSS_NAME, USER_SIGNING_KEY_SSSS_NAME, SELF_SIGNING_KEY_SSSS_NAME)) + session.flow().liveUserAccountData(setOf(MASTER_KEY_SSSS_NAME, USER_SIGNING_KEY_SSSS_NAME, SELF_SIGNING_KEY_SSSS_NAME)) .map { session.sharedSecretStorageService.isRecoverySetup() } diff --git a/vector/src/main/res/drawable/circle_with_halo.xml b/vector/src/main/res/drawable/circle_with_halo.xml new file mode 100644 index 0000000000..8e44bfde7f --- /dev/null +++ b/vector/src/main/res/drawable/circle_with_halo.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/drawable/ic_jump_to_unread.xml b/vector/src/main/res/drawable/ic_jump_to_unread.xml index 2c5b8b90c1..3d13aabd50 100644 --- a/vector/src/main/res/drawable/ic_jump_to_unread.xml +++ b/vector/src/main/res/drawable/ic_jump_to_unread.xml @@ -5,6 +5,6 @@ android:viewportHeight="24"> diff --git a/vector/src/main/res/drawable/ic_presence_offline.xml b/vector/src/main/res/drawable/ic_presence_offline.xml new file mode 100644 index 0000000000..3f0dc251ce --- /dev/null +++ b/vector/src/main/res/drawable/ic_presence_offline.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/drawable/ic_presence_online.xml b/vector/src/main/res/drawable/ic_presence_online.xml new file mode 100644 index 0000000000..2184f359b2 --- /dev/null +++ b/vector/src/main/res/drawable/ic_presence_online.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + diff --git a/vector/src/main/res/drawable/ic_radio_on.xml b/vector/src/main/res/drawable/ic_radio_on.xml index 5cda8285e6..4ee05d690c 100644 --- a/vector/src/main/res/drawable/ic_radio_on.xml +++ b/vector/src/main/res/drawable/ic_radio_on.xml @@ -9,6 +9,6 @@ android:fillType="evenOdd"/> + android:fillColor="#FF0000"/> diff --git a/vector/src/main/res/drawable/ic_selected_community.xml b/vector/src/main/res/drawable/ic_selected_community.xml deleted file mode 100644 index e95b54aab3..0000000000 --- a/vector/src/main/res/drawable/ic_selected_community.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/vector/src/main/res/drawable/ic_voice_message_locked.xml b/vector/src/main/res/drawable/ic_voice_message_locked.xml index 2b92d9d5e0..a396f684b1 100644 --- a/vector/src/main/res/drawable/ic_voice_message_locked.xml +++ b/vector/src/main/res/drawable/ic_voice_message_locked.xml @@ -1,5 +1,5 @@ - + diff --git a/vector/src/main/res/drawable/ic_voice_mic_recording.xml b/vector/src/main/res/drawable/ic_voice_mic_recording.xml index eb6cea39a5..a57852c92f 100644 --- a/vector/src/main/res/drawable/ic_voice_mic_recording.xml +++ b/vector/src/main/res/drawable/ic_voice_mic_recording.xml @@ -3,14 +3,6 @@ android:height="52dp" android:viewportWidth="52" android:viewportHeight="52"> - - - + + + + + + + + + app:title="@string/invite_by_mxid_or_mail" /> - - - - + + android:orientation="vertical" + android:paddingStart="?dialogPreferredPadding" + android:paddingTop="12dp" + android:paddingEnd="?dialogPreferredPadding" + tools:ignore="SpUsage"> - + + - + - + - + - + - - \ No newline at end of file + + + + + diff --git a/vector/src/main/res/layout/fragment_room_detail.xml b/vector/src/main/res/layout/fragment_room_detail.xml index be4559d009..c0ac3170e5 100644 --- a/vector/src/main/res/layout/fragment_room_detail.xml +++ b/vector/src/main/res/layout/fragment_room_detail.xml @@ -45,19 +45,50 @@ + + + tools:ignore="MissingConstraints" + tools:layout_constraintCircleRadius="8dp" + tools:src="@drawable/ic_presence_offline" + tools:visibility="visible" /> + + + tools:text="@sample/rooms.json/data/topic" + tools:visibility="visible" /> @@ -98,7 +131,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" app:barrierDirection="top" - app:constraint_referenced_ids="composerLayout,notificationAreaView,failedMessagesWarningView" /> + app:constraint_referenced_ids="composerLayout,notificationAreaView,failedMessagesWarningStub" /> - + app:layout_constraintStart_toStartOf="parent" /> - + app:layout_constraintTop_toBottomOf="@+id/appBarLayout" /> + app:constraint_referenced_ids="composerLayout,notificationAreaView, failedMessagesWarningStub" /> + android:minHeight="0dp"> + + + + + + + + + + diff --git a/vector/src/main/res/layout/item_invite_by_mail.xml b/vector/src/main/res/layout/item_invite_by_mail.xml new file mode 100644 index 0000000000..1cd9e105cc --- /dev/null +++ b/vector/src/main/res/layout/item_invite_by_mail.xml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/layout/item_profile_matrix_item.xml b/vector/src/main/res/layout/item_profile_matrix_item.xml index 636113752b..cea2f62968 100644 --- a/vector/src/main/res/layout/item_profile_matrix_item.xml +++ b/vector/src/main/res/layout/item_profile_matrix_item.xml @@ -1,4 +1,5 @@ + + + + tools:ignore="MissingConstraints" + tools:src="@drawable/ic_presence_offline" + tools:visibility="visible" /> - +